Files
DentalManagementMH07/README.md
Gitead 8c65c3d625 docs: clarify RustDesk autostart opens a window, not just the service tray
The core service already auto-spawns its own tray icon per session, so
Step 18's autostart entry is specifically for opening the main window.
Also note that a permanent password avoids the OTP-refresh confusion
where the window briefly shows one password then updates to another.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 23:58:50 -04:00

34 KiB
Raw Blame History

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

passwd

It will ask for the current password, then the new password twice.

Step 2 — Set the root password

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

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:

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):

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:

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:

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 LoginChange Password → set both new-password fields blank.

Step 2 — Install Git

sudo apt update
sudo apt install -y git

# Verify
git --version

Step 3 — Clone the repository

git clone <your-repo-url>
cd DentalManagementMHAprilgg

Step 4 — Install Node.js

Required to run the Backend and Frontend.

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.

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.

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.

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:

sudo sed -i 's/scram-sha-256/md5/g' /etc/postgresql/*/main/pg_hba.conf
sudo systemctl restart postgresql

Verify

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.

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:

rclone version

Step 10 — Install Node.js dependencies

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.

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

VITE_CLOUDFLARE_HOST=yoursubdomain.mydentalofficemanagement.com

apps/Backend/.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

# 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:

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:

sudo systemctl disable --now apache2

The repo includes nginx.conf in the project root. Install it as the active site config:

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:

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:

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:

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.

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 for the latest version number if the link above has moved on.

Get this machine's RustDesk ID:

rustdesk --get-id

Set a permanent password for unattended access (skip this to require manual approval on each connection instead):

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 — 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:

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


🌐 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.)

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:
  • Current setup: Frontend runs on 0.0.0.0 and is accessible via the device IP.

.env file (apps/Frontend/.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):

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

🌐 Selenium Service

💳 Payment OCR Service

📖 Notes

  • These values come from per-app .env files (apps/<App>/.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.

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.jsons

Each package/app is 100% TypeScript (except the Python services).

Utilities


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://<machine-ip>:3000
  • Internet: anyone reaches the app via https://<subdomain>.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.comAdd 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 1030 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:

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

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:

cloudflared tunnel create <office-tunnel-name>
# 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

sudo mkdir -p /etc/cloudflared
sudo nano /etc/cloudflared/config.yml

Paste (replace tunnel ID, credentials path, and subdomain for this office):

tunnel: <tunnel-ID>
credentials-file: /home/ee/.cloudflared/<tunnel-ID>.json

ingress:
  - hostname: <subdomain>.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

cloudflared tunnel route dns <office-tunnel-name> <subdomain>.mydentalofficemanagement.com

Step 7 — Install as a system service (auto-starts on boot)

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:

cloudflared tunnel create summit-dental-app
# Example output: Created tunnel summit-dental-app with id a1b2c3d4-...

Step 5 — /etc/cloudflared/config.yml:

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:

cloudflared tunnel route dns summit-dental-app summitdentalcare.mydentalofficemanagement.com

Step 7:

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 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) — 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):

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 67 above:

cloudflared tunnel route dns summit-dental-twilio summit.mydentalofficemanagement.com
sudo cloudflared service install
sudo systemctl enable cloudflared
sudo systemctl start cloudflared

Skip Steps 89 (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://<its-ip>:3000 https://summitdentalcare.mydentalofficemanagement.com summit-dental-app
Next office http://<its-ip>:3000 https://<subdomain>.mydentalofficemanagement.com <office>-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://<LAN-ip>: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-<office>.mydentalofficemanagement.comA 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 <subnet>; deny all;)
  • Staff browse to https://local-<office>.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-<office> (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)

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:

sudo systemctl disable --now apache2

Step 3 — Install certbot with the Cloudflare DNS plugin

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 ProfileAPI TokensCreate Token → use the "Edit zone DNS" template, restricted to Specific zone → mydentalofficemanagement.com.

Save it in a credentials file, root-only:

sudo mkdir -p /etc/letsencrypt
sudo nano /etc/letsencrypt/cloudflare.ini
dns_cloudflare_api_token = <paste token here>
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

sudo certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d local-<office>.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-<office>.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 <subnet>; line) for this office, then reinstall and reload:

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-<office>.mydentalofficemanagement.com to FRONTEND_URLS

Staff can now use https://local-<office>.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-<office>.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 <hostname> 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-<office>.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:

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-<office>.mydentalofficemanagement.com

Save and exit — no service restart needed, /etc/hosts is checked before DNS automatically.

Windows staff PCs (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-<office>.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-<office>.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.


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):

node /home/ff/Desktop/LicenseGenerator/generate-license.js

Generate a key with a custom duration:

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 ManagementNetwork 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 ManagementNetwork 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:
cp -r /home/ff/.claude/projects/-home-ff-Desktop-DentalManagementMH06/memory/ /media/usb/claude-memory-backup/
  1. On the new PC, recreate the directory and paste:
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.