# Developer guide

How the codebase is organized, how the main subsystems work, and where to
extend them.

## Layout

```
server.js                        Express entry, middleware stack, route mount
Dockerfile                       node:20-alpine, argon2 native compile deps
docker-compose.yml               app + postgres services

migrations/                      node-pg-migrate versioned schema changes
scripts/
  maintenance.js                 REINDEX / drift CLI
  release.sh                     semver bump + tag + push
  import-milestones.js           one-off seed import

src/
  db/
    database.js                  pool, baseline init, query helpers
    migrate.js                   programmatic node-pg-migrate runner
  middleware/
    auth.js                      JWT + session-table validation + sliding idle
    logging.js                   request log
  utils/
    ai.js                        callAI() multi-provider router + model whitelist
    models.js                    built-in model registry
    prompts.js                   templates (DB-overridable)
    promptSafe.js                <UNTRUSTED_*> wrapping + INJECTION_GUARD
    crypto.js                    AES-256-GCM (PHI at rest)
    passwords.js                 argon2id + bcrypt fallback + rehash
    sessions.js                  token hash, UA parse, session id
    platform.js                  isMobileClient()
    redact.js                    PHI redactor for audit details
    auditQueue.js                batched audit/api/access writer
    fileType.js                  magic-byte upload verifier
    errors.js                    generic 500 responder
    logger.js                    audit + api + access + Loki shipper
    embeddings.js                Vertex / LiteLLM / OpenAI embeddings
    notify.js                    ntfy push
    transcribe*.js, tts*.js      STT / TTS provider clients
  routes/                        Express routers for auth, AI workflows, education, logs, and user data

public/
  index.html                     SPA shell, version-stamped asset refs
  sw.js                          cache shell, network-first API
  manifest.json                  PWA
  js/                            24 vanilla JS modules (no bundler)
  components/                    per-tab HTML fragments loaded on demand
  css/styles.css
  template-guide.md              downloadable user template guide

mobile/                          Capacitor 6 wrapper (Android + iOS)
.github/workflows/               CI (auto-version, APK, docker)
```

## Backend

### Middleware stack (`server.js`)

```
request
  → helmet            CSP, HSTS, X-Content-Type-Options
  → CORS              fail-closed in prod if APP_URL/CORS_ORIGINS missing
  → cookieParser
  → express.json      10 MB cap
  → rate limiters     200/min general; 10/15min login; 20/15min sensitive
  → static            public/ with per-filetype Cache-Control; ?v=BUILD_ID cache-busts HTML on deploy
  → route handlers
  → 404 fallback      serves index.html for SPA routes
```

On boot:
- `APP_VERSION` read from `package.json`, printed + returned by `/api/health/detailed`.
- `BUILD_ID` = short git HEAD SHA (or random on non-git deploys). Rewritten into HTML at startup.
- `JWT_SECRET` / `DATA_ENCRYPTION_KEY` fail-fast if missing in production.
- `initDatabase()` → `runMigrations()` → collation drift check.
- SIGTERM / SIGINT handler drains the audit queue and closes the pool.

### DB helpers (`src/db/database.js`)

```js
await db.get(sql, params);     // first row or null
await db.all(sql, params);     // array of rows
await db.run(sql, params);     // { lastInsertRowid, changes }
await db.query(sql, params);   // raw pg result
await db.getSetting(key);      // app_settings value (2 min cache)
await db.setSetting(key, v);   // writes + invalidates cache
db.pool                        // pg.Pool instance for transactions
```

SQL uses `?` placeholders (auto-converted to `$1, $2, ...`) OR native `$N`.
INSERTs without explicit RETURNING get `RETURNING id` appended.

### Auth middleware (`src/middleware/auth.js`)

```js
var { authMiddleware, adminMiddleware, moderatorMiddleware } = require('../middleware/auth');
router.post('/thing', authMiddleware, handler);            // requires auth
router.post('/admin-thing', authMiddleware, adminMiddleware, handler);
```

Sets `req.user` with `{ id, email, name, role, totp_enabled, disabled }` and
`req.sessionId`.

### AI (`src/utils/ai.js`)

```js
var { callAI } = require('../utils/ai');
var result = await callAI([
  { role: 'system', content: PROMPTS.hpiEncounter + INJECTION_GUARD },
  { role: 'user',   content: wrapUserText('transcript', transcript) }
], { model, maxTokens: 4000 });
// result = { content, model, usage }
```

Throws `{ code: 'model_not_permitted' }` if the requested model isn't in the
active allowlist.

### Prompts (`src/utils/prompts.js`)

Flat `PROMPTS` object. Any template can be overridden live by writing a
`prompt.{name}` row in `app_settings`. Admin Panel's Prompt Editor is the UX
for that.

### Settings (`src/utils/config.js`)

```js
var v = await config.get('feature.read_aloud', 'false');   // key, default
await config.set('registration_enabled', 'true');
```

