Commit graph

120 commits

Author SHA1 Message Date
Daniel
f71e1d6570 chore: delete vanilla tree — React+Tailwind is the only client now
Closes the migration. Every line of vanilla JS/CSS/HTML that used to
render the user-facing app is gone. The React bundle in public/app/
is the entire client.

Deleted (everything was 100% covered by ported React equivalents):
  • public/index.html              (vanilla shell — auth screen + tab loader)
  • public/js/                     (~30 vanilla modules: app.js, auth.js,
                                     admin.js, calculators.js, peGuide.js,
                                     bedside/*, learningHub.js, encounters.js,
                                     etc. — all replaced by client/src/pages/*
                                     and client/src/data/*)
  • public/components/             (18 lazy-loaded HTML fragments — not
                                     loaded by any React route)
  • public/css/styles.css          (vanilla design system — superseded by
                                     Tailwind + shadcn classes throughout
                                     the React tree)
  • public/e2e-harness.html        (vanilla-only Playwright bootstrapper)
  • e2e/tests/bedside-smoke.spec.js
  • e2e/tests/top-calculators.spec.js
                                   (the two e2e specs that exercised
                                     /e2e-harness.html and the vanilla
                                     calculators directly — replaced by
                                     calculators-react.spec.js +
                                     bedside-react.spec.js + the 136
                                     vitest parity tests in
                                     shared/clinical/*.test.ts)

Moved (still needed by the backend):
  • public/js/pediatricScheduleData.js → src/data/pediatric-schedule-data.js
    Required by src/routes/wellVisit.ts at runtime; should never have been
    in public/ anyway since it carries server-side schedule + growth +
    BMI reference tables and was being served as a 2120-line public JS
    blob to every browser. Not in public/ means it's not exposed to
    anonymous web requests anymore.

server.ts
  • Removed the dead getTemplatedIndex() / INDEX_PATH machinery that
    used to BUILD_ID-stamp /js/* and /css/* references in the vanilla
    HTML. Vite's hashed asset URLs already do that job for the React
    bundle.
  • express.static cache header: removed the /components/ branch
    (folder no longer exists) and changed /js/ + /css/ from 1-hour
    to 1-day immutable cache — safe because Vite hashes the
    filenames on every build.

Backend tsc + client tsc + vite build all green. 136/136 vitest
parity tests still pass against the captured calc-vectors.json.
Initial bundle unchanged at 343.97 kB / 106.59 kB gz.
2026-04-24 02:56:04 +02:00
Daniel
c157c709c8 feat: React auth screen + SPA now serves every route (vanilla retired)
The final big piece of "everything in React + Tailwind". Login,
register, forgot-password, reset-password, and email-verification all
render from the React bundle now. The root path / serves the SPA,
vanilla index.html + public/js/* are no longer served by the server.

BACKEND — src/routes/auth.ts
  New GET /api/auth/public-config (public — no auth required) returns
  { registrationEnabled, turnstileSiteKey, oidcEnabled,
    disableLocalAuth, ssoButtonLabel }.
  Single round-trip the React auth screen needs on mount. Reuses
  existing DB settings; no new tables.

BACKEND — server.ts
  • / and /index.html now send public/app/index.html (React SPA),
    not public/index.html (vanilla).
  • /auth, /reset-password, /verify-email explicitly route to the SPA
    so the email links land on the React router.
  • /app/*splat preserved as an alias so old bookmarks keep working.
  • SPA fallback added after express.static so hard-refresh on
    /encounter / /bedside / /settings etc. serves the React index
    instead of 404ing. API paths and static-file extensions still
    fall through to their existing handlers.
  • The dead app.get('/') duplicate that also pointed at the vanilla
    index is removed.

CLIENT — React auth flow
  client/src/pages/Auth.tsx (new)
    Login / register / forgot sub-forms with a single useQuery on
    ['public-config'] driving Turnstile + SSO button visibility.
    Login flow handles all three vanilla-equivalent responses
    (token / requires2FA / needsVerification). 2FA field reveals
    inline when the server asks for it; resend-verification link
    appears when needsVerification fires. SSO button renders
    whenever oidcEnabled is true, even if local auth is disabled
    (disableLocalAuth hides the login/register/forgot forms
    entirely). HIPAA notice + APK download link preserved.

  client/src/pages/ResetPassword.tsx (new)
    Reads ?token=xxx from the URL, POSTs /api/auth/reset-password.
    Confirm-password match, 8+ char validation, server
    passwordWarning (pwned password) surfaces as an amber info box.
    Redirects to /auth 2.5 s after success.

  client/src/components/Turnstile.tsx (new)
    Loads the challenges.cloudflare.com/turnstile script once,
    renders a widget per form, calls onToken(token) on success and
    onToken('') on error / expiry. If siteKey is null/empty (e2e
    container with TURNSTILE_SITE_KEY="") renders nothing and
    auto-reports empty — matches the vanilla no-key-no-widget
    behaviour.

  client/src/components/AuthGuard.tsx (new)
    useQuery(['auth-me']) with retry: false. On 401/error redirects
    to /auth?next=<current-url> so the deep link survives sign-in.
    Used as a parent route in App.tsx wrapping every private page.

  client/src/components/Layout.tsx
    "← back to legacy app" link replaced with "Sign out" — calls
    POST /api/auth/logout then window.location = /auth.

  client/src/App.tsx
    BrowserRouter no longer has basename (was "/app"). Public
    routes: /auth, /reset-password. Everything else lives under
    <AuthGuard> → <Layout>. Lazy-loaded Auth + ResetPassword join
    the existing heavy-route code-split.

  client/vite.config.ts
    base stays "/app/" so hashed asset URLs resolve to
    /app/assets/... (served unchanged by express.static).

shared/types.ts + client/src/shared/types.ts — additive:
  PublicConfigOk { registrationEnabled, turnstileSiteKey,
    oidcEnabled, disableLocalAuth, ssoButtonLabel }.

Bundle — Auth chunk splits out at 10.87 kB / 3.35 kB gz, lazy-loaded
only on the sign-in path; initial bundle unchanged at 343.97 kB /
106.59 kB gz.

Backend tsc + client tsc + vite build + 136/136 vitest all green.
2026-04-24 02:45:21 +02:00
Daniel
72df4f4852 feat: port Vaccine Schedule + Catch-Up Schedule (real tables, not stubs)
Instead of shipping placeholder pages, added a new backend endpoint
so the React client can render the real AAP/CDC tables without
duplicating the 2000-line pediatricScheduleData module in the client
bundle:

  GET /api/schedule-data  (authed)
    Returns { visitAges, periodicity, catchUpSchedule, vaccineFullNames }
    — everything the vaccine table and catch-up views need from the
    server-side pediatricScheduleData require(). VACCINE_FULL_NAMES
    (which was inlined in public/js/wellVisit.js) now lives in the
    route file so it's single-sourced.

client/src/pages/VaxSchedule.tsx
  useQuery → /api/schedule-data. Renders the full vaccine × visit-age
  grid with sticky header + sticky first column for long scrolling.
  Each filled cell shows the dose number or bullet, with the original
  note text on hover.

client/src/pages/Catchup.tsx
  Card-per-vaccine layout with min-age / min-interval tables and
  catch-up notes. Matches the vanilla layout.

Layout: vaccine + catch-up links now available in the sidebar.

Typecheck green both sides. Vite build 382 kB / 112 kB gzipped.
2026-04-23 22:25:41 +02:00
Daniel
f71241cd82 refactor(ts): day 4 — middleware + utils + db .js → .ts (24 files)
All remaining backend files renamed:
  src/middleware/auth.ts, logging.ts (2 files)
  src/utils/*.ts         (20 files: ai, auditQueue, config, crypto,
                          embeddings, errors, fileType, logger, models,
                          notify, passwords, platform, promptSafe,
                          prompts, redact, sessions, transcribe*,
                          ttsGoogle)
  src/db/database.ts, migrate.ts (2 files)

Spot-fixes to satisfy tsc (all within the spirit of 'no behavior
change' — added `: any` annotations where the original JS relied on
duck typing that tsc's default inference narrows too aggressively):

  utils/ai.ts — body, converseParams, request literals + fallback
    result object + err.code/model/message casts. AI client has lots
    of provider-specific ad-hoc object shapes; Day 5 will replace the
    `any`s with proper provider-response interfaces.
  utils/embeddings.ts — payload + request as `any`; generateEmbedding
    call sites pass `undefined as any` for the now-required second
    arg (model) until we refactor the signature.
  utils/prompts.ts — PROMPTS typed as Record<string, any> so
    .loadFromDb / .updatePrompt / .getAllPrompts attachments after
    the const literal compile.
  utils/transcribeLocal.ts — buildArgs() has two `var args = [...]`
    in the same function scope (var-hoisted); both now typed as
    any[] so they don't type-clash across conditionals.

Backend is now 54 of 54 TypeScript files, permissive mode.
`npm run typecheck` EXIT 0. Prod container still running the old
JS image — no Dockerfile change yet.

Next: Day 5 flips strict: true, fixes every error tsc surfaces, adds
Vitest + Zod + Knip tooling.
2026-04-23 19:52:16 +02:00
Daniel
3d597d61a8 refactor(ts): day 3 batch 6 — final 8 routes renamed
admin.ts adminConfig.ts adminMilestones.ts
  auth.ts oidc.ts
  learningHub.ts learningAI.ts learningAdmin.ts

These were the largest files (auth alone is ~600 lines). Minimum-
viable conversion: extension change + spot-fix the few places where
TypeScript's default inference caught genuine `unknown` escapes
from fetch().json() — patched with `: any` type annotations so the
compile passes. Day 5 strict-mode pass will replace those `any`s
with proper response type narrowing.

Fixes in this batch:
- adminConfig.ts: sttResp var shadowing (two declarations of same
  name with different types); renamed to sttRespFetch / sttRespAxios
- auth.ts: turnstileData + tsData from fetch(...).json() now `: any`
- auth.ts: resp object for password change / reset now typed
  { success: boolean; message?: string; passwordWarning?: string }
- learningAI.ts: parseInt(questionCount) cast through `any`

All 29 of 29 routes now .ts. Backend progress: 30/54 files
migrated (server.ts + 29 routes). Remaining: 2 middleware + 20
utils + 2 db files. Day 4 handles those.

tsc --noEmit green (EXIT 0).
2026-04-23 19:48:56 +02:00
Daniel
acdd9313d0 refactor(ts): day 3 batch 5 — billing, nextcloud (+ CPT/ICD10 types) 2026-04-23 19:46:25 +02:00
Daniel
9da31b4d34 refactor(ts): day 3 batch 4 — hospital course, encounters, memories, documents, audio backups
hospitalCourse.ts — /api/generate-hospital-course + clarify/update
  encounters.ts     — /api/encounters/saved CRUD with optimistic locking
  memories.ts       — /api/memories CRUD + /context + /correction
  documents.ts      — S3-backed /api/documents upload/list/download/delete
  audioBackups.ts   — /api/audio-backups gzip+AES storage

19 of 29 routes converted. 19/54 backend files. tsc --noEmit green.
2026-04-23 19:43:42 +02:00
Daniel
c48406536b refactor(ts): day 3 batch 3 — clinical note generators
sickVisit.ts       — /api/sick-visit/note
  wellVisit.ts       — /api/well-visit/{shadess,note}
  peGuide.ts         — /api/generate-pe-narrative (with typed PeStep)
  milestones.ts      — /api/{milestones-data, generate-milestone-narrative, generate-milestone-summary}
  chartReview.ts     — /api/generate-chart-review (typed VisitEntry etc.)

All five follow the established pattern. Added a handful of inline
interfaces (PeStep, MilestoneItem, VisitEntry) where the existing code
was juggling anonymous object shapes — these will migrate into
shared/types.ts during Day 5 if reused elsewhere.

14 of 29 routes converted. Progress: 14/54 backend files.
tsc --noEmit green.
2026-04-23 19:39:51 +02:00
Daniel
5ebabb633a refactor(ts): day 3 batch 2 — hpi, soap, refine, tts, transcribe
Same CJS-compatible pattern as batch 1 (import express = require;
const {...} = require for internal utils; export = router). No
behavior change — TS only strips annotations during compile.

AI route touchpoints:
  /api/generate-hpi-encounter, /generate-hpi-dictation  → hpi.ts
  /api/generate-soap                                    → soap.ts
  /api/refine, /shorten, /clarify                       → refine.ts
  /api/text-to-speech                                   → tts.ts
  /api/transcribe, /transcribe/status                   → transcribe.ts

All five handlers retain identical request/response shapes; the
shared/types.ts contract was defined on day 2 from these very files.

One fetch() typing fix: the transcribe route's LiteLLM branch uses
DOM fetch, but the file imports Express's Response type, so the
`.then((r: Response) => ...)` annotation was shadowing the global
Response. Removed the explicit annotation — tsc infers correctly.

Progress: 9 of 54 files migrated. tsc --noEmit green.
2026-04-23 19:36:43 +02:00
Daniel
bf47d29ab5 refactor(ts): day 3 batch 1 — convert 4 small route files
Migrates the 4 smallest and best-understood routes to TypeScript:
  src/routes/logs.ts
  src/routes/userPreferences.ts
  src/routes/sessions.ts
  src/routes/extensions.ts   (11 Playwright tests cover this one)

Pattern established for the remaining 25 routes:

  import express = require('express');            // CJS-style, fully typed
  import type { Request, Response } from 'express';
  const db = require('../db/database');           // stays `any` until Day 4
  const { authMiddleware } = require('../middleware/auth');

  const router = express.Router();
  router.get('/foo', async function (req: Request, res: Response) {
    // req.user is typed via src/types/express.d.ts augmentation
  });

  export = router;                                 // CJS-compatible export

Why `import = require()` instead of `import from`:

Express is imported via the namespace-import syntax so tsc preserves
`require("express")` in the emitted CJS. Using plain ES import would
emit `require("express").default` which doesn't exist on Express'
CommonJS default export. The pattern keeps the compiled dist/*.js
byte-identical to what vanilla Node expects.

Why `const { authMiddleware } = require(...)` for other imports:

The middleware + utils files are still .js and their module.exports
shape isn't fully typed yet (Day 4 work). Using `require` avoids
dragging Day 4 work forward; the destructuring still gives us the
variable name we want.

New file — src/types/express.d.ts:

Augments Express.Request with `user?: AuthUser` and `sessionId?: string`
(attached by authMiddleware). Route handlers now type-check against
`req.user!.id` instead of requiring a runtime cast.

`req.user!` uses non-null assertion because authMiddleware guarantees
user is set for every mounted route; strict mode on Day 5 will keep
the assertion but add a type-guard check inside authMiddleware itself
so it propagates.

Verification

- npm run typecheck → 0 errors
- scripts/lint-references.js → green
- Compiled dist/src/routes/logs.js diffs against the original .js by
  whitespace + var→const only. Behavior preserved.

Nothing deployed. Prod + e2e containers still run the previous image.

Progress: 1/30 (server.ts) + 4/29 routes = 5 of 54 total files migrated.
Remaining batches go in subsequent Day 3 commits.
2026-04-23 19:31:20 +02:00
Daniel
02e7281e52 fix(pe-guide): correct route imports (prod 500 regression)
The route destructured PROMPTS/INJECTION_GUARD/wrapUserText from
../utils/prompts, but PROMPTS is the module's default export and the
other two live in ../utils/promptSafe. All three resolved to undefined,
so every /api/generate-pe-narrative request crashed with
"Cannot read properties of undefined (reading 'peGuideNarrative')"
and the client surfaced "Request failed" for PE Guide narrative and
summary generation. Split the require into the two-line form that
every other AI route already uses.
2026-04-23 18:58:38 +02:00
Daniel
d859c8c5a9 feat(extensions): personal Pagers & Extensions directory with soft-delete
New top-level tab positioned after Physical Exam Guide. Per-user
directory of hospital phone extensions and pagers — grouped by location
then type, searchable, soft-deleted.

Data:
- New table user_phone_extensions (id, user_id, location, name, number,
  type CHECK (extension|pager), notes, trashed_at, timestamps).
  Partial indexes on active vs trashed rows for fast filtering.
- Not PHI — hospital internal phone directory. Plaintext.

API (all user-scoped, all params validated):
- GET    /api/extensions?trash=1&q=text  — list active or trash, optional search
- POST   /api/extensions                  — create
- PUT    /api/extensions/:id              — update (requires all three core fields)
- DELETE /api/extensions/:id              — soft-delete (sets trashed_at)
- POST   /api/extensions/:id/restore      — un-trash
- DELETE /api/extensions/:id/purge        — hard-delete (only if trashed)

All :id params parsed + validated (positive integer) before query.
All queries parameterized, every WHERE includes user_id scoping.

UI (public/js/extensions.js + components/extensions.html):
- Search bar with 200ms debounce, server-side LIKE on location/name/number/notes
- Add button expands inline form — location (with datalist of existing
  locations for autocomplete), name/dept, number, type, optional notes
- Each entry renders as a card: big monospace number, dept, type badge,
  edit + delete inline
- Grouped by location → type (Extensions / Pagers subheaders)
- Trash view: toggle shows trashed items with Restore + Purge actions
- Trash count badge on the Trash button updates after every delete/restore
- Delete requires confirm() dialog, then soft-delete (easy to undo)
- Purge from trash requires a second confirm() ("cannot be undone")
- Esc closes the form; form resets between Add and Edit
2026-04-22 18:54:26 +02:00
Daniel
c13ba04955 feat(pe-guide): step-by-step OSCE checklist, Bates/Nelson/Hutchison sourced
Replaces the generic one-line-per-component format with a step-level
checklist. Each exam component now contains 3–13 discrete steps, each
with its own Normal/Abnormal/Skip toggle and optional abnormal note.
Physician ticks the exam off step-by-step; report generation
summarises at the component level but knows exactly which steps were
performed.

Example — previously the adolescent "Cranial nerves (II–XII)" was a
single row: "How to perform: Full formal adult-pattern exam. Expected:
All cranial nerves intact." That's unhelpful. Now it's 14 discrete
steps: CN I, CN II acuity, CN II fields, CN II fundoscopy, CN II/III
pupils, CN III/IV/VI EOM, CN V sensation V1/V2/V3, CN V motor, CN V
corneal, CN VII forehead/eye-close/smile/puff, CN VIII, CN IX/X, CN
XI, CN XII — each with specific method and expected finding. Same
depth for MSK: scoliosis = 5 discrete steps (standing inspection,
Adam forward-bend, rib-hump check, scoliometer, plumb-line), joint
stability = 8 named tests (Lachman, anterior drawer, varus/valgus,
McMurray, apprehension, Neer/Hawkins, anterior drawer ankle, talar
tilt), Beighton = 5 per-joint measurements, etc.

Sources cited in code header: Bates' Guide 13th ed, Nelson Textbook
22nd ed, Hutchison's Clinical Methods 25th ed, Fenichel Clinical
Pediatric Neurology 8th ed.

Backend route accepts the flat step array (grouped by component on
the server), passes structured text to the AI with methods and
expected findings per step. Prompts updated to summarise at the
component level rather than step-by-step, so output is clinically
readable.

Scope: MSK + Neuro × 6 age groups (newborn, infant, toddler, preschool,
school-age, adolescent). More systems follow the same pattern —
append to PE_DATA.
2026-04-22 17:15:27 +02:00
Daniel
ac5292b015 feat(pe-guide): Physical Exam Guide tab — OSCE reference + narrative report
New top-level tab (positioned after Catch-Up Schedule) combining two
functions:
1. Study reference — for each (age group, system) shows OSCE-style
   components with technique, expected normal finding, and abnormal-
   feature watch-list.
2. Documentation generator — physician marks each component
   Normal / Abnormal (with free-text detail) / Skip; AI produces a
   two-section report (Technique + Findings), narrative or structured
   list format.

Scope v1: MSK + Neuro × 6 age groups (newborn, infant, toddler,
preschool, school-age, adolescent). More systems can be added to the
embedded PE_DATA in peGuide.js without route changes.

Files:
- src/routes/peGuide.js      — POST /api/generate-pe-narrative (mirrors
                                milestone-narrative pattern: AppRole-level
                                injection guard, clinical audit category,
                                PHI redaction upstream already in place)
- src/utils/prompts.js       — peGuideNarrative + peGuideList prompts,
                                structured two-section output
- public/components/pe-guide.html — demographics bar + sub-pills + cards
- public/js/peGuide.js       — embedded PE_DATA (all clinical content),
                                render + state + AI call
- public/index.html          — tab button, section, script include
- server.js                  — mount route at /api

No schema change. No PHI stored — findings live in memory only, exported
via existing copy/read-aloud/Nextcloud actions.
2026-04-22 16:52:13 +02:00
Daniel
250646110f fix(security): timing-safe forgot-password + redact log file writer
Two independent PHI-leak hardenings folded together:

1. forgot-password timing oracle
   The hit path previously did SELECT + token gen + UPDATE + SMTP send
   before responding; the miss path returned after the SELECT. An
   attacker could distinguish registered emails by response latency
   (SMTP RTT is hundreds of ms). Response is now sent immediately after
   Turnstile, with the DB and email work fired-and-forgotten in a
   background async block. Hit and miss take identical wall-clock time.

   Also hardened req.body.email to tolerate missing/non-string input
   instead of throwing 500.

2. logger.file redaction
   logger.info/warn/error wrote straight to /app/data/logs/YYYY-MM-DD.log
   without going through redact(). Current callers are metadata-only and
   safe, but any future caller writing logger.error('boom', req.body)
   would silently drop PHI to disk. Route both message and optional data
   through redact() — same helper the audit path already uses. Benign
   startup messages pass through unchanged; SSN/phone/email/DOB patterns
   are tokenised, long note-body-shaped text is truncated.
2026-04-22 01:01:00 +02:00
Daniel
6dc7870a1b feat(security): AES-256-GCM at-rest encryption for encounters and memories
Extends the existing crypto helper (already used for audio backups and the
Nextcloud token) to cover every column that can hold PHI:

- saved_encounters.transcript, .generated_note, .partial_data
- user_memories.content (templates + Dragon-style corrections)
- user_memories.name (auto-derived from original snippet on corrections,
  so effectively PHI)

Reads decrypt transparently. Legacy plaintext rows continue to work —
decryptString passes non-enc1: values through unchanged — so no migration
is required; rows re-encrypt on their next save.

The encounters list query previously used LEFT(transcript, 200) for a
preview. With ciphertext that slice is meaningless, so the route now
fetches the full columns, decrypts in Node, then slices. At 7-day auto-
delete the row count is bounded and the cost is a handful of GCM
decrypts per list call.

user_memories ORDER BY moved from (category, name) to (category, id)
since SQL can no longer order on encrypted names.

Closes the HHS breach-notification safe-harbor gap on at-rest PHI.
2026-04-22 01:01:00 +02:00
Daniel
e5f7167b8d fix: auth/API logging to Loki, TTS voice auto-detection, STT ElevenLabs support
- Add logger.audit/access calls to auth route (login, login_failed,
  login_blocked, register, password_changed, 2fa_backup_code_used,
  2fa_backup_codes_regenerated) — these previously only wrote to DB
  via raw SQL, bypassing Loki shipper
- Replace logger.info with logger.apiCall in callAI() so every AI call
  ships to Loki with model, tokens, cost, duration
- Add device identifier (parsed user agent) to audit and access logs
- Fix TTS voice/model provider mismatch: auto-detect Vertex voices
  (Puck, Charon, Kore, etc.) and ElevenLabs voice IDs, override model
  to match provider regardless of what model was previously set
- Fix TTS discovery: model IDs saved to tts.voice are detected and
  redirected to tts.model (regex for openai-tts, elevenlabs, vertex-tts)
- Fix STT transcription route: add scribe/elevenlabs/transcri to the
  isTranscriptionModel regex so ElevenLabs Scribe uses /audio/transcriptions
  endpoint instead of chat completions
- Remove OpenObserve/SigNoz code from logger (reverted to Loki-only)
2026-04-19 21:26:49 +02:00
Daniel
2f6e5a7d8f feat: neonatal calculator, DOCX/PPTX/ODT/EPUB support, gateway-agnostic URL helper, TTS/STT fixes
- Add neonatal assessment calculator: GA classification (extremely preterm through
  post term), weight-for-GA percentile (AGA/SGA/LGA) using Fenton 2013 LMS data,
  birth weight category (ELBW/VLBW/LBW/normal/macrosomia)
- Add DOCX support via mammoth, PPTX/ODT/EPUB via jszip in Learning Hub content
  generator file upload
- Add gatewayUrl() helper for consistent API URL construction — handles
  LITELLM_API_BASE with or without /v1 suffix, works with any OpenAI-compatible
  gateway (LiteLLM, Bifrost, etc.)
- Fix TTS model/voice separation: discovery now tags items as MODEL or VOICE,
  auto-detects provider from voice name (Vertex, ElevenLabs, OpenAI)
- Fix STT discovery to include ElevenLabs Scribe and Chirp models
- Fix TTS discovery to include ElevenLabs and Vertex voices alongside models
- Fix admin model test to bypass allowlist check (skipAllowlistCheck) so
  discovered models can be tested before adding
- Fix Nextcloud token decryption in learningAI.js WebDAV browse and file import
- Fix admin embedding test to show DB model name instead of hardcoded default
- Fix admin STT test to use correct endpoint for Whisper models
- Add AI gateway migration guide to configuration docs
- Add Grafana dashboard JSON for Loki log visualization
2026-04-19 02:17:06 +02:00
Daniel
5b0c296a88 Prompt-injection wrap: remaining AI routes
Applied the <UNTRUSTED_*> delimiter + INJECTION_GUARD pattern to:
  - src/routes/sickVisit.js  (chief complaint, transcript, dictation,
                               ROS, physical exam, diagnoses, style hints)
  - src/routes/wellVisit.js  (SSHADESS answers + full well-visit context)
  - src/routes/chartReview.js (PMH + all visit content + labs)
  - src/routes/hospitalCourse.js (all notes/H&P/ED + clarification
                                   & update endpoints)
  - src/routes/milestones.js (narrative + summary)

Each wraps patient-derived text in <UNTRUSTED_*>…</UNTRUSTED_*>
tags and appends the INJECTION_GUARD system instruction that tells
the model to treat wrapped content strictly as data. Operator-
supplied `additionalInstructions` stays unwrapped (trusted).
2026-04-14 05:31:54 +02:00
Daniel
7a06a4aa63 Batch of security + scale fixes
Age parser (src/routes/billing.js):
  - Now sums year + month + week + day matches so "4 yr 11 mo"
    (59 months) correctly maps to the 5-11y billing bracket instead
    of being billed as 1-4y. Added bounds sanity check.

Graceful SIGTERM shutdown (server.js):
  - Closes the HTTP listener first, then drains batched audit queues,
    then ends the Postgres pool. 9-second hard deadline to beat
    Docker's 10-second SIGKILL. Previously an in-flight note save
    during a container restart could truncate the write.

Explicit LLM fallback opt-in (src/utils/ai.js):
  - The OpenRouter / LiteLLM silent fallback now requires admin
    setting `ai.allow_model_fallback = true` (default: false). If
    primary fails and fallback is disabled, the error is surfaced
    to the caller. Prevents silent spillover from a BAA-covered
    primary to a non-covered fallback.

Prompt injection delimiters (src/utils/promptSafe.js):
  - Wraps user transcripts, dictations, refine-instructions, and
    pasted documents in <UNTRUSTED_*>...</UNTRUSTED_*> tags and
    appends an explicit system instruction telling the model to
    treat the wrapped content as data rather than commands.
  - Applied to soap.js, hpi.js, refine.js. Extend to other AI
    routes incrementally.

Cross-tab logout sync (public/js/authFetch.js, auth.js):
  - BroadcastChannel('pedscribe-auth') — logout in one tab posts
    a message; all sibling tabs clear state and reload, dropping
    any PHI-containing UI immediately.

Backup code race-free consumption (src/routes/auth.js):
  - tryConsumeBackupCode() now uses a Postgres transaction with
    SELECT ... FOR UPDATE so concurrent login attempts using the
    same code serialize. First wins, second sees the already-
    shortened array.

Optimistic encounter locking (migrations/...add-encounter-version):
  - saved_encounters.version INTEGER NOT NULL DEFAULT 1
  - POST /api/encounters/saved accepts an expected_version and
    rejects with 409 if the row has advanced. Falls back to
    last-write-wins if the client doesn't pass one (backward compat).

Audit log batching (src/utils/auditQueue.js):
  - Audit / api_log / access_log writes are buffered in memory and
    flushed every 1s or every 50 entries via one multi-row INSERT.
    Under load this reduces DB pressure by ~50x. On SIGTERM the
    shutdown path drains the queue before exiting.
2026-04-14 05:24:40 +02:00
Daniel
8db25f39be Enforce server-side LLM model whitelist + scope idle timeout to writes
Two findings from review:

1. callAI() previously accepted any model string from the client.
   POST /api/hpi with { model: "openai/o1" } would call the reasoning
   model regardless of whether the operator enabled it. Added
   getAllowedModelIds() in src/utils/models.js (60s TTL DB-backed
   cache) and a guard at the top of callAI() that rejects with
   "model_not_permitted" when the requested ID isn't in the active
   roster. No model supplied → silent fallback to DEFAULT_MODEL.

2. Middleware was updating user_sessions.last_activity on every
   request, including GETs. Client-side polling (/api/auth/me
   heartbeats, dashboard refreshes, log tail calls) kept sessions
   alive indefinitely, defeating the 24h sliding idle policy. Now
   only POST/PUT/DELETE/PATCH count as "user activity". GETs are
   read-only and often automated — they no longer extend the
   session. Idle enforcement still runs on every method, so a
   24h-idle user still gets kicked on their next GET.
2026-04-14 05:15:55 +02:00
Daniel
64ac4ff6bb Add node-pg-migrate for versioned schema changes + better mobile UA labels
Infrastructure only — no existing data or tables modified.

  src/db/migrate.js           — programmatic runner, fires at boot after
                                 the existing idempotent initDatabase()
  migrations/1744600000000...  — intentionally empty example, documents
                                 the file shape. Registered in the new
                                 pgmigrations tracking table so it won't
                                 rerun.
  .node-pg-migraterc.json     — CLI config (migrations-dir, utc naming)
  docs/migrations.md          — workflow + conventions
  package.json                — migrate:up/down/new/status npm scripts
                                 (status is a direct pgmigrations query
                                 since node-pg-migrate v7 lacks a status
                                 subcommand)

src/utils/sessions.js:
  - parseUserAgent now recognizes the Capacitor wrapper (UA suffix
    "PedScribe-Android" / "PedScribe-iOS") and labels sessions
    "PedScribe (Android)" instead of "Chrome on Android".

Going forward: schema changes go in /migrations as versioned files
with up() + down(); the inline init in database.js is the implicit
baseline for everything already in production.
2026-04-14 05:06:19 +02:00
Daniel
040218a7bf Fix critical auth bug: set httpOnly cookie on local login/register
After the hybrid auth migration, web users log in but the
setAuthCookie() helper was never actually called in /login or
/register — only in the OIDC callback. Result: local sign-in worked
until the first page reload, then the user appeared logged out. The
Settings page's Active Sessions list came up empty because
/api/sessions received no auth.

Added setAuthCookie(res, token) calls on successful:
  - /register (auto-verified first admin path)
  - /login (after TOTP / backup code verification)

Mobile is unaffected — it uses Bearer from Keychain and always has.
2026-04-14 04:55:12 +02:00
Daniel
ed015f3774 Fix local-auth sections not showing for normal users + backup-code modal signature
settings load2FAStatus():
  - Explicit credentials: 'same-origin' on the /me fetch (was relying
    on fetch defaults, which can behave oddly in some browsers/edges)
  - Fall back to window.CURRENT_USER (cached at login) if /me fails,
    so local-auth users still see their password/2FA sections after
    a transient error. Keeps cache in sync on each successful fetch.

enterApp():
  - Cache the logged-in user object on window.CURRENT_USER so modules
    that need the canLocalAuth flag don't have to re-fetch /me.

2FA regenerate modal:
  - Previous call passed a wrong-shape options object to showConfirm.
    Updated to the correct (message, callback, opts) signature with
    input:true, inputType:'password', placeholder, required.

OIDC email_verified check:
  - Accept boolean true or string 'true' for robustness. Some IdPs
    serialize ID-token booleans as strings.
2026-04-14 04:43:07 +02:00
Daniel
f9732f25d0 Server-side SSO/local-auth enforcement + OIDC account-link hardening
Endpoint guards (defense-in-depth over hidden UI):
  - POST /api/auth/change-password: 400 with SSO-aware message if
    the caller's stored password is not a real bcrypt/argon2 hash.
    Prior behaviour was to fail at passwords.verify() with an
    ambiguous "current password is incorrect".
  - POST /api/auth/setup-2fa: 400 with same SSO-aware message for
    SSO-only accounts. Prior behaviour allowed TOTP setup on an
    account where it could never actually trigger (user never logs
    in locally).

OIDC account-link safety (src/routes/oidc.js):
  - Auto-link to an existing local account now requires the IdP to
    assert email_verified=true in the ID token (or userinfo). If
    absent/false, the callback redirects with ?error=email_unverified.
    Prevents an attacker at a misconfigured IdP from taking over a
    local account by claiming an email they don't own.
  - If an existing user already has oidc_sub set and the incoming
    sub is different, refuse with ?error=sub_mismatch. Prior
    behaviour silently did nothing, hiding a potential attack.
  - Audit 'oidc_linked' written on first successful link.

Frontend:
  - Added user-facing messages for the two new SSO error codes.
2026-04-14 04:35:00 +02:00
Daniel
97f60876c5 Idle timeout observability + cut write frequency in half + hide local-auth UI for SSO-only users
Middleware:
  - Log to console.warn + audit_log when a session is killed for
    inactivity. Shows up in Grafana/Loki so you can see how often
    users actually get kicked. Audit action: 'session_idle_timeout'
  - last_activity throttle bumped 5 min → 10 min — halves DB writes
    per active user. Idle precision slop widens to 24h00-24h10;
    still invisible in practice.

Per-user local-auth visibility:
  - /api/auth/me now returns user.canLocalAuth: true when the stored
    password is a real bcrypt / argon2 hash, false for the random
    blob OIDC auto-creates for SSO-only users.
  - Settings page hides "Change Password" and "Two-Factor
    Authentication" sections when canLocalAuth is false — those UIs
    are meaningless for users whose sign-in lives at the IdP.
  - Password hash is not leaked in the /me payload.

Mobile (restating existing behaviour for clarity): no idle check,
365-day JWT in Keychain/Keystore, never auto-logs-out. Only logout
triggers are: manual logout, password change, admin revoke, JWT hit
365d, or app uninstall.
2026-04-14 04:32:53 +02:00
Daniel
c411e5f16f Sliding 24h idle timeout (web) + persistent mobile + 2FA backup codes
Session model:
  Web     — 24h sliding idle timeout enforced server-side via
             user_sessions.last_activity. 30-day JWT + cookie are a
             safety net; middleware is the real clock. Cookie is
             re-set on active use so browsers match the sliding window.
  Mobile  — 365-day JWT, no idle timeout (stays persistent via Keychain
             / Keystore). Detected via User-Agent ("PedScribe" /
             "Capacitor") or X-Client: mobile header.

2FA backup codes:
  - 10 single-use codes generated when 2FA is first enabled
  - Stored as bcrypt hashes in new users.totp_backup_codes column
  - Consumed atomically on successful login fallback (when TOTP fails)
  - Regenerate endpoint (POST /api/auth/2fa/backup-codes) requires
    current password; invalidates prior codes
  - Count endpoint (GET /api/auth/2fa/backup-codes/count) powers a
    "N codes remaining" indicator on the 2FA settings card
  - Modal shows codes exactly once with Copy + Download .txt actions
  - Codes cleared when 2FA is disabled

New files:
  src/utils/platform.js — isMobileClient() helper

Schema migration (idempotent):
  ALTER TABLE users ADD COLUMN IF NOT EXISTS totp_backup_codes TEXT
2026-04-14 04:24:54 +02:00
Daniel
6c98e36511 Session lifetime: 7 days → 24 hours
Shortens both JWT expiresIn and httpOnly cookie maxAge to 24h in
auth.js (local + register + reset flows) and oidc.js (SSO callback).

Rationale: shorter absolute session window for a PHI-adjacent app.
No sliding idle refresh — user re-logs in once a day.
2026-04-14 04:17:54 +02:00
Daniel
cd2513d361 Pin critical auth indexes to COLLATE "C" (ICU-drift immune)
idx_users_email and idx_sessions_token_hash now use byte-order
collation so a future ICU library bump cannot silently corrupt the
indexes the way it did this week. The columns themselves retain
their default collation; only the index comparison is C, which is
safe for these because:

  - users.email is lowercased ASCII in practice
  - user_sessions.token_hash is SHA-256 hex (pure ASCII)

Both are used for equality lookups only, never ORDER BY. Migration
is idempotent, gated on app_settings.migration.text_indexes_c.

Slug indexes on learning_* tables left at default for now — those
are also ASCII in practice but under lighter load; the startup
drift check + auto-REINDEX covers them.
2026-04-14 03:55:09 +02:00
Daniel
9423ffc3a7 Collation-drift guard + lookup-miss visibility
Root cause of recent "invalid credentials on correct password" was
a silent btree index corruption: pgvector/pgvector:pg16 was pulled
with a different ICU library than the one used to build existing
indexes. Queries returned 0 rows even though matching heap rows
existed. Postgres logged nothing (corrupt index → empty result set
is a "successful" query) and the login path never logged unknown-
user attempts (enumeration protection).

Three defenses:

  1. Pin postgres image by digest in docker-compose.yml so a
     silent pull can't change ICU under our feet.
  2. Startup collation-drift check in src/db/database.js:
     compares pg_database.datcollversion to the library's actual
     version and, on mismatch, runs REINDEX DATABASE + ALTER
     DATABASE REFRESH COLLATION VERSION. Logs "Collation versions:
     aligned" on clean boot.
  3. Server-side console.warn on login lookup-miss (no email, no
     audit row — preserves enumeration protection but gives
     Grafana/Loki a signal for unusual miss rates).
2026-04-14 03:52:29 +02:00
Daniel
43d26fd306 Login: remove temporary debug logging
Root cause for "invalid credentials" on correct password was a
corrupt btree index (idx_users_email) causing user lookups to miss
existing rows. Fixed by REINDEX DATABASE. Keeping a typed catch
around passwords.verify() so any future verify throw is logged
cleanly instead of bubbling as 500.
2026-04-14 03:48:56 +02:00
Daniel
74c5cde8e1 Stop leaking e.message to clients across all routes
88 occurrences of res.status(500).json({ error: e.message }) (or
err.message) swept to generic 'Request failed'. Server-side
console.error / logger.error calls are untouched, so the full detail
still lands in logs and Grafana.

Covers: admin, adminConfig, adminMilestones, chartReview, documents,
encounters, hospitalCourse, hpi, learningAdmin, learningAI, learningHub,
logs, memories, milestones, oidc, refine, sessions, sickVisit, soap,
userPreferences, wellVisit.

Also extends .gitignore to exclude .env.backup-* files.
2026-04-14 03:04:24 +02:00
Daniel
7c45367c02 Security hardening: PHI encryption, argon2, DOMPurify, SRI
- App-layer AES-256-GCM crypto helper (src/utils/crypto.js)
- Nextcloud tokens encrypted at rest; transparent migration on next use
- Audio backups encrypted at rest (version byte 0x01 envelope); legacy
  rows still decrypt as-is until overwritten
- argon2id password hashing via src/utils/passwords.js with bcrypt
  fallback; bcrypt hashes rehashed to argon2id on next successful login.
  argon2 package is optional — server keeps running with bcrypt only
  until npm install adds the native dep
- PHI redactor for audit log details (src/utils/redact.js) — strips SSN,
  phone, email, DoB, long IDs; caps at 500 chars; detects note bodies
- DOMPurify (cdnjs, SRI-pinned) replaces custom regex sanitizer in
  Learning Hub content rendering
- SRI integrity hashes added for Font Awesome CSS and Chart.js
- Magic-byte file-type verification on document uploads
  (src/utils/fileType.js)
- Generic 500 error responses via src/utils/errors.js applied to
  nextcloud and audioBackups; full detail still logged server-side
- DATA_ENCRYPTION_KEY env documented in .env.example

Deploy: requires rebuild of the container image to pick up the new
files and `npm install` (adds argon2). Existing users keep working
because bcrypt stays available and crypto helpers pass through
plaintext when the key is not yet set in dev.
2026-04-14 02:49:38 +02:00
Daniel
e625c634b6 Security hardening: low-risk easy wins
- JWT_SECRET fails fast at startup in production
- CORS fails closed if APP_URL + CORS_ORIGINS are both missing
- Explicit HSTS (1y, includeSubDomains, preload)
- Rate limit sensitive auth endpoints (change-password, 2FA)
- /api/health now returns {ok:true}; details gated behind admin auth
- Login enumeration removed — generic 401 + dummy bcrypt on miss
- ReDoS guard: 20KB input cap on /suggest-codes
- showToast uses textContent, no innerHTML
- clearSession() clears service worker caches on logout
- OIDC state is now HMAC-signed and stateless (survives restart)
- SSRF guard on admin-set OIDC issuer (blocks private IPs, requires HTTPS)

Adds docs/mobile-build.md covering APK build, release, git push,
keystore, and troubleshooting for both PedScribe and PedsHub apps.
2026-04-14 02:42:32 +02:00
Daniel
011fae9b7a Enhance audit logging: user agent, session ID, PHI access tracking
Loki logs now include:
- User agent string (browser/device identification)
- Session ID (ties actions to specific login session)
- Status field (success/failure)

New logging:
- encounter_load: logged when user opens a saved encounter (with label)
- copy_to_clipboard: logged when user copies note content (PHI access)
- Client event endpoint: POST /api/logs/client-event (auth required)

Encounter save/delete/load all include the encounter label for
patient identification in audit trail.

HIPAA audit trail now covers: who, what, when, from where, which
device, which session, what patient data, success/failure.
2026-04-11 06:17:05 +02:00
Daniel
47844ff29b Add Loki + Grafana monitoring stack, ntfy notifications, biometric auth
Monitoring:
- docker-compose.monitoring.yml — opt-in Loki + Grafana stack
- Loki config with 6-year retention (HIPAA compliant)
- Grafana auto-provisioned with Loki datasource + PedScribe dashboard
  (login activity, failed logins, clinical actions, API calls, log viewer)
- Logger ships to Loki in parallel with PostgreSQL (fire-and-forget)
- Labels: app=pedscribe, type=audit|api_call|access, category, action

Usage: docker compose -f docker-compose.yml -f docker-compose.monitoring.yml up -d
Grafana at localhost:3003 (admin/pedscribe)

Notifications:
- ntfy push support (src/utils/notify.js)
- Notifications on: login, password change, registration
- Self-hosted, no Firebase dependency

Mobile:
- Biometric auth on app launch (Face ID/Touch ID/fingerprint)
- PIN/password fallback, auto-prompt, skip option
2026-04-11 03:00:52 +02:00
Daniel
c7038d9db1 Add biometric auth, ntfy push notifications, mobile improvements
Mobile:
- Add biometric authentication (Face ID/Touch ID/fingerprint) on app launch
  with PIN/password fallback, auto-prompts on launch, skip option
- Add @aparajita/capacitor-biometric-auth plugin

Backend:
- Add ntfy push notification support (src/utils/notify.js)
  Self-hosted, no Firebase dependency, uses user's existing ntfy instance
- Notifications for: new login, password changed, new registration (admin)
- Topic format: pedscribe-user-{id} for users, pedscribe-admin for admins
- Env: NTFY_URL, NTFY_TOKEN (optional)
2026-04-11 02:35:07 +02:00
Daniel
639a5d2873 Add automatic ICD-10 and CPT billing code suggestions
New feature: after generating any clinical note, the app automatically
suggests relevant billing codes displayed as clickable chips below the output.

Backend (src/routes/billing.js):
- POST /api/suggest-codes endpoint analyzes note text
- Extracts diagnoses from Assessment section via regex
- Looks up ICD-10 codes: local common pediatric map (40+ conditions)
  first, then NLM Clinical Tables API for unknown terms
- Suggests CPT E/M codes based on note type, visit complexity,
  ROS/PE system counts, and MDM level estimation
- Supports: outpatient (new/established), well visit (age-based),
  ED, inpatient (admit/subsequent/discharge)

Frontend (public/js/app.js):
- suggestBillingCodes() renders collapsible card with ICD-10 and CPT chips
- Click any chip to copy the code to clipboard
- Shows E/M level assessment (diagnosis count, ROS, PE, MDM complexity)
- Disclaimer: "Suggestions only. Always verify codes."

Integration: called after note generation in all 6 tabs
(encounter, SOAP, sick visit, well visit, hospital course, chart review)
2026-04-11 01:50:17 +02:00
Daniel
719fe0533f Fix session revocation bug that could log out current device
- Fix: DELETE all other sessions query used empty string fallback when
  req.sessionId was undefined, causing id != '' to match ALL rows
  (including current session). Now skips deletion if sessionId unknown.
- Fix: Revoke All endpoint returns error if current session not identified
- Fix: var confirm shadowing window.confirm in password change handler
2026-04-09 02:33:00 +02:00
Daniel
09193538fb v6.2: Session management, password change, audit logging, refine context, UI fixes
Security:
- Add session management: users can view/revoke active sessions in Settings
- Add password change in Settings (requires current password, HIBP check)
- Force logout all sessions on password reset
- Fix logout to destroy server-side session (was only clearing cookie)
- Add trust proxy for correct client IP in rate limiting and audit logs
- Add CORS support for multiple domains (CORS_ORIGINS env var)
- Add HIBP breach check endpoint and inline warnings on password fields

Audit logging:
- Add audit logging to all 24 PHI-handling endpoints across 13 route files
- Covers: generation, transcription, TTS, refine, encounters, documents, Nextcloud
- All fire-and-forget (no response delay)

AI improvements:
- Refine now includes original source material (transcript, notes, labs)
  so AI can reference the full input when modifying output
- Add correction tracking (trackAIOutput) to sick visit and well visit tabs
- Fix sickvisit missing from encounter save noteIdMap

UI fixes:
- Non-blocking busy bar for transcription and AI generation (replaces full-screen overlay)
- Fix encounter recording: hide record button during recording (was showing two stop buttons)
- Fix ROS/PE "All WNL" stacking duplicate event handlers; add Clear buttons
- Enlarge AI instructions textarea in Learning Hub CMS

Domain:
- Primary domain now app.pedshub.com, with scribe.pedshub.com and peds.danvics.com as CORS origins
2026-04-08 20:27:45 +02:00
Daniel
869fa14a77 v6.1: Turnstile bot protection, LiteLLM provider, PPTX tables, audio backup fixes, docs
- Add Cloudflare Turnstile to login, register, and password reset forms
- Switch AI provider to LiteLLM, transcription to OpenAI Whisper
- Change domain to scribe.pedshub.com
- Fix PPTX export: add tables, bold/italic, numbered lists, code blocks, blockquotes
- Fix announcement banner close button (CSP was blocking inline onclick)
- Fix auth middleware: empty Bearer token now falls through to cookie auth
- Fix audio backups: only save on transcription failure, stop auto-deleting on success
- Soften AI correction injection to prevent model hallucination from correction history
- Fix LiteLLM TTS model name handling (no incorrect openai/ prefix)
- Expand AI instructions textarea in Learning Hub CMS
- Update README for v6 with all features and providers
- Add comprehensive docs/: architecture, API reference, database schema,
  authentication, AI providers, speech, learning hub, configuration, deployment
2026-04-04 22:56:24 +02:00
ifedan-ed
783679a3f7 feat: Add model search, testing, and TTS/STT/embedding management to admin
- Fix model search for all providers: Bedrock now falls back to built-in
  list (with live ListFoundationModels attempt), Azure returns built-in list
- Add Test button on every model row (built-in, discovered, custom) that
  sends a live prompt and shows response + latency in a toast
- Add TTS management section: search voices from provider API (Google TTS
  voices.list, LiteLLM /v1/models, ElevenLabs /v1/voices), Set as Default
  writes tts.voice/tts.model to DB, runtime respects DB override
- Add STT management section: search models from provider (Gemini, Whisper,
  LiteLLM, OpenAI, local), Set as Default writes stt.model to DB, runtime
  respects DB override in transcribe.js
- Add Embedding models section: search from provider (LiteLLM, Vertex,
  OpenAI), Set as Default writes embeddings.model+dimensions to DB,
  embeddings.js respects DB override
- Add record-and-transcribe STT test (browser MediaRecorder)
- Add TTS synthesize-and-play test (returns base64 audio)
- Add embedding generate test (shows dims + vector sample)
- Expand PUT /config/:key(*) whitelist to include tts., stt., embeddings.
- Add @aws-sdk/client-bedrock as optional dependency for live Bedrock discovery
2026-04-03 19:55:11 +00:00
ifedan-ed
fdf29b5ed7 v2.1: Add visible bulk import UI for developmental milestones
NEW FEATURES:
- Bulk Import button in Admin Panel → Developmental Milestones section
- "Import Default Milestones Data" button appears when database is empty
- "Re-import All" button to clear and re-import all static data
- Visible notice when no milestones exist with one-click import

IMPROVEMENTS:
- Auto-shows empty state notice when database has no milestones
- Backend bulk-import endpoint now supports clearExisting parameter
- Imports ALL age groups from static data (birth to 11 years)
- Better UX - admin doesn't need CLI to populate milestone data

FIXES:
- Makes milestone admin editing feature discoverable and usable
- No need to manually run import script anymore
2026-04-01 18:06:42 +00:00
ifedan-ed
dc2e000e88 v4: Fix milestones display + add OpenID auth + 100MB PDF support
FIXES:
- Milestones now show correctly on encounter page (use static fallback if DB empty)
- Static data preserved as MILESTONES_DATA_STATIC for compatibility
- Database-driven milestones still work (admin can edit via CMS)

NEW FEATURES:
- OpenID Connect (OIDC) authentication support (PocketID, Keycloak, Azure AD, etc.)
- Comprehensive setup guide: OPENID_SETUP.md
- Auto-linking existing users by email on SSO login
- Multiple PDF upload support in Learning Hub (up to 10 files)
- 100 MB per file limit (was 20 MB)
- Full PDF content used for AI generation
- Embeddings use first ~8K chars for semantic search

IMPROVEMENTS:
- Updated UI to show multiple file selection with list
- Drag-and-drop supports multiple files
- Better file upload validation and error handling
- Added clarifying comments about embedding truncation
2026-04-01 17:59:51 +00:00
ifedan-ed
a7dd08c9d1 Add admin dashboard for developmental milestones management
Features:
- Admin can add, edit, and delete developmental milestones via dashboard
- Milestones stored in PostgreSQL (developmental_milestones table)
- Client-side loads milestones from API instead of static file
- Import script to migrate existing static data to database
- Organized by age group and domain
- Supports sorting and filtering

Admin UI:
- New section in Admin panel for milestone management
- Filter by age group
- Add/Edit modal with validation
- Delete with confirmation
- Auto-complete for age groups and domains

API Endpoints:
- GET /api/milestones-data - Public endpoint for authenticated users
- GET /api/admin/milestones - List all milestones (admin only)
- GET /api/admin/milestones/meta - Get age groups and domains
- POST /api/admin/milestones - Create milestone
- PUT /api/admin/milestones/:id - Update milestone
- DELETE /api/admin/milestones/:id - Delete milestone
- POST /api/admin/milestones/bulk-import - Bulk import

Usage:
1. Run import script: node scripts/import-milestones.js
2. Access Admin dashboard → Developmental Milestones section
3. Add/Edit/Delete milestones as needed
2026-03-31 20:55:41 +00:00
ifedan-ed
364b686619 Add per-user voice preferences (STT model + TTS voice selection)
- NEW: User preferences for STT model and TTS voice
- Database: stt_model and tts_voice columns in users table
- UI: Voice Preferences section in Settings with dropdowns
- API: /api/user/preferences (GET/POST) + /preferences/options
- Transcribe: Respects user's STT model (Google, LiteLLM)
- TTS: Respects user's TTS voice (Google, LiteLLM, OpenAI, ElevenLabs)
- Preview: Test TTS voice before saving
- Available models/voices auto-detected from provider config
2026-03-31 14:47:00 +00:00
ifedan-ed
ea213d8baf Add Vertex AI embeddings + semantic search for Learning Hub
- New: Vector search with pgvector extension (cosine similarity)
- Embeddings: Vertex AI text-embedding-005 (768 dims, HIPAA-eligible)
- 3 search modes: keyword, semantic, hybrid (best of both)
- Auto-generate embeddings on content create/update
- Admin endpoints: /api/admin/learning/embeddings/generate (backfill), /status
- User endpoints: /api/learning/search/semantic, /search/hybrid
- Falls back to OpenAI embeddings if Vertex not configured
- Supports LiteLLM proxy routing

Models tested:
- vertex_ai/text-embedding-005 (768 dims, English+code) 
- vertex_ai/gemini-embedding-001 (3072 dims, multilingual) 
- vertex_ai/text-multilingual-embedding-002 (768 dims) 
2026-03-31 14:36:49 +00:00
Daniel Onyejesi
f13eb05218 Emails: true markdown/Resend style — plain white, no card, clean type
TTS: prefix model with openai/ so LiteLLM routes correctly

Email: horizontal rules instead of card border, spacious padding,
wordmark + divider + body + divider + footer. Reads like a doc.
TTS: tts-1 becomes openai/tts-1 automatically unless already prefixed.
2026-03-30 20:53:11 -04:00
Daniel Onyejesi
58094f6298 Clean email templates (Linear/Resend style) + LiteLLM Gemini STT
Emails: white card, clean typography, dark button, no gradients.
Same minimal aesthetic as Linear/Resend/Notion emails.
Verify page responses also updated to match.
2026-03-30 20:51:11 -04:00
Daniel Onyejesi
61ed414785 Fix LiteLLM STT: use chat/completions with Gemini audio instead of broken /audio/transcriptions
LiteLLM /audio/transcriptions gives 'Unmapped provider' for Vertex AI Chirp.
The correct approach: use /v1/chat/completions with a Gemini model and send
audio as base64 input_audio content block — Gemini natively understands audio.
Set LITELLM_STT_MODEL to your Gemini model name (e.g. gemini-2.5-flash).
2026-03-30 19:52:58 -04:00