# cPanel deployment

This app is an Express server with a file-based SQLite database and on-disk photo uploads.
It needs a real Node process and a persistent filesystem, so it must run through
**cPanel -> Setup Node.js App** (Passenger / LiteSpeed), not through PHP or a static host.

## 0. Hard requirement: Node version

`server/src/db.js` imports `node:sqlite`.

| Node on host | Result |
| --- | --- |
| 24.x | works (same major as the dev machine) |
| 22.5 - 22.x | needs `--experimental-sqlite`, which cPanel does not let you pass |
| 20.x or older | will not start (`Cannot find module 'node:sqlite'`) |

Check the **Node.js Version** dropdown in Setup Node.js App before anything else. If 24 is not
listed, ask the host to enable `alt-nodejs24`, or port the data layer to `better-sqlite3`.

## 1. Folder layout on the host

`ROOT` is resolved two levels above `server/src` (`server/src/db.js:7`), so `data/` and
`backups/` must sit next to `server/`:

```
/home/USER/dermaclin
  server/          src/, package.json   (node_modules created by npm install on the host)
  client/          src/, public/fonts/, package.json, vite.config.js  -> built on the host
  data/            clinic.db + -wal + -shm + uploads/ + secret.key
  backups/         created on first start
  deploy/          setup.sh, test-on-host.sh, full-test.mjs, host-check.mjs, backup.cron.mjs
```

The UI build and the Vazirmatn font files are part of the app: `client/public/fonts.css` and
`client/public/fonts/*.woff2` are copied into `client/dist` by the build, and `client/index.html`
loads `/fonts.css`. Nothing is fetched from a CDN, so a domain with no outbound access still renders
identically to the dev machine.

Keep the app **outside** `public_html` and leave `public_html` empty. The Node app is mapped to
the domain by cPanel itself; no `.htaccess` rewrite is needed because Express serves the SPA and
handles client-side routes (`server/src/index.js:34`).

## 2. What to upload

Upload these (the ZIP already has everything except `node_modules`):

- `server/src/`, `server/package.json` and `server/package-lock.json`
- `client/src/`, `client/index.html`, `client/package.json`, `client/package-lock.json`,
  `client/vite.config.js`, `client/public/` (including `fonts.css` and `fonts/*.woff2`) - the UI is
  built on the host
- `data/` - the live clinic data: `clinic.db` (packaged checkpointed, WAL already folded in, so no
  `-wal` / `-shm` side files ship), `uploads/`, `secret.key`
- `deploy/` and `package.json`

Do **not** upload:

- `client/node_modules` (~84 MB) and `server/node_modules` - Windows binaries and absolute paths
- `client/dist` - the host build produces it; a stale local copy hides host-side build errors
- `data/*.log`

The ZIP contains patient photos and the session-signing key. Extract it in `/home/USER/dermaclin`
only, never in `public_html`, and delete the ZIP from the server when the deploy is done.

## 3. Build on the host and create the app

1. Select the Node version first: cPanel -> **Setup Node.js App** -> **Node.js version: 24.x**
   (the app needs `node:sqlite`; see the table in section 0). Save, then open cPanel -> **Terminal** -
   the terminal inherits the selected version.
2. In the terminal:
   ```
   cd /home/USER/dermaclin
   bash deploy/setup.sh
   ```
   It checks the Node major, installs `server` dependencies, installs `client` devDependencies,
   runs the Vite build, creates `data/uploads` + `backups` and chmods `data/secret.key`.
   Expected tail: `dist/index.html`, `dist/assets/index-*.js`, `dist/assets/index-*.css`.
3. cPanel -> **Setup Node.js App** -> Create Application:
   - Node.js version: **24.x**
   - Application root: `dermaclin`
   - Application startup file: `server/src/index.js`
   - Application URL: your domain or subdomain
4. Same screen, **Environment variables**: add `TZ=Asia/Tehran`.
   `today()` (`server/src/routes/_util.js:16`) uses the local date; on a UTC host, records created
   between 00:00 and 03:30 Tehran time get stamped with the previous day.
   Do not set `PORT` by hand - cPanel provides it and the app reads it (`server/src/index.js:48`).
5. **Restart App**, then open `https://yourdomain/api/health` -> `{"ok":true}`.

## 4. HTTPS and first login