2-minute in-memory cache. Writes invalidate immediately.

### Logger (`src/utils/logger.js`)

```js
logger.audit(userId, 'action', 'details', req, { category: 'auth' });
logger.apiCall(userId, endpoint, { model, tokensInput, tokensOutput, duration });
logger.access(userId, 'login', req, true);
logger.error('scope', err.message);
```

Writes go to the database (batched), the daily JSONL file, and Loki (if
configured).

## Frontend

No framework, no bundler, no build step. `public/index.html` is the only
document. Modules communicate through `window.*` globals and `CustomEvent` on
`document`.

### Tab system

`app.js` owns `activateTab(name)`:
1. Fetch `/components/{name}.html` (browser-cached for 1 h, bust by
   `?v=BUILD_ID` that the server injects).
2. Inject into `.app-body`.
3. Dispatch `CustomEvent('tabChanged', { detail: { tab: name } })`.
4. Feature modules (`soap.js`, `encounters.js`, etc.) listen for their own
   tab name and initialize DOM references inside the just-injected fragment.

### Auth model (client)

`auth.js` branches on `isNativeApp()`:

- **Web** — no localStorage token; relies on the `ped_auth` httpOnly cookie
  set by the server. `/api/auth/me` on boot verifies the session; failed
  verification shows the login screen.
- **Mobile** — token lives in `capacitor-secure-storage-plugin` (iOS
  Keychain / Android EncryptedSharedPreferences). `getAuthHeaders()` emits
  `Authorization: Bearer <token>`.

`authFetch.js` wraps `window.fetch` to catch 401 on authenticated `/api/*`
requests and force re-login. `BroadcastChannel('pedscribe-auth')` propagates
logout to sibling tabs.

### Module load order

Fixed in `index.html`:

```
secureStorage → authFetch → auth → (feature modules)
```

All `<script defer>`. Dependencies enforced by declaration order.

## Adding things

### A new AI endpoint

1. Create `src/routes/myFeature.js`:
   ```js
   var express = require('express');
   var router = express.Router();
   var { callAI } = require('../utils/ai');
   var { authMiddleware } = require('../middleware/auth');
   var PROMPTS = require('../utils/prompts');
   var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
   var logger = require('../utils/logger');

   router.post('/my-feature', authMiddleware, async (req, res) => {
     try {
       var { transcript, model } = req.body;
       var result = await callAI([
         { role: 'system', content: PROMPTS.myFeature + INJECTION_GUARD },
         { role: 'user',   content: wrapUserText('transcript', transcript) }
       ], { model });
       res.json({ success: true, text: result.content });
       logger.audit(req.user.id, 'generate_my_feature', 'Generated', req, { category: 'clinical' });
     } catch (err) {
       res.status(500).json({ error: 'Request failed' });
     }
   });
   module.exports = router;
   ```

2. Mount in `server.js`:
   ```js
   app.use('/api', require('./src/routes/myFeature'));
   ```

3. Add the template to `src/utils/prompts.js`.
4. Add a component under `public/components/myfeature.html`.
5. Add `public/js/myFeature.js` with a `tabChanged` listener.
6. Register the tab button in `public/index.html`.

### A new table / column

New changes go in a migration file. See `docs/migrations.md`.

```bash
docker exec -w /app pediatric-ai-scribe npm run migrate:new -- add_my_table
# edit the generated file
```

### A new setting

1. Optional — add a default in the `defaults` array inside `initDatabase()`
   (only needed if the app should seed it on fresh installs).
2. Read at runtime: `await db.getSetting('my_key')`.
3. Admin-editable automatically through `PUT /api/admin/config` which accepts
   arbitrary keys.

## Physician memory / correction tracker

1. On note generation, `trackAIOutput(elementId, text)` captures the original
   output in memory.
2. User edits the note in a contenteditable field.
3. On Save, `saveCorrection(elementId, section)` diffs current vs. original.
4. If changed by > 2 words or > 20 characters, `POST /api/memories/correction`
   stores the before/after in `user_memories` with category
   `correction_{section}`.
5. Next generation: `GET /api/memories/context` fetches the 10 most recent per
   category and `src/utils/prompts.js` injects them as
   `[STYLE HINTS (low priority)]` 200-character snippets.

