We hit this exact conflict during setup (Apache pre-installed and bound to port 80), so nginx failed to start until it was disabled.
785 lines
25 KiB
Markdown
785 lines
25 KiB
Markdown
# 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 — Clone the repository
|
||
|
||
```sh
|
||
git clone <your-repo-url>
|
||
cd DentalManagementMHAprilgg
|
||
```
|
||
|
||
### Step 2 — 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 3 — 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 4 — Install Chrome
|
||
|
||
Required for the Selenium service to control a browser.
|
||
|
||
```sh
|
||
wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
|
||
echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee /etc/apt/sources.list.d/google-chrome.list
|
||
sudo apt-get update
|
||
sudo apt-get install -y google-chrome-stable
|
||
|
||
# Verify
|
||
google-chrome --version
|
||
```
|
||
|
||
> The `webdriver-manager` package (included in `requirements.txt`) automatically downloads the matching ChromeDriver — no manual driver setup needed.
|
||
|
||
### Step 5 — 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 6 — 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 7 — 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 8 — Install Node.js dependencies
|
||
|
||
```sh
|
||
npm install
|
||
```
|
||
|
||
### Step 9 — Install Python dependencies
|
||
|
||
Python dependencies are installed automatically by `npm install` (Step 8) 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 10 — Set up environment variables
|
||
|
||
Copy the `.env.example` files and fill in the required values.
|
||
|
||
```sh
|
||
npm run setup:env
|
||
```
|
||
|
||
### Step 10a — Set the Cloudflare subdomain for this office
|
||
|
||
After running `npm run setup:env` (Step 10), 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 11 — 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 12 — 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 13 — 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 14 — 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.
|
||
|
||
---
|
||
|
||
## 📖 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://<your-ip>: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/<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](#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://<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.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 <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
|
||
|
||
```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: <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
|
||
|
||
```bash
|
||
cloudflared tunnel route dns <office-tunnel-name> <subdomain>.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.
|
||
|
||
---
|
||
|
||
### 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.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 <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 12 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 = <paste token here>
|
||
```
|
||
|
||
```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-<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:
|
||
|
||
```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-<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.
|
||
|
||
---
|
||
|
||
## 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.
|