- Drop first/second-person voice; reference-style prose throughout - Remove stale information; align with current code (argon2id primary, hybrid cookie/Bearer auth, sliding 24h idle, AES-256-GCM PHI at rest, backup codes, node-pg-migrate, collation-drift guard, multi-arch Docker, auto-version pipeline) - Preserve all technical accuracy and code examples - Remove any remaining references to separate PedsHub Quiz app - Keep consistent tone across files (tables + code blocks, imperatives where needed) - api-reference.md and developer-guide.md route tables expanded to reflect current routes (billing, sessions)
374 lines
14 KiB
Markdown
374 lines
14 KiB
Markdown
# 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/ 27 routers
|
|
|
|
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
|
|
models/ bundled Whisper WASM
|
|
|
|
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 |
|
|
| `browserWhisper.js` | In-browser WASM Whisper |
|
|
| `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.
|