Tabs with correction capture: Live Encounter, SOAP, Dictation, Sick Visit,
Well Visit (Hospital Course and Chart Review save corrections when available
but don't always have a trackable single output element).

Maximum 20 corrections retained per category (oldest deleted).

## Route reference

| File | Mount | Auth | Purpose |
|---|---|---|---|
| `auth.js` | `/api/auth` | Public | Register, login, 2FA, email verify, password reset, backup codes |
| `oidc.js` | `/api/auth` | Public | OIDC SSO (Authorization Code + PKCE) |
| `sessions.js` | `/api/sessions` | Auth | Active sessions list + revoke |
| `hpi.js` | `/api` | Auth | HPI from encounter or dictation |
| `soap.js` | `/api` | Auth | SOAP generation |
| `chartReview.js` | `/api` | Auth | Chart review / pre-charting |
| `hospitalCourse.js` | `/api` | Auth | Hospital course |
| `wellVisit.js` | `/api` | Auth | Well visit + SSHADESS |
| `sickVisit.js` | `/api` | Auth | Sick visit |
| `milestones.js` | `/api` | Auth | Developmental milestone narratives |
| `refine.js` | `/api` | Auth | Refine / shorten / clarify |
| `transcribe.js` | `/api` | Auth | STT (5 providers) |
| `tts.js` | `/api` | Auth | TTS (3 providers) |
| `encounters.js` | `/api` | Auth | Save / load / optimistic-lock encounters |
| `memories.js` | `/api` | Auth | Templates + corrections |
| `audioBackups.js` | `/api` | Auth | Encrypted audio retry store |
| `documents.js` | `/api` | Auth | S3 documents (magic-byte checked) |
| `userPreferences.js` | `/api` | Auth | Per-user STT/TTS choice |
| `nextcloud.js` | `/api` | Auth | Encrypted WebDAV tokens |
| `billing.js` | `/api` | Auth | ICD-10 / CPT code suggestion |
| `logs.js` | `/api` | Auth | Usage + audit dump (admin-only endpoints gated further) |
| `admin.js` | `/api/admin` | Admin | User management |
| `adminConfig.js` | `/api/admin` | Admin | Settings, prompts, models, SMTP, OIDC |
| `adminMilestones.js` | `/api/admin` | Admin | Milestone data management |
| `learningHub.js` | `/api/learning` | Auth | Content delivery + quizzes |
| `learningAdmin.js` | `/api/admin/learning` | Moderator | CMS CRUD |
| `learningAI.js` | `/api/admin/learning` | Moderator | AI content gen, PPTX export |

## Frontend JS module reference

| File | Purpose |
|---|---|
| `secureStorage.js` | Platform-branched token storage (Keychain / localStorage) |
| `authFetch.js` | Global fetch wrapper (401 → logout + reload) |
| `auth.js` | Login / register / SSO / session management / Turnstile / backup-code modal |
| `app.js` | Tab navigation, model selector, audio recorder, transcription orchestration |
| `liveEncounter.js` | Live recording UI + live preview |
| `soap.js`, `hpi.js` (none — in liveEncounter), `sickVisit.js`, `wellVisit.js`, `hospitalCourse.js`, `chartReview.js` | Clinical tabs |
| `milestones.js` + `milestonesData.js` | Milestones tab |
| `shadess.js` | SSHADESS adolescent assessment |
| `encounters.js` | Save / load / resume with optimistic lock |
| `memories.js` | Physician templates + corrections UI |
| `correctionTracker.js` | Captures AI-output edits |
| `speechRecognition.js` | Explicit opt-in browser Web Speech support |
| `speechRecognition.js` | Web Speech API preview |
| `voicePreferences.js` | Per-user STT/TTS override |
| `audioBackup.js` | Server + IndexedDB backup retries |
| `nextcloud.js` | Connect / export |
| `documents.js` | S3 upload / download |
| `calculators.js` | Pediatric calculators (BP, BMI, growth, bilirubin, vitals, etc.) |
| `learningHub.js` | Content browser + CMS editor |
| `admin.js` | Admin panel (users, settings, prompts, models) |

## Common tasks

### Change default temperature

Edit the default in `callAI()` in `src/utils/ai.js`. Per-call `options.temperature`
overrides.

### Override a prompt without deploying

Admin Panel → Prompts → pick the template → edit → save. Takes effect
immediately; cache is invalidated on write.

### Add a model to the dropdown

Admin Panel → Models → Add Custom Model. Exact provider ID, display name,
cost string, category (`free` / `fast` / `smart` / `premium`). Appears
immediately for all users.

### Trace an AI call

```bash
docker compose logs -f pediatric-scribe | grep '\[AI\]'
```

Or:

```sql
SELECT endpoint, model_used, tokens_input, tokens_output, duration_ms, cost_estimate, error
FROM api_log
ORDER BY timestamp DESC
LIMIT 20;
```

### Force a re-index

```bash
docker exec -w /app pediatric-ai-scribe npm run maint:reindex
```

Runs after any Postgres image upgrade if the startup drift check didn't
catch it.

## Local development

```bash
docker compose up -d postgres      # just the DB
npm install
cp .env.example .env               # set JWT_SECRET, DATA_ENCRYPTION_KEY, provider credentials
node server.js
```

App binds `http://localhost:3000`. Without `APP_URL`, production-mode guards
relax (open CORS, non-secure cookies) — never deploy like this.
