# Dental Manager - Starter A monorepo setup to manage both Backend and Frontend of the Dental Manager application. ## ๐Ÿ”’ Security Setup (First Thing on a New PC) ### Step 1 โ€” Change the user password ```bash passwd ``` It will ask for the current password, then the new password twice. ### Step 2 โ€” Set the root password ```bash sudo passwd root ``` Enter a strong password. This prevents anyone from getting root access with a blank password. ### Step 3 โ€” Check that SSH is not running ```bash systemctl status sshd ``` If it shows "inactive (dead)" or "not found", you're safe โ€” no one can remotely access this PC via SSH. If it shows "active (running)", disable it: ```bash sudo systemctl stop sshd && sudo systemctl disable sshd ``` --- ## ๐Ÿ–ฅ๏ธ Setup Guide (Fresh Machine) Follow these steps in order after cloning the repository. ### Step 1 โ€” Enable auto-login (LXQt / SDDM) This office PC uses Debian with the LXQt desktop and the SDDM login manager. Set it to log straight into the desktop on boot, no password prompt. Create a drop-in config (replace `jj` with the actual Linux username if different): ```sh sudo mkdir -p /etc/sddm.conf.d sudo tee /etc/sddm.conf.d/autologin.conf > /dev/null <<'EOF' [Autologin] User=jj Session=lxqt.desktop EOF ``` Verify it was written: ```sh cat /etc/sddm.conf.d/autologin.conf ``` Reboot to apply โ€” the PC should boot straight to the desktop with no login screen. > If a different session is installed, list available sessions with `ls /usr/share/xsessions/` and use that filename instead of `lxqt.desktop`. ### Step 1a โ€” Remove gnome-keyring (fixes "Authentication required" popup) With autologin enabled, no password is ever typed at login, so `gnome-keyring` (the "Login" keyring) never gets unlocked automatically. This shows up as a recurring **"Authentication required โ€” The login keyring did not get unlocked when you logged into your computer"** popup whenever an app (Chrome, NetworkManager, etc.) tries to read a stored password. Since this PC doesn't need the keyring (no GNOME apps depending on it), remove it: ```sh sudo apt-get purge -y gnome-keyring gnome-keyring-pkcs11 libpam-gnome-keyring sudo apt-get autoremove -y sudo sed -i '/pam_gnome_keyring\.so/d' /etc/pam.d/sddm /etc/pam.d/common-password ``` Reboot to confirm the popup no longer appears. > Trade-off: Chrome saved passwords/cookie encryption falls back to its weaker "Basic" store, and NetworkManager Wi-Fi passwords may need to be re-entered once. If a future setup needs the keyring kept, unlock it silently instead by opening **Passwords and Keys** (`seahorse`) โ†’ right-click **Login** โ†’ **Change Password** โ†’ set both new-password fields blank. ### Step 2 โ€” Install Git ```sh sudo apt update sudo apt install -y git # Verify git --version ``` ### Step 3 โ€” Clone the repository ```sh git clone cd DentalManagementMHAprilgg ``` ### Step 4 โ€” Install Node.js Required to run the Backend and Frontend. ```sh curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt-get install -y nodejs # Verify node -v # should print v20.x.x npm -v ``` ### Step 5 โ€” Install Python Required to run the Selenium and OCR services. ```sh sudo apt-get install -y python3 python3-pip python3-venv # Verify python3 --version # should print 3.10 or higher ``` ### Step 6 โ€” Install Chrome Required for the Selenium service to control a browser. ```sh wget -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb -O /tmp/google-chrome-stable.deb sudo apt install -y /tmp/google-chrome-stable.deb # Verify google-chrome --version ``` > `apt-key` is removed on modern Debian (13+), so the old signing-key + repo method no longer works. `apt install` on the downloaded `.deb` pulls in dependencies automatically. > The `webdriver-manager` package (included in `requirements.txt`) automatically downloads the matching ChromeDriver โ€” no manual driver setup needed. ### Step 7 โ€” Install PostgreSQL Primary database for the application. ```sh sudo apt-get install -y postgresql postgresql-contrib sudo systemctl enable postgresql sudo systemctl start postgresql # Create the database the app uses sudo -u postgres psql -c "CREATE DATABASE dentalapp OWNER postgres;" # Set the postgres user password to match packages/db/.env sudo -u postgres psql -c "ALTER USER postgres WITH PASSWORD 'mypassword';" ``` #### Enable password authentication over TCP By default Debian uses `scram-sha-256` or `peer` auth for local connections, which blocks password login. Switch to `md5`: ```sh sudo sed -i 's/scram-sha-256/md5/g' /etc/postgresql/*/main/pg_hba.conf sudo systemctl restart postgresql ``` #### Verify ```sh psql -U postgres -d dentalapp -h 127.0.0.1 -W # Enter: mypassword # You should see: dentalapp=# ``` > The `DATABASE_URL` in `packages/db/.env` is already set to: > ``` > DATABASE_URL="postgresql://postgres:mypassword@localhost:5432/dentalapp" > ``` ### Step 8 โ€” Install Redis Used as the job queue for Selenium and OCR background tasks. ```sh sudo apt-get install -y redis-server sudo systemctl enable redis-server sudo systemctl start redis-server # Verify redis-cli ping # should print: PONG ``` ### Step 9 โ€” Install rclone Required for cloud backup destinations (Google Drive, Dropbox, etc.). 1. Open the file manager and navigate to the `scripts/` folder inside the project 2. Right-click `install-rclone.sh` โ†’ choose **Run as a program** or **Execute in Terminal** 3. A terminal window will open โ€” enter your password when prompted 4. Wait for it to finish โ€” it will print the installed version when done To verify it installed correctly, open a terminal and run: ```sh rclone version ``` ### Step 10 โ€” Install Node.js dependencies ```sh npm install ``` ### Step 11 โ€” Install Python dependencies Python dependencies are installed automatically by `npm install` (Step 10) via each service's `postinstall` script. Each service creates its own `.venv` virtual environment โ€” no manual pip commands needed. > This approach is required on Debian 13+ where system-wide pip installs are blocked (PEP 668). ### Step 12 โ€” Set up environment variables Copy the `.env.example` files and fill in the required values. ```sh npm run setup:env ``` ### Step 12a โ€” Set the Cloudflare subdomain for this office After running `npm run setup:env` (Step 12), open the two `.env` files and fill in this office's Cloudflare subdomain. **`apps/Frontend/.env`** ```env VITE_CLOUDFLARE_HOST=yoursubdomain.mydentalofficemanagement.com ``` **`apps/Backend/.env`** ```env CLOUDFLARE_HOST=yoursubdomain.mydentalofficemanagement.com FRONTEND_URLS=http://localhost:3000,http://yoursubdomain.mydentalofficemanagement.com,https://yoursubdomain.mydentalofficemanagement.com ``` Replace `yoursubdomain` with this office's actual subdomain (e.g. `summitdentalcare`). > If you skip this step, Cloudflare tunnel access will not work. LAN access still works without it. ### Step 13 โ€” Set up the database ```sh # Run migrations npm run db:migrate # Generate Prisma types npm run db:generate # Insert seed data npm run db:seed ``` ### Step 14 โ€” Configure nginx Install nginx: ```sh sudo apt update sudo apt install -y nginx # Verify nginx -v ``` > If Apache is already installed, it will also be bound to port 80/443 and nginx will fail to > start. Disable it first: > ```sh > sudo systemctl disable --now apache2 > ``` The repo includes `nginx.conf` in the project root. Install it as the active site config: ```sh sudo cp nginx.conf /etc/nginx/sites-available/dental-app sudo ln -sf /etc/nginx/sites-available/dental-app /etc/nginx/sites-enabled/dental-app sudo nginx -t && sudo systemctl reload nginx ``` > **Important:** The `/api/` location block must include `proxy_set_header Authorization $http_authorization;` > Without it, nginx strips the Authorization header and the backend returns "Access denied. No token provided." ### Step 15 โ€” Run the app Open two terminals: **Terminal 1** โ€” Backend + Frontend: ```sh npm run dev ``` > On first boot the server automatically seeds all AI chat templates, SMS templates, and greeting messages for every user โ€” no manual configuration needed. **Terminal 2** โ€” Selenium service: ```sh cd apps/SeleniumService .venv/bin/python3 agent.py ``` ### Step 16 โ€” Create desktop shortcuts (optional) Instead of opening two terminals manually every time, you can create desktop shortcuts that start and stop all services with a single double-click. Run this once after cloning and installing: ```sh bash ~/Desktop/DentalManagementMH06/setup-desktop-shortcut.sh ``` Two shortcuts will appear on your desktop: - **Dental App** โ€” starts the app. Double-clicking it opens: - A terminal running `npm run dev` (Backend + Frontend + Python services) - A terminal running the Selenium service - **Stop Dental App** โ€” stops all services and frees all ports (5000, 5001, 5002, 5003, 3000/3001) > No username or path editing needed โ€” the script automatically detects the current user's home folder. ### Step 17 โ€” Install RustDesk for remote support (optional) Lets you remotely access this PC for troubleshooting/support without needing SSH. ```sh wget -q https://github.com/rustdesk/rustdesk/releases/latest/download/rustdesk-1.4.9-x86_64.deb -O /tmp/rustdesk.deb sudo apt install -y /tmp/rustdesk.deb ``` > Check [github.com/rustdesk/rustdesk/releases](https://github.com/rustdesk/rustdesk/releases) for the latest version number if the link above has moved on. Get this machine's RustDesk ID: ```sh rustdesk --get-id ``` Set a permanent password for unattended access (skip this to require manual approval on each connection instead): ```sh rustdesk --password YOUR_STRONG_PASSWORD ``` On the connecting device, install RustDesk and enter this machine's ID and password to connect. ### Step 18 โ€” Auto-start Chrome and RustDesk on login (optional) Pairs well with [Step 1's auto-login](#step-1--enable-auto-login-lxqt--sddm) โ€” the PC boots straight to the desktop with Chrome open and the RustDesk window visible, no manual clicks needed. > The RustDesk **core service** (`rustdesk.service`) is already enabled at install time and runs at boot as root โ€” remote access works even before anyone logs in, and it auto-spawns its own tray icon for the logged-in session on its own. This step additionally opens the full RustDesk **window** (showing the ID/password) on login, which the service alone does not do. LXQt reads autostart entries from `~/.config/autostart/`. Create one `.desktop` file per app: ```sh mkdir -p ~/.config/autostart cat > ~/.config/autostart/google-chrome.desktop <<'EOF' [Desktop Entry] Type=Application Name=Google Chrome Exec=/usr/bin/google-chrome-stable Icon=google-chrome Terminal=false X-GNOME-Autostart-enabled=true EOF cat > ~/.config/autostart/rustdesk-tray.desktop <<'EOF' [Desktop Entry] Type=Application Name=RustDesk Tray Exec=/usr/bin/rustdesk Icon=rustdesk Terminal=false X-GNOME-Autostart-enabled=true EOF ``` Log out and back in (or reboot) to verify โ€” Chrome and the RustDesk window should both open on their own. > Set a permanent password (Step 17) instead of relying on the default one-time password โ€” a permanent password avoids the OTP-refresh confusion on autostart, where the window can appear to briefly show one password and then update to another as RustDesk finishes contacting the ID server. --- ## ๐Ÿ“– Developer Documentation - [Setting up server environment](docs/server-setup.md) โ€” the first step, to run this app in environment. - [Development Hosts & Ports](docs/ports.md) โ€” which app runs on which host/port, and how to configure `.env` for LAN or single-device access --- ## ๐ŸŒ Development Hosts & Ports This section defines the default host and port used by each app/service in this turborepo. Update it whenever a new service is added or a port is changed. (Full copy also kept in [docs/ports.md](docs/ports.md).) ### Frontend (React + Vite) - **Host:** `localhost` (default) โ€” use `0.0.0.0` if you need LAN access (phone/other device on same Wi-Fi) - **Port:** `3000` - **Access URLs:** - Local: http://localhost:3000 - LAN: `http://:3000` (only if `HOST=0.0.0.0`) - **Current setup:** Frontend runs on `0.0.0.0` and is accessible via the device IP. `.env` file (`apps/Frontend/.env`): ```env NODE_ENV=development HOST=0.0.0.0 PORT=3000 VITE_API_BASE_URL_BACKEND=http://192.168.1.8:5000 ``` `VITE_API_BASE_URL_BACKEND` should point at the Backend's `HOST`/`PORT` as seen from the browser โ€” use `localhost` for single-device access, or the machine's LAN IP for access from other devices. ### Backend (Express.js) - **Host:** `0.0.0.0` (all interfaces) - **Port:** `5000` - **Access URL:** http://localhost:5000 - **Current setup:** Runs for all network interfaces and allows the configured `FRONTEND_URLS` via CORS. `.env` file (`apps/Backend/.env`): ```env NODE_ENV="development" HOST=0.0.0.0 PORT=5000 FRONTEND_URLS=http://localhost:3000,http://192.168.1.8:3000 ``` ### ๐Ÿงพ Patient Data Extractor Service - **Host:** `localhost` - **Port:** `5001` - **Access URL:** http://localhost:5001 ### ๐ŸŒ Selenium Service - **Host:** `localhost` - **Port:** `5002` - **Access URL:** http://localhost:5002 ### ๐Ÿ’ณ Payment OCR Service - **Host:** `0.0.0.0` - **Port:** `5003` - **Access URL:** http://localhost:5003 ### ๐Ÿ“– Notes - These values come from per-app `.env` files (`apps//.env`). - `HOST` controls binding โ€” `localhost` = loopback only, `0.0.0.0` = all interfaces. - `PORT` controls the service's port. - Frontend uses variables prefixed with `VITE_` for client-side access (e.g. `VITE_API_BASE_URL_BACKEND`). - In production, ports and hosts may differ โ€” traffic is instead routed through the [Cloudflare Tunnel](#cloudflare-tunnel-setup-remote-access-per-office). --- ## This is a Turborepo. What's inside? ### Apps and Packages - `apps/Backend` โ€” Express.js API server - `apps/Frontend` โ€” React + Vite frontend - `apps/SeleniumService` โ€” Python FastAPI service for browser automation (insurance eligibility, claims) - `apps/PaymentOCRService` โ€” Python service for payment OCR extraction - `@repo/eslint-config` โ€” shared ESLint configuration - `@repo/typescript-config` โ€” shared `tsconfig.json`s Each package/app is 100% [TypeScript](https://www.typescriptlang.org/) (except the Python services). ### Utilities - [Tailwind CSS](https://tailwindcss.com/) for styles - [TypeScript](https://www.typescriptlang.org/) for static type checking - [ESLint](https://eslint.org/) for code linting - [Prettier](https://prettier.io) for code formatting --- ## Cloudflare Tunnel Setup (Remote Access per Office) This connects each office's local app to a public subdomain via a Cloudflare Tunnel โ€” no port forwarding needed, local network access is unchanged. **How it works:** - Local network: other office PCs reach the app directly at `http://:3000` - Internet: anyone reaches the app via `https://.mydentalofficemanagement.com` - Both paths hit the same app simultaneously with no conflict --- ### Step 1 โ€” Add domain to Cloudflare (done once for all offices) > Skip this step if the domain is already on Cloudflare. 1. Go to `dash.cloudflare.com` โ†’ **Add a site** โ†’ enter `mydentalofficemanagement.com` (free plan) 2. Cloudflare scans existing DNS records โ€” review and keep them 3. Cloudflare gives you 2 nameservers (e.g. `holly.ns.cloudflare.com`, `amir.ns.cloudflare.com`) 4. Log into **Ionos** โ†’ replace the domain's nameservers with Cloudflare's two 5. Wait 10โ€“30 min โ†’ Cloudflare emails you when active 6. DNSSEC: if not purchased on Ionos, it was never enabled โ€” nothing to turn off ### Step 2 โ€” Install `cloudflared` on the office PC Run this in a terminal on the office machine: ```bash curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o cloudflared.deb sudo dpkg -i cloudflared.deb cloudflared --version ``` ### Step 3 โ€” Authenticate with Cloudflare ```bash cloudflared tunnel login ``` A browser window opens โ†’ click `mydentalofficemanagement.com` โ†’ terminal shows success and saves a certificate to `~/.cloudflared/cert.pem`. ### Step 4 โ€” Create a tunnel for this office Use a unique tunnel name per office: ```bash cloudflared tunnel create # Example: cloudflared tunnel create summit-dental-app ``` Note the **tunnel ID** (UUID) printed โ€” you need it in Step 5. ### Step 5 โ€” Create the config file ```bash sudo mkdir -p /etc/cloudflared sudo nano /etc/cloudflared/config.yml ``` Paste (replace tunnel ID, credentials path, and subdomain for this office): ```yaml tunnel: credentials-file: /home/ee/.cloudflared/.json ingress: - hostname: .mydentalofficemanagement.com service: http://localhost:3000 - service: http_status:404 ``` Save: `Ctrl+O` โ†’ Enter โ†’ `Ctrl+X`. ### Step 6 โ€” Route DNS for this office's subdomain ```bash cloudflared tunnel route dns .mydentalofficemanagement.com ``` ### Step 7 โ€” Install as a system service (auto-starts on boot) ```bash sudo cloudflared service install sudo systemctl enable cloudflared sudo systemctl start cloudflared sudo systemctl status cloudflared ``` ### Step 8 โ€” Allow the subdomain in Vite In `apps/Frontend/vite.config.js`, add the subdomain to `server.allowedHosts` so Vite does not block external requests. ### Step 9 โ€” Allow the subdomain in backend CORS In `apps/Backend`, add the subdomain URL to the allowed CORS origins so login and API calls work from the public URL. --- ### Example โ€” Adding Summit Dental Care **Office subdomain:** `summitdentalcare.mydentalofficemanagement.com` **Step 1:** Already done (domain is on Cloudflare). **Step 2:** Install `cloudflared` on Summit Dental's PC. **Step 3:** Run `cloudflared tunnel login` on Summit Dental's PC. **Step 4:** ```bash cloudflared tunnel create summit-dental-app # Example output: Created tunnel summit-dental-app with id a1b2c3d4-... ``` **Step 5 โ€” `/etc/cloudflared/config.yml`:** ```yaml tunnel: a1b2c3d4-xxxx-xxxx-xxxx-xxxxxxxxxxxx credentials-file: /home/ee/.cloudflared/a1b2c3d4-xxxx-xxxx-xxxx-xxxxxxxxxxxx.json ingress: - hostname: summitdentalcare.mydentalofficemanagement.com service: http://localhost:3000 - service: http_status:404 ``` **Step 6:** ```bash cloudflared tunnel route dns summit-dental-app summitdentalcare.mydentalofficemanagement.com ``` **Step 7:** ```bash sudo cloudflared service install sudo systemctl enable cloudflared sudo systemctl start cloudflared ``` **Step 8:** Add `summitdentalcare.mydentalofficemanagement.com` to `allowedHosts` in `vite.config.js`. **Step 9:** Add `https://summitdentalcare.mydentalofficemanagement.com` to backend CORS allowed origins. --- ### Variant โ€” Tunnel scoped to Twilio webhooks only The example above tunnels the whole app (`service: http://localhost:3000`) to the public subdomain. If the only reason you need public access is Twilio's webhooks (inbound SMS/voice โ€” see [Twilio In-Browser Calling Setup](#twilio-in-browser-calling-setup-dial-pad) below), point the tunnel at nginx's port-80 block instead, which only forwards `/api/twilio/*` and returns `403` for everything else (see [nginx.conf](nginx.conf)) โ€” the rest of the app stays unreachable from the internet even though the tunnel is live. Example actually used for Summit Dental Care's Twilio integration โ€” paste this into `/etc/cloudflared/config.yml` (`sudo nano /etc/cloudflared/config.yml`, this is a YAML file, not the certbot `.ini` credentials file from the LAN HTTPS section above): ```yaml tunnel: fc423bdb-eaae-4af5-bd6d-961a60b1e624 credentials-file: /home/gg/.cloudflared/fc423bdb-eaae-4af5-bd6d-961a60b1e624.json ingress: - hostname: summit.mydentalofficemanagement.com service: http://localhost:80 - service: http_status:404 ``` Then route DNS and install the service same as Steps 6โ€“7 above: ```bash cloudflared tunnel route dns summit-dental-twilio summit.mydentalofficemanagement.com sudo cloudflared service install sudo systemctl enable cloudflared sudo systemctl start cloudflared ``` Skip Steps 8โ€“9 (Vite `allowedHosts` / backend CORS) for this variant โ€” the public hostname never reaches the frontend or triggers a login/API CORS check, only the unauthenticated Twilio webhook routes. --- ### Multi-office overview Each office runs its own `cloudflared` tunnel on its own PC. Ports never conflict because each PC is a separate machine. | Office | Local access | Public access | Tunnel name | |---|---|---|---| | Community Dentists of Lowell | `http://192.168.1.236:3000` | `https://communitydentistsoflowell.mydentalofficemanagement.com` | `dental-app` | | Summit Dental Care | `http://:3000` | `https://summitdentalcare.mydentalofficemanagement.com` | `summit-dental-app` | | Next office | `http://:3000` | `https://.mydentalofficemanagement.com` | `-app` | --- ## LAN HTTPS Setup (Trusted Certificate for Local Access) Some browser features โ€” notably the Screen Capture API used by the Copy Agent's screenshot tool โ€” only work in a "secure context" (`https://`, or the literal hostname `localhost`). Plain `http://:3000` doesn't qualify, so staff would see the feature report itself as unsupported. The fix used here: a real, publicly-trusted certificate (Let's Encrypt) for a subdomain whose DNS record points at this office's private LAN IP. Since the cert is issued via a DNS-01 challenge (not a live connection to the server), staff PCs need zero configuration โ€” no local CA to install, unlike a self-signed/mkcert certificate. **How it works:** - DNS: `local-.mydentalofficemanagement.com` โ†’ `A` record โ†’ this PC's LAN IP, **unproxied** ("DNS only" in Cloudflare โ€” proxying doesn't work for private IPs) - nginx terminates TLS using the Let's Encrypt cert and only accepts connections from the office subnet (`allow ; deny all;`) - Staff browse to `https://local-.mydentalofficemanagement.com` instead of the raw IP This is separate from the Cloudflare Tunnel above โ€” the tunnel is for public access (e.g. Twilio webhooks), this is for trusted HTTPS on the local network only. --- ### Step 1 โ€” Create the DNS record (in Cloudflare, not the registrar) In the Cloudflare dashboard for `mydentalofficemanagement.com` โ†’ DNS โ†’ Add record: - Type: `A`, Name: `local-` (e.g. `local-summit`), Content: this PC's LAN IP - **Proxy status: DNS only** (grey cloud) โ€” required, Cloudflare can't proxy a private IP ### Step 2 โ€” Install nginx (if not already done in Step 14 above) ```sh sudo apt update sudo apt install -y nginx ``` > If another web server (e.g. Apache) is already bound to port 80/443, nginx will fail to start. > Check with `sudo ss -tlnp | grep -E ':80|:443'` and disable the conflicting service first: > ```sh > sudo systemctl disable --now apache2 > ``` ### Step 3 โ€” Install certbot with the Cloudflare DNS plugin ```sh sudo apt update sudo apt install -y certbot python3-certbot-dns-cloudflare ``` ### Step 4 โ€” Create a scoped Cloudflare API token Cloudflare dashboard โ†’ profile icon โ†’ **My Profile** โ†’ **API Tokens** โ†’ **Create Token** โ†’ use the **"Edit zone DNS"** template, restricted to **Specific zone โ†’ mydentalofficemanagement.com**. Save it in a credentials file, root-only: ```sh sudo mkdir -p /etc/letsencrypt sudo nano /etc/letsencrypt/cloudflare.ini ``` ```ini dns_cloudflare_api_token = ``` ```sh sudo chmod 600 /etc/letsencrypt/cloudflare.ini ``` > The same token can be reused across offices (Cloudflare tokens are scoped to the whole zone, > not a single subdomain) โ€” but issuing one token per server makes it easy to revoke just one if > a machine is ever decommissioned. ### Step 5 โ€” Issue the certificate ```sh sudo certbot certonly --dns-cloudflare \ --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \ -d local-.mydentalofficemanagement.com ``` Certbot adds a temporary DNS TXT record via the API to prove domain ownership, then removes it. The cert lands at `/etc/letsencrypt/live/local-.mydentalofficemanagement.com/` and auto-renews via a systemd timer โ€” no manual renewal steps. ### Step 6 โ€” Point nginx at the new cert Update the LAN server block in `nginx.conf` (`server_name`, `ssl_certificate`, `ssl_certificate_key`, and the `allow ;` line) for this office, then reinstall and reload: ```sh sudo cp nginx.conf /etc/nginx/sites-available/dental-app sudo nginx -t && sudo systemctl reload nginx ``` ### Step 7 โ€” Allow the new hostname in Vite and backend CORS - `apps/Frontend/vite.config.ts` โ†’ add the hostname to `server.allowedHosts` - `apps/Backend/.env` โ†’ add `https://local-.mydentalofficemanagement.com` to `FRONTEND_URLS` Staff can now use `https://local-.mydentalofficemanagement.com` with a trusted padlock โ€” no certificate warnings, no CA install on any PC. ### Troubleshooting โ€” router blocks the hostname (DNS rebinding protection) Some routers (this has been confirmed on Verizon Fios gateways, e.g. the G3100) refuse to resolve `local-.mydentalofficemanagement.com` even though the DNS record is correct and public resolvers (`1.1.1.1`, `8.8.8.8`) answer it fine. Symptoms: `ping`/`dig` against the hostname fails or returns no answer when using the router as the DNS server, while `dig @1.1.1.1 ` returns the right LAN IP. Cause: the router's **DNS rebinding protection** blocks any public hostname that resolves to a private IP (`192.168.x.x`) โ€” a heuristic meant to stop DNS rebinding attacks, which happens to match this setup's pattern (public domain โ†’ private LAN IP) exactly. Preferred fix: on the router's admin page (e.g. `https://192.168.1.1` for Fios), look under **Advanced โ†’ Network Settings โ†’ DNS Server** for a **DNS Rebind Protection** exception list, and add `local-.mydentalofficemanagement.com`. If no exception list exists, disabling DNS Rebind Protection entirely also works, at the cost of that protection network-wide. If you don't have router access, override DNS resolution per machine with a hosts-file entry instead โ€” this works because every OS checks its local hosts file before asking the router's DNS, so the query never reaches the router: **Debian/Linux server itself:** ```sh sudo nano /etc/hosts ``` Add a line at the bottom (replace with this office's actual LAN IP and hostname): ``` 192.168.1.229 local-.mydentalofficemanagement.com ``` Save and exit โ€” no service restart needed, `/etc/hosts` is checked before DNS automatically. **Windows staff PCs (PowerShell):** ```powershell # 1. Open PowerShell as Administrator, then confirm it's actually elevated ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) # must print True โ€” if False, close this window and reopen via right-click "Run as administrator" # 2. Append the entry Add-Content -Path C:\Windows\System32\drivers\etc\hosts -Value "192.168.1.229 local-.mydentalofficemanagement.com" # 3. Verify it saved Get-Content C:\Windows\System32\drivers\etc\hosts -Tail 3 # 4. Flush the DNS cache and test ipconfig /flushdns ping local-.mydentalofficemanagement.com ``` > **If `Add-Content` fails with "the process cannot access the file... being used by another > process,"** antivirus software (Malwarebytes, Avast/AVG "Hosts File Guard", McAfee, and some VPN > clients are known to do this) is locking the hosts file to prevent tampering. Temporarily > disable real-time protection, redo step 2, verify with step 3, then **re-enable antivirus** โ€” > the hosts file entry itself doesn't need protection disabled to keep working, only to make the > edit. Editing Notepad directly instead of PowerShell is not recommended: saving `C:\Windows\System32\drivers\etc\hosts` without true elevation fails silently in some cases (the file appears saved but `LastWriteTime` never changes) โ€” PowerShell's `Add-Content` surfaces a clear permission error instead, which is easier to diagnose. The real Let's Encrypt certificate from Step 5 keeps working normally regardless of how each client resolves the hostname โ€” it was validated via DNS-01 against Cloudflare at issuance time, not by a live connection to the server, so staff still get a trusted padlock with no warnings. This is a per-machine workaround; the router-level exception above is preferred when possible since it fixes every device on the network in one place instead of requiring a hosts-file edit (and possibly an antivirus fight) on every PC. --- ## Payment OCR Service Setup (Google Cloud Vision) The Payment OCR Service (`apps/PaymentOCRService`, port 5003) uses Google Cloud Vision to read payment/EOB documents and to locate exact text positions for the Windows Type Agent. It needs a Google Cloud service-account key that is **not** included in the repo (it's a live secret and is gitignored on purpose) โ€” each PC needs its own copy placed locally. ### Step 1 โ€” Enable the Cloud Vision API 1. Go to `console.cloud.google.com` and select (or create) the project this service should use 2. **APIs & Services โ†’ Library** โ†’ search for **"Cloud Vision API"** โ†’ click **Enable** (skip if already enabled) ### Step 2 โ€” Create a service account key 1. **IAM & Admin โ†’ Service Accounts** โ†’ either pick an existing service account for this app, or **Create Service Account** (any name, e.g. `ocr-service`; no special roles needed beyond default โ€” Vision API access comes from the API being enabled on the project, not a role grant) 2. Open that service account โ†’ **Keys** tab โ†’ **Add Key โ†’ Create new key โ†’ JSON** This immediately downloads a `.json` file to your browser's Downloads folder โ€” **this is the only time the private key content is shown**, so keep the file safe (a password manager or secure backup, not just Downloads). ### Step 3 โ€” Install the key on this PC Move the downloaded file into `apps/PaymentOCRService/` and rename it to exactly `google_credentials.json` โ€” this is the filename `apps/PaymentOCRService/.env` already expects: ```sh mv ~/Downloads/.json apps/PaymentOCRService/google_credentials.json ``` > `google_credentials.json` is gitignored on purpose โ€” **never commit it**. If a key is ever > accidentally exposed (committed, pasted, screenshotted), go back to the Keys tab in Step 2 and > delete it, then generate a new one. ### Step 4 โ€” Verify With the service running (Step 15 above starts it as part of `npm run dev`, or run it directly โ€” see `apps/PaymentOCRService/README.md`): ```sh curl localhost:5003/health # should report "GOOGLE_APPLICATION_CREDENTIALS set: True" ``` --- ## Twilio In-Browser Calling Setup (Dial Pad) The dial pad on the Patient Connection page lets staff make real phone calls directly through the browser (mic + speaker) using Twilio Voice SDK. One-time setup is required in the Twilio Console. ### One-time Twilio Console setup (required before first call) 1. Go to **Twilio Console โ†’ Explore Products โ†’ Voice โ†’ TwiML Apps** 2. Click **Create new TwiML App** 3. Set the **Voice Request URL** to: ``` https://communitydentistsoflowell.mydentalofficemanagement.com/api/twilio/webhook/voice-browser ``` 4. Save โ€” copy the **TwiML App SID** (starts with `AP`) 5. In the dental app, go to **Settings โ†’ Twilio Settings โ†’ TwiML App SID** โ†’ paste the SID and save Once saved, the dial pad on the Patient Connection page is fully functional. The staff member's browser mic/speaker is used for the call; the patient receives a normal phone call from the office Twilio number. --- ## License Key Generator The license key generator is a private tool that lives only on your dev PC. Use it to generate a new license key for any office every 3 months. **Location:** `/home/ff/Desktop/LicenseGenerator/` **Generate a 3-month key (default):** ```bash node /home/ff/Desktop/LicenseGenerator/generate-license.js ``` **Generate a key with a custom duration:** ```bash node /home/ff/Desktop/LicenseGenerator/generate-license.js --months=6 ``` **Example output:** ``` === Dental App License Key === License Key: DENTAL-8ED7AAEF3E0CA008D98CC1E0-2026-08-26 Expires: 2026-08-26 Duration: 3 month(s) Paste this key into the Activation page in the app. ``` **Workflow:** 1. Office pays renewal fee 2. Run the generator script above 3. Copy the License Key 4. Paste it into the **Activation** page in the app (via RustDesk or in person) 5. Record the key, office name, expiry, and payment in your records **Important โ€” `secret.key`:** - `/home/ff/Desktop/LicenseGenerator/secret.key` is the private secret used to sign all keys - Back it up on a USB drive or password manager - If lost, all existing keys become invalid and new keys must be issued to all offices --- ## Network Backup Setup (PC-to-PC Sync) Two PCs running the app can be linked so the backup PC automatically pulls a fresh copy of the main PC's database every night. The config survives database restores because it is stored in local files, not in the database. **Prerequisites:** Both PCs must be on the same local network (e.g. connected to the same router or switch). Set a static IP on the main PC so its address never changes after a reboot (set in the OS network settings, not in the router). ### On PC1 (main server) 1. Open the app โ†’ **Database Management** โ†’ **Network Backup** 2. Under **This Machine's Backup Key**, click the eye icon to reveal the key 3. Click the copy button to copy it ### On PC2 (backup PC) 1. Open the app โ†’ **Database Management** โ†’ **Network Backup** 2. Under **Sync from Another PC**: - Toggle **Enable daily sync** on - Select the hour you want the sync to run (e.g. `12:00 AM (midnight)`) - Enter PC1's URL in the **Source PC URL** field, e.g. `http://192.168.0.94:3000` - Paste PC1's key into the **Source PC API Key** field 3. Click **Save Settings** 4. Click **Sync Now** to test โ€” PC2's database will be replaced with PC1's After a successful test, the sync will run automatically at the scheduled hour every day. **Notes:** - The API key and sync config are stored in `apps/Backend/network-backup-key.json` and `apps/Backend/network-sync-config.json` โ€” they survive database restores - If you regenerate PC1's key, you must update it on PC2 as well - The sync is one-way: PC2 always mirrors PC1; PC1 is never modified --- ## Compile & Deploy to New PC ### The Plan On your dev PC, run `npm run build` โ€” this compiles the Frontend (React โ†’ `dist/`) and Backend (TypeScript โ†’ JS). The result is placed in a new deploy folder that contains no source code, then copied via USB to the office PC. ``` /home/ff/Desktop/DentalManagement-Deploy/ โ† copy this to the new PC (no source code inside) ``` ### New Office PC โ€” One-time Setup (manual) | Step | How | |---|---| | Install Linux, Chrome, PostgreSQL, Python, Node | Manually | | Install RustDesk | Manually | | Paste the deploy folder from USB | You | | Run `setup.sh` | You | | Enter license key in Activation page | You | ### Updates (after bug fixes) Build on dev PC โ†’ copy only the updated `dist/` folders via USB to the office PC (not the whole folder): ``` apps/Backend/dist/ โ†’ /home/ff/Desktop/DentalManagement-Deploy/apps/Backend/dist/ apps/Frontend/dist/ โ†’ /home/ff/Desktop/DentalManagement-Deploy/apps/Frontend/dist/ ``` ### License System - All PCs (including your dev PC) need a license key every 3 months - Keys have no machine ID โ€” just an expiry date + your HMAC signature - Generate a key: `node /home/ff/Desktop/LicenseGenerator/generate-license.js` - Key format: `DENTAL-{24-char-signature}-YYYY-MM-DD` - `secret.key` must be backed up โ€” losing it invalidates all existing keys ### Free vs Premium | Tier | Features | |---|---| | **Free** | MassHealth Eligibility, MassHealth Claim, Documents, Payments, Database Backups, Reports | | **Premium (license required)** | CCA, DDMA, United, Tufts Eligibility & Claims, Pre-Auths, AI SMS | --- ## Claude Code Memory Claude Code (the AI assistant used to build this project) stores its memory locally on the PC. This memory contains project context, architecture decisions, feature history, and working preferences โ€” allowing Claude to pick up where it left off in new sessions. **Memory location:** ``` /home/ff/.claude/projects/-home-ff-Desktop-DentalManagementMH06/memory/ ``` **To copy to a new PC:** 1. On the old PC, copy the memory folder: ```bash cp -r /home/ff/.claude/projects/-home-ff-Desktop-DentalManagementMH06/memory/ /media/usb/claude-memory-backup/ ``` 2. On the new PC, recreate the directory and paste: ```bash mkdir -p /home/ff/.claude/projects/-home-ff-Desktop-DentalManagementMH06/memory/ cp -r /media/usb/claude-memory-backup/* /home/ff/.claude/projects/-home-ff-Desktop-DentalManagementMH06/memory/ ``` The memory is plain markdown files and can also be copied manually via a USB drive or file manager. Enable "show hidden files" (Ctrl+H) in the file manager to see the `.claude` folder.