# Pilot Setup — Imani Health Facility-Local Server

This guide walks through installing and running `imani-health-local` at a
single pilot facility (a laptop, mini-PC, or on-prem server at the
hospital/clinic). It is intentionally scoped to a pilot: one facility, one
Postgres instance, manual sync. It is not a multi-facility or
high-availability installer.

The facility's actual staff-facing UI is `imani-health-website` running in
`facility-local` mode (`NEXT_PUBLIC_APP_MODE=facility-local`) — this repo
only runs the local API + local database that website talks to.
`apps/local-web` in this repo is a test-only harness for developing this
repo itself; it is not what pilot staff use.

## 1. Prerequisites

- Node.js 20+
- Docker Desktop (or another Docker daemon) — used to run local Postgres
- Network access from this machine to the cloud sync URL (only needed when
  actually syncing — the facility can run fully offline otherwise)

## 2. Install dependencies

```bash
npm install
```

## 3. Register this facility in the Imani Health admin panel

Before this server can do anything beyond serve a login page, the facility
needs to be registered:

1. In the Imani Health admin panel, go to **Sync** → **Register facility**.
2. Enter:
   - **Facility server ID** — a short, unique id for this physical server
     (e.g. `acme-hospital-facility-01`). This becomes `LOCAL_FACILITY_SERVER_ID`.
   - **Organization ID** — the hospital/organization this facility belongs to.
     This becomes `LOCAL_ORGANIZATION_ID`.
   - **Name** (optional) — a human-readable label.
3. Submit. The admin panel will show a **secret exactly once** — copy it
   immediately. This becomes `IMANI_FACILITY_SECRET`. If you lose it, you'll
   need to rotate it from the same admin panel screen (rotating invalidates
   the old one).

Do not paste this secret into chat, tickets, screenshots, or any doc —
including this one. It only ever belongs in your local `.env` file.

## 4. Configure `.env`

```bash
cp .env.example .env
```

Edit `.env` and set:
- `LOCAL_FACILITY_SERVER_ID` — from step 3
- `LOCAL_ORGANIZATION_ID` — from step 3
- `LOCAL_ORGANIZATION_NAME` — the hospital's display name
- `IMANI_LOCATION_CODE` — a short 4-letter code for this facility (used in
  patient/staff IDs, e.g. `LAGH`)
- `IMANI_FACILITY_SECRET` — the secret from step 3
- `IMANI_CLOUD_SYNC_URL` — leave as the default unless told otherwise. **Must
  include the `/sync` path suffix** (e.g. `https://<host>/sync`) — sync-service
  mounts every facility route under `/sync`, so a bare host URL will 404 on
  every backfill/push/pull call.

Leave `LOCAL_DATABASE_URL`, `LOCAL_API_PORT` as-is unless you have a reason
to change them (e.g. port 4100 is already in use on this machine).

`LOCAL_STAFF_USERNAME` / `LOCAL_STAFF_PASSWORD` are filled in later, in step 7.

## 5. Start the database

```bash
npm run db:up
```

This starts a Postgres container (`imani-local-postgres`) via Docker Compose
and waits for it to report healthy. Data persists in a Docker volume across
restarts — `npm run db:down` stops the container without deleting data.

## 6. Run migrations

```bash
npm run migrate
```

Applies any pending schema migrations from `packages/local-db/migrations/`.
Safe to re-run — already-applied migrations are skipped.

## 7. Start the local API

```bash
npm run dev:api
```

This warns loudly if any config still looks like a dev default (see step 3/4)
before starting. Leave this running in its own terminal/tab.

Verify it's up:

```bash
npm run health
```

This checks `/health` and `/local/facility/info`, and tells you whether the
facility is actually registered (not just running).

### Give the facility server a stable address

Before staff connect the PWA, make sure the server has an address that will not change. A random router-assigned IP can change after Wi-Fi reconnects, router reboots, sleep/wake cycles, or DHCP lease expiry. If the server changes from `192.168.1.5` to `192.168.1.8`, staff devices will lose their saved connection.

**Best pilot setup:** use both a router DHCP reservation and a friendly local hostname. The hostname is easier for staff; the reserved IP is the fallback if `.local` names do not resolve on the facility network.

1. Find the server's current IP address.

   macOS Wi-Fi:

   ```bash
   ipconfig getifaddr en0
   ```

   Linux:

   ```bash
   hostname -I
   ```

   Windows PowerShell:

   ```powershell
   ipconfig
   ```

2. Find the server's MAC address for the network adapter it will use.

   macOS Wi-Fi:

   ```bash
   networksetup -getmacaddress Wi-Fi
   ```

   macOS Ethernet:

   ```bash
   networksetup -getmacaddress Ethernet
   ```

   Linux:

   ```bash
   ip link
   ```

   Windows PowerShell:

   ```powershell
   getmac /v
   ```