- cPanel -> **SSL/TLS Status** -> Run AutoSSL (or install a Let's Encrypt certificate), then
  enable **Force HTTPS** on the domain.
- Log in with the admin account and immediately change the seeded password
  (`admin` / `admin123`, created in `server/src/db.js:197`).
- Tighten the key: `chmod 600 data/secret.key`

## 5. Test on the host - two passes

**Pass 1 - full feature test on a throwaway database.** From the cPanel terminal:

```
cd /home/USER/dermaclin
bash deploy/test-on-host.sh
```

It starts a second instance with `DATA_DIR=$PWD/data-selftest` and `PORT=3412` (fresh seed, its own
`secret.key`, `uploads/` and `backups/` - the live `data/` folder is never opened), waits for
`/api/health`, runs `deploy/full-test.mjs`, prints `full-test: 153 passed, 0 failed`, stops the
instance and deletes `data-selftest`. Use another port if 3412 is busy: `PORT=3500 bash deploy/test-on-host.sh`.

Covered by those 153 checks: SPA + local font serving and deep links, the 30 MB body limit, the user
lifecycle (create, password change, deactivate), the role matrix for admin / doctor / reception /
portal tokens, patients and the 11-section chart, before/after pairs plus composite and simulated
renders, portal privacy filters, appointments with double-booking rules, services / visits /
procedures / prescriptions / packages, invoices with line, head and insurance discount maths,
POS / check / gateway payments, the check ledger lifecycle, deposits, debts, claims, all eleven
reports, the SMS queue, the audit trail, backup -> download -> restore, and the POS sale with its
patient guard.

**Pass 2 - read-only check of the real hosted instance:**

```
cd /home/USER/dermaclin
node deploy/host-check.mjs --url https://yourdomain/api --user admin --pass 'YOUR_NEW_PASSWORD'
```

Read-only: checks the runtime, the migration columns, file permissions, the main endpoints and the
POS guard. It writes nothing.

Then in the browser: patient chart -> before/after tab (upload a real photo pair, run the smart
prediction), a POS sale with and without a patient, and the patient portal.

`smoke.mjs` and `smoke-accounting.mjs` are older partial suites and are **not** safe against live
data - they create a test patient, invoices, sales and purchases and never clean up. `deploy/full-test.mjs`
replaces both, and `BASE_URL=... node deploy/full-test.mjs` aims it at any instance, so point it only
at a throwaway one.

## 6. Upload size

The API accepts 30 MB JSON bodies (`server/src/index.js:17`), but LiteSpeed/Apache on cPanel often
caps request bodies at 8-32 MB. If a photo upload returns `413`, either ask the host to raise the
limit or shrink the image in the browser before the POST. Over-limit requests now answer
`{"error":"payload_too_large"}` with HTTP 413 instead of a 500 page, so a 413 with that body came from
the app and a bare 413 HTML page came from the web server in front of it. Bad enum values
(`gender`, appointment status, ...) answer `400 invalid_value` rather than 500
(`server/src/index.js:38`).

## 7. Backups and keep-alive

cPanel cron does not ship the `sqlite3` CLI, so the backup runs through Node:

```
0 3 * * * cd /home/USER/dermaclin && /usr/bin/env node deploy/backup.cron.mjs >> data/backup.log 2>&1
*/10 * * * * curl -s -o /dev/null https://yourdomain/api/health
```

The first line checkpoints the WAL and copies the database into `backups/` (snapshots older than 14
days are pruned) and mirrors new files from `data/uploads`. The second line keeps the Passenger app
warm so the first staff request is not a cold start. Copy `backups/` off the server periodically -
a backup next to the data protects nothing from a lost disk.

Restore: **Settings -> Backups** in the admin panel, or `POST /api/admin/restore` with
`{"file":"clinic-<stamp>.db"}` (a safety copy of the current database is written first).

## 8. Open security items before advertising the domain

- `/uploads` is served without authentication (`server/src/index.js:19`). Names are random UUIDs,
  but anyone who has a link sees the photo.
- `cors()` allows every origin (`server/src/index.js:16`) while the client only calls relative
  `/api` paths, so it can be closed.
- `/api/auth/login` and `/api/portal/lookup` (national code + last name or PIN) have no rate
  limiting; add `limit_req` in front of them or a throttle in the app.
- Session tokens live in `localStorage`, so any injected script can read them.