3. Log into the facility router and create a DHCP reservation. Common router pages are `192.168.1.1`, `192.168.0.1`, or `192.168.1.254`. Look for **DHCP**, **LAN**, **Address Reservation**, **Static Lease**, or **Reserved IP**.

   Suggested reservation:

   ```text
   Device name: Imani Facility Server
   MAC address: <server Wi-Fi/Ethernet MAC address>
   Reserved IP: 192.168.1.50
   ```

   After saving, reconnect/reboot the server and confirm it keeps that IP. The fallback PWA URL becomes:

   ```text
   https://192.168.1.50:4100
   ```

4. Optional but recommended: set a friendly `.local` hostname. On macOS:

   ```bash
   sudo scutil --set ComputerName "Imani Facility Server"
   sudo scutil --set LocalHostName imani-facility
   sudo scutil --set HostName imani-facility.local
   ```

   Test from a staff device on the same Wi-Fi/LAN:

   ```bash
   ping imani-facility.local
   curl -k -i https://imani-facility.local:4100/local/facility/info
   ```

   If it works, staff can use:

   ```text
   https://imani-facility.local:4100
   ```

5. Make sure the HTTPS certificate covers the final URL staff will enter. If the address changes, regenerate the certificate and restart the local API. Every staff device must then open the Facility Server URL directly once and accept the certificate warning before using the PWA's Facility Connection screen.

### Creating local staff logins

Sync operations (`sync:run`, `backfill`, `sync:status`) and website logins in
facility-local mode require a local staff account.

**Preferred: pull real staff logins from the cloud.** Staff who can already
log in online (email/Username ID + password) can use the *exact same
credentials* offline — no separate offline password to remember or manage.
Run:

```bash
npm run staff-backfill
```

This is facility-secret protected (not session protected — there's no local
session to require yet on a fresh facility), pulls every staff member at
this organization along with their real password hash, and imports them
into the local database. It's safe to re-run any time:
- a password changed online → next run picks up the new hash
- a staff member is suspended or removed from the org online → next run
  disables their local login too

See [Security model](#security-model-cloud-credential-import) below for how
this works without ever handling a plaintext password.

Set `LOCAL_STAFF_USERNAME`/`LOCAL_STAFF_PASSWORD` in `.env` to one of the
imported staff's real online email/Username ID + password — the CLI sync
scripts use them to obtain a session token.

**Fallback: manually seed a local-only account.** If a staff member doesn't
have (or shouldn't have) an online account, or for quick local
testing/seeding, create a facility-local-only login directly (never expose
this endpoint to anyone but yourself):

```bash
curl -X POST http://localhost:4100/local/staff \
  -H "Content-Type: application/json" \
  -H "X-Facility-Secret: $IMANI_FACILITY_SECRET" \
  -d '{
    "email": "admin@example.com",
    "name": "Facility Admin",
    "role": "admin",
    "password": "choose-a-real-password"
  }'
```

This account's password only ever exists locally — it has no cloud
counterpart, and won't be touched or disabled by `staff-backfill` runs
unless a staff-backfill row happens to reuse the same email/username_id
(avoid that: use distinct usernames for manually-seeded test accounts).

## 8. Run the initial backfill

Pulls existing cloud patient records for this organization down into the
local database, so the facility isn't starting from zero:

```bash
npm run backfill
```

## 9. Configure `imani-health-website` for facility-local mode

In the `imani-health-website` repo, set (see that repo's own docs for the
full list):
- `NEXT_PUBLIC_APP_MODE=facility-local`
- The local API base URL (pointing at `http://localhost:4100`, or this
  machine's LAN address if staff are on other devices)

Start it and log in with the local staff credentials created in step 7.

## 10. Smoke test

- Log in on the website in facility-local mode.
- Confirm patients from the backfill are visible.
- Register a new local-only patient, confirm it appears immediately.
- Run `npm run sync:status` and confirm the new patient shows up as an
  outgoing/pending change.
- Run `npm run sync:run` and confirm it pushes successfully (check for cloud
  connectivity first — this will fail gracefully if offline, which is fine).

## 11. Ongoing operation

- `npm run doctor` — run this any time something seems wrong. It checks env,
  database, migrations, local API, facility registration, and cloud
  reachability, without changing anything.
- `npm run sync:run` — manual sync is the default (no background job runs
  automatically). Trigger this periodically, or use the "Sync now" button on
  the website's Sync page, or enable auto-sync from that same page.
- `npm run staff-backfill` / `npm run staff-auth-refresh` — the local API
  already refreshes staff auth automatically every 15 minutes on its own
  (independent of the manual/auto-sync toggle above — staff access
  revocation is security-sensitive enough to always run). Use
  `staff-auth-refresh` (session-authenticated) to force an immediate refresh
  without waiting for the timer — e.g. right after suspending someone
  online. `staff-backfill` (facility-secret-authenticated) is specifically
  for bootstrapping the very first local login on a freshly-registered
  facility, before any session exists yet.
- `npm run stop` — stops the local API process. Postgres keeps running; use
  `npm run db:down` separately if you also want to stop the database.

## Security model: cloud credential import

Offline login accepts the exact same email/Username ID + password staff
already use online. This works without this server (or anyone who can read
its database) ever seeing a plaintext password:

- Auth-service hashes online passwords with **argon2id** — a modern,
  self-contained hash format (algorithm, cost parameters, salt, and hash all
  bundled into one string). No separate secret/pepper is needed to verify
  it.
- `npm run staff-backfill` imports that hash string directly — never a
  plaintext password — via a chain of internal, service-to-service-only
  endpoints (never publicly reachable): sync-service's `/sync/backfill`
  variant → organization-service (which knows this org's staff and their
  membership status) → auth-service (which holds the hash). Every hop is
  authenticated with a shared service secret, the same pattern already used
  for patient backfill.
- This server verifies the imported hash with the same argon2id algorithm.
  Existing facility-local-only accounts (created via the manual `/local/staff`
  fallback above) keep using the original local scrypt-based verification —
  both formats are supported side by side, tracked per-account by a
  `hash_scheme` column (`local_scrypt` or `cloud_argon2`).
- Password changes, suspensions, and removals online are reflected locally
  automatically within 15 minutes (the server's own background refresh
  timer), or immediately via `npm run staff-auth-refresh`. A staff member
  suspended or removed online loses offline access — both new logins and any
  already-open session — within that window, not just at their next login.
- Offline password changes are not supported in v1 — a locally-imported
  cloud login's password can only change by changing it online, then either
  waiting for the next automatic refresh or running `staff-auth-refresh`.
- **Only an admin-role account may log in with email offline — every other
  staff role must use their Username ID.** This mirrors the online
  convention (regular staff/providers always log in via Username ID; only
  the admin-equivalent account uses email) — offline, a single login form
  accepts either identifier, so the role is checked directly instead of
  relying on separate login pages. This applies to both hash schemes: a
  manually-seeded local-only doctor/nurse/receptionist/etc. account can no
  longer log in with its email either, only its Username ID.
- `POST /local/sync/staff-backfill` accepts either an already-logged-in
  staff session (what the "Sync staff logins" button on the website's
  Facility Sync page sends) or the facility secret directly (what
  `npm run staff-backfill` sends — this is what bootstraps the very first
  local login on a fresh facility, before any session exists). The facility
  secret must never be given to the browser/website.

## 12. Backup and restore

```bash
npm run backup
```

Writes a timestamped, gzipped `pg_dump` to `backups/` (git-ignored — these
files are not meant to be committed).

```bash
npm run restore -- backups/imani-local-YYYYMMDD-HHMMSS.sql.gz
```

**This is destructive** — it overwrites every table in the local database
with the backup's contents. It requires typing `restore` to confirm and
cannot be undone. Take a fresh backup before restoring an old one if you
want to keep current data as a fallback.

## Troubleshooting

| Symptom | Likely cause |
|---|---|
| `npm run health` shows `registered: false` | Facility not registered yet, or `.env` still has dev-default values — see step 3/4 |
| Sync scripts fail to log in | `LOCAL_STAFF_USERNAME`/`LOCAL_STAFF_PASSWORD` not set, or no staff account created yet — see step 7 |
| `npm run db:up` hangs or fails | Docker Desktop isn't running |
| `npm run migrate` reports nothing applied | Already up to date — this is normal on a re-run |
| Backfill returns 0 records | Either the org genuinely has no cloud patients yet, or the backfill ran against the wrong `LOCAL_ORGANIZATION_ID` |
| `npm run staff-backfill` fails with 503 | The cloud sync-service/organization-service isn't yet deployed with the staff auth-snapshot endpoints, or `IMANI_CLOUD_SYNC_URL` is unreachable |
| `npm run staff-backfill` (or `backfill`) returns a 404-flavored error | `IMANI_CLOUD_SYNC_URL` is missing the `/sync` path suffix — see step 4 |
| A staff member's online password/status change hasn't taken effect offline yet | Refreshes automatically within 15 minutes; run `npm run staff-auth-refresh` to force it immediately |
