Commit graph

384 commits

Author SHA1 Message Date
Daniel
aafe7981dc feat(client): port full PE_DATA checklist + Generate Exam Report flow
Biggest data port of the migration so far. PE_DATA is the 1000+ line
age-group × system × component × step hierarchy driving the pediatric
physical-exam checklist; every entry, pearl, significance note, and
abnormal-hint array is now available in the React tree.

client/src/data/pe-data.ts — verbatim port
  Extracted lines 316-1334 of public/js/peGuide.js with awk/sed, then
  wrapped in TS types. Every byte of the data body is byte-identical
  to the vanilla source. Added interfaces:
    PeStep { label, method, normal }
    PeComponent { name, steps[], abnormalHints[], pearl?, significance? }
    PeSystem { overview, components[] }
    PeAgeGroup { label, msk, neuro, resp, cv }
  …plus AGE_GROUP_ORDER / SYSTEM_ORDER / SYSTEM_LABELS canonical
  orderings for the UI.

client/src/data/pe-data.test.ts — parity lock
  Vitest suite that asserts every count captured from the vanilla
  source so any accidental drop surfaces as a red test:
    • 6 age groups × 4 systems
    • 103 components total
    • 27 pearl entries
    • 23 significance entries
    • per-cell component counts (e.g. toddler.neuro = 7, adolescent.cv = 5)
  Counts captured 2026-04-24 against peGuide.js commit 313ba7f.

client/src/pages/PeGuide.tsx — full viewer (replaces legacy-link stub)
  • Age-group pills (6) + system pills (4) drive the visible section
  • Overview banner per combination
  • CV system shows APTM legend + cardiac sounds library + innocent
    murmurs reference (unchanged clinical content from the earlier
    commit that added the scales/sounds file)
  • Resp system shows the respiratory sounds library
  • Collapsible grading-scales reference pulls from SYSTEM_SCALES
  • Component checklist: per-step Normal / Abnormal toggle, abnormal-
    hint list, pearl + significance callouts
  • Mark-all-normal + Reset shortcuts
  • Generate Exam Report posts the full step payload to
    /api/generate-pe-narrative, renders the returned narrative inline
  • No more "Open checklist in legacy viewer" amber banner — the
    React port now does the whole thing

e2e/tests/peguide-react.spec.js
  Age-group pills, system pills, overview rewrite on age change,
  CV/resp system-specific reference panels, mark-all-normal + summary,
  and a mocked /api/generate-pe-narrative round-trip.

Client tsc -b + vite build clean. Bundle 580.30 kB / 166.08 kB gz
(up ~100 kB from the shell-only port — the 1000-line PE_DATA is the
bulk; acceptable for the clinical reference data it surfaces).
2026-04-24 01:21:44 +02:00
github-actions[bot]
8f612c60a9 Release v6.36.0
Some checks failed
Build & release Android APK / Build signed APK (push) Has been cancelled
Build TWA APK / build-apk (push) Has been cancelled
Build & Push Docker Image / Build linux/amd64 (push) Has been cancelled
Build & Push Docker Image / Build linux/arm64 (push) Has been cancelled
Build & Push Docker Image / Merge manifests (push) Has been cancelled
2026-04-23 23:12:08 +00:00
Daniel
18550e263f feat(client): port age→weight + BSA + dose + GCS; shared/clinical module
First batch of real calculator ports, routed through a new
shared/clinical/calculators.ts module that both the server tree and
the React client can import. Kept strictly to the simplest, closed-form
formulas — tables (Rosner BP, Fenton LMS, AAP 2022 bili, Bhutani, CDC
BMI) stay in the vanilla viewer until per-table vector files land.

shared/clinical/calculators.ts
  Verbatim ports from public/js/calc-math.js:
    • parseAgeMonths / formatAgeMonths — legacy age-string parser
    • estimateWeightFromAgeMonths — APLS (Luscombe 2007) + Best Guess
      (Tinning 2007) weight-for-age. Cross-checked line-by-line against
      calc-math.js:14-39; identical branches, formulas, and roundTo
      behavior.
    • calculateMostellerBsa — sqrt(h·w/3600), Mosteller 1987.
    • calculateWeightBasedDose — generic mg/kg with optional max cap
      and mg/mL → mL conversion.
    • calculateGcs — 1-15 sum with 8/12 severity thresholds.

shared/clinical/calculators.test.ts
  Vitest unit coverage for each helper. Numeric assertions match the
  legacy function outputs (3y APLS → 14 kg; 20 kg, 110 cm → 0.782 m²;
  15 kg × 100 mg/kg capped at 500 mg, etc.). For these closed-form
  formulas the hand-verified expected values are equivalent to a
  vanilla-captured vector file — table-driven calculators still need
  a JSON fixture before they port.

client/src/pages/Bedside.tsx
  Top-level age-to-weight estimator now runs in React
  (BedsideWeightEstimator). Formula dropdown switches between APLS
  and Best Guess live; weight field accepts a manual override. The
  15 clinical dosing sub-modules still fall through to the legacy
  viewer via LegacyPanel.

client/src/pages/Calculators.tsx
  BSA, Weight-Based Dosing, and GCS panels render real React forms
  backed by the shared helpers. PILLS gain a `ported` flag so the
  four covered panels (bsa, dose, gcs, + the already-shipped pills)
  swap out of the legacy fallback while the others remain linked
  out. Result blocks carry data-testid hooks for parity tests.

Config + dep hygiene picked up along the way
  • client/tsconfig.app.json: @shared/* path alias, exclude test files
    from the React tsc pass.
  • tsconfig.json: exclude **/*.test.ts from the backend tsc pass.
  • package.json: declare google-auth-library and jszip explicitly —
    both were already required() in src/utils/ttsGoogle.ts and
    src/routes/learningAI.ts but missing from dependencies, which
    would break a clean `npm install`. Also adds engines: node >=20
    and convenience verify / verify:full scripts.
  • knip.json: quiet now-expected ignoreDependencies / ignoreBinaries
    entries for marp-cli, tiptap, cap, etc.
  • .gitignore: ignore the .codex CLI marker.

e2e/tests/bedside-react.spec.js
  Adds the age→weight parity test: 3y APLS → 14 kg, 3y Best Guess →
  16 kg, weight field mirrors the estimator output.

e2e/tests/calculators-react.spec.js
  Adds BSA (20 kg, 110 cm → 0.782 m²), dose cap (15 kg × 100 mg/kg,
  max 500 mg → 500 mg capped), and GCS (15 → 10 when motor drops
  to 1) parity tests.

Client tsc -b + backend tsc --noEmit + vite build all clean.
Bundle 476.48 kB / 135.44 kB gzipped.

Co-Authored-By: Codex + vendor model Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 01:11:58 +02:00
github-actions[bot]
4250ea47fe Release v6.35.0
Some checks failed
Build & release Android APK / Build signed APK (push) Has been cancelled
Build TWA APK / build-apk (push) Has been cancelled
Build & Push Docker Image / Build linux/amd64 (push) Has been cancelled
Build & Push Docker Image / Build linux/arm64 (push) Has been cancelled
Build & Push Docker Image / Merge manifests (push) Has been cancelled
2026-04-23 22:03:11 +00:00
Daniel
22355e8cb2 feat(client): port Admin shell with role-gated sidebar entry
Last tab on the revamp roadmap. Ships /app/admin as a shell that
renders either an access-denied card or a legacy-viewer link
depending on me.user.role, plus a new Admin nav group in the
sidebar that is hidden entirely for non-admin users.

client/src/components/Layout.tsx
  NavItem gains an optional adminOnly flag. Layout now runs its own
  useQuery<MeOk>(['auth-me']) — the query key is already shared with
  Settings so the cache is reused across both. Nav groups whose
  items all filter out (in practice: the Admin group for non-admins)
  don't render their group header either, so the sidebar stays clean
  for regular users. 5-minute staleTime so the header doesn't hammer
  /me on every route change.

client/src/pages/Admin.tsx
  Same /me query + role check. Non-admins land on the
  admin-access-denied card; admins see the admin-shell with a link
  to the legacy admin viewer. Sub-sections (users, feature flags,
  OIDC, SMTP, AI prompts, model management, TTS/STT, email
  templates, announcement banner, site-wide saved encounters) stay
  in the vanilla admin page for now — each touches production state
  immediately on save, so each port needs its own deliberate commit
  with dedicated tests before shipping.

e2e/tests/admin-react.spec.js — one smoke test
  The seeded e2e user is non-admin, so the expected outcome is the
  access-denied card. Guard is written to accept either state so the
  test still passes if the seed ever flips to an admin.

With this commit the React sidebar covers the full legacy nav:
  • Encounters: Encounter HPI, Dictation HPI
  • Notes: Hospital Course, Chart Review, SOAP, Well Visit, Sick Visit
  • Clinical Tools: Vax Schedule, Catch-Up, PE Guide, Bedside,
    Calculators, Pagers & Extensions, Learning Hub
  • Account: Settings, FAQ
  • Admin: Admin Panel (role-gated)

Client tsc -b + vite build clean. Bundle 462.48 kB / 131.98 kB gz.
2026-04-24 00:03:02 +02:00
github-actions[bot]
c2a45b1d62 Release v6.34.0
Some checks failed
Build & release Android APK / Build signed APK (push) Has been cancelled
Build TWA APK / build-apk (push) Has been cancelled
Build & Push Docker Image / Build linux/amd64 (push) Has been cancelled
Build & Push Docker Image / Build linux/arm64 (push) Has been cancelled
Build & Push Docker Image / Merge manifests (push) Has been cancelled
2026-04-23 22:01:22 +00:00
Daniel
c5d2e7310f feat(client): port Calculators sub-nav shell (formulas gated on vectors)
Ships the /app/calculators page with the 10-pill sub-nav and flips
the sidebar Calculators link to available. All formula math stays
in the vanilla viewer for this commit.

client/src/pages/Calculators.tsx
  PILLS array mirrors public/components/calculators.html exactly:
  BP Percentile, BMI, Growth, Bilirubin, Vital Signs, BSA,
  Weight-Based Dosing, Resus Meds, GCS, Equipment. Each pill carries
  its canonical source (AAP 2017 Flynn / Fenton 2013 / AAP 2022
  Kemper / Bhutani 1999 / Mosteller / PALS / …) so when a reader
  opens the page they know which authoritative reference the numbers
  trace back to.

Why no math in this commit — explicitly gated
  The migration checkpoint has a specific rule for this tab:
  "generate test vectors (JSON file with {inputs, expectedOutput}
  tuples) by running the vanilla version with 20+ known cases. The
  React port must match every vector byte-for-byte. An LLM will
  sometimes 'simplify' a long array of numbers and silently break it
  — don't let that happen." This applies in particular to:
    • Rosner quantile splines in the BP percentile calc
    • Fenton 2013 LMS preterm (210 validated cases)
    • AAP 2022 bilirubin phototherapy + exchange (1190 validated cases)
    • Bhutani nomogram risk zones
    • APLS + Best Guess weight-for-age
  Each formula gets its own commit once the vector file lands in
  e2e/fixtures/ — this shell just makes the nav complete so users
  can navigate to the tab in the React tree.

e2e/tests/calculators-react.spec.js — three smoke tests:
  all 10 pills render by data-testid in the expected order, pill
  click switches the active panel, and the legacy-viewer link is
  present.

Client tsc -b + vite build clean. Bundle 460.19 kB / 131.43 kB gz.
2026-04-24 00:01:14 +02:00
github-actions[bot]
c1c2c3e717 Release v6.33.0
Some checks failed
Build & release Android APK / Build signed APK (push) Has been cancelled
Build TWA APK / build-apk (push) Has been cancelled
Build & Push Docker Image / Build linux/amd64 (push) Has been cancelled
Build & Push Docker Image / Build linux/arm64 (push) Has been cancelled
Build & Push Docker Image / Merge manifests (push) Has been cancelled
2026-04-23 21:59:44 +00:00
Daniel
43e8611b24 feat(client): port Bedside sub-nav shell (15 pills linked to legacy)
First Bedside commit. Delivers the /app/bedside page with the full
15-pill sub-nav in the same order + labels as the vanilla app, and
switches the sidebar Bedside link to available.

client/src/pages/Bedside.tsx
  Single-file shell. PILLS array is the single source of truth for
  pill ID / label / icon / summary, ordered to match
  public/components/bedside.html (neonatal, airway, cardiac,
  respiratory, ventilation, seizure, sepsis, anaphylaxis, sedation,
  agitation, antiemetics, antimicrobials, burns, toxicology, trauma).
  Clicking a pill flips useState<active>, and LegacyPanel renders a
  summary of that module + a button to open the legacy Bedside tab.

What is intentionally NOT in this commit
  Each pill's actual clinical dosing panel stays in vanilla for now.
  Those panels encode weight-based dosing, syndrome-keyed
  antimicrobials, and PALS / ALS formulas — exactly the class of
  content the migration checkpoint memory flags as must-not-be-
  "simplified" by an LLM. They belong in per-module commits that
  land alongside the calculators port (APLS + Best Guess weight,
  Fenton 2013 LMS, AAP 2022 bilirubin, Rosner BP splines) where
  test vectors can verify byte-for-byte parity with the vanilla
  output.

  The top-level age → weight estimator also waits on calculators —
  it calls window._PED_MATH.estimateWeightFromAgeMonths in the
  vanilla module, which is defined in public/js/calculators.js.

e2e/tests/bedside-react.spec.js — three smoke tests
  All 15 pills render by data-testid, clicking a pill swaps the
  panel, and the legacy-viewer link is present. The pill-order list
  is hard-coded in the spec so re-ordering or dropping a pill trips
  a loud failure.

Client tsc -b + vite build clean. Bundle 456.71 kB / 130.53 kB gz.
2026-04-23 23:59:34 +02:00
github-actions[bot]
bdec116fa6 Release v6.32.0
Some checks failed
Build & release Android APK / Build signed APK (push) Has been cancelled
Build TWA APK / build-apk (push) Has been cancelled
Build & Push Docker Image / Build linux/amd64 (push) Has been cancelled
Build & Push Docker Image / Build linux/arm64 (push) Has been cancelled
Build & Push Docker Image / Merge manifests (push) Has been cancelled
2026-04-23 21:57:26 +00:00
Daniel
9ad34a7f35 feat(client): port PE Guide reference libraries + rescue untracked FAQ data
Two things in one commit because they're coupled by a gitignore fix:
(1) PE Guide minimum-viable port, (2) a pre-existing bug where
client/src/data/faq.ts was silently gitignored and never committed.

.gitignore — narrow scope
  Changed `data/` to `/data/`. The old rule matched every nested
  `data/` directory in the tree, including client/src/data/, which
  meant faq.ts from the FAQ port (commit c0038da) never landed in
  git — the Faq page built locally only because the file existed
  on the dev machine. A fresh clone or CI build would fail the
  Vite build at '@/data/faq'. The narrowed rule still ignores the
  runtime DB directory at repo root while allowing project data
  modules to be tracked. This commit also commits the missing
  faq.ts so the FAQ page is actually buildable from git again.

client/src/data/pe-guide.ts
  Verbatim port of lines 23-311 of public/js/peGuide.js — the stable
  reference content:
    • SCALES (12 scales: MRC, DTR, plantar, Beighton, ATR, RR, SpO2,
      Silverman, Westley, Levine murmur, pulse amp, cap refill)
    • SYSTEM_SCALES (per-body-system scale mapping)
    • APTM_LEGEND (5 cardiac auscultation points)
    • INNOCENT_MURMURS (5 benign childhood murmurs)
    • RESP_SOUNDS (7 entries with /audio/respiratory/*.ogg paths)
    • CARDIAC_SOUNDS (6 entries with /audio/cardiac/* paths)
  Counts preserved exactly. Audio files stay under public/audio/ and
  are served unchanged.

What is NOT in this commit — on purpose
  PE_DATA (the ~1000-line age-group × system × component × step
  hierarchy) stays in the vanilla app. The migration checkpoint memory
  explicitly warns about the class of bug where an LLM silently drops
  entries from long clinical arrays. PE_DATA porting needs its own
  session with per-entry counts + visual diff against the vanilla
  source. An amber banner at the top of the React page links to
  /#peGuide (the legacy checklist viewer) so users still reach the
  full exam-step checklist + Generate-Exam-Report flow.

  Also skipped: the big inline APTM_SVG chest diagram. The letter
  legend (A/P/E/T/M) carries the clinical content; the pictorial
  SVG can land later without content risk.

client/src/pages/PeGuide.tsx — viewer
  Grid-of-cards layout: one card per scale, per APTM point, per
  innocent-murmur, and per sound entry. Sound cards use native
  <audio controls> so the browser does the usual play/pause/seek —
  no custom player. data-testid hooks throughout for the spec.

e2e/tests/peguide-react.spec.js — four smoke tests:
  all 12 scales render (this is the count that would fail loudly if
  someone trimmed SCALES later), APTM has all 5 letters, sound
  libraries have the exact respiratory + cardiac keys, and the
  legacy-viewer link is present.

Client tsc -b + vite build clean. Bundle 452.91 kB / 129.35 kB gz.
2026-04-23 23:57:16 +02:00
github-actions[bot]
ab7534b4f7 Release v6.31.0
Some checks failed
Build & release Android APK / Build signed APK (push) Has been cancelled
Build TWA APK / build-apk (push) Has been cancelled
Build & Push Docker Image / Build linux/amd64 (push) Has been cancelled
Build & Push Docker Image / Build linux/arm64 (push) Has been cancelled
Build & Push Docker Image / Merge manifests (push) Has been cancelled
2026-04-23 21:50:04 +00:00
Daniel
8d7bffb07a feat(client): port Learning Hub to React
Minimum-viable port of the user-facing Learning Hub at /app/learning.
Sidebar nav flipped to available in the same commit.

client/src/pages/Learning.tsx
  Three-screen flow: search + category pills drive a feed grid; clicking
  a card opens the viewer; viewer shows body + progress + quiz (if any).

  Feed: one query key per filter — ['learning-feed'], ['learning-category',
  slug], or ['learning-search', q] — so React Query caches each view
  independently and flicking between categories is instant after the first
  load. Search hits /api/learning/search; category filter hits
  /api/learning/category/:slug; default hits /api/learning/feed?limit=30.

  Viewer: body rendered as pre-wrap text intentionally. The vanilla tree
  uses DOMPurify (CDN-loaded) to render HTML bodies; adding that dep to
  the client bundle is a follow-up. Authored content is still clinical
  info, so plain-text preservation is acceptable for this commit — no
  content is lost, just unstyled. Presentations (content_type === 'presentation')
  link to the legacy viewer at /#learning/:slug — Marp slide rendering is
  its own port.

  Quiz: supports single-choice, multi-select, and true/false. Answers
  tracked via { optionId?, optionIds: Set<number> } per question so the
  same state shape drives both radio and checkbox rendering. Submit POSTs
  to /api/learning/submit-quiz; results screen shows per-question verdict
  with correct answer + why-incorrect + general explanation — same fields
  the vanilla showQuizResults renders. Retake wipes the answer map;
  Back-to-Feed returns to the list.

  Progress list reads content.progress[] directly from the content response
  — last 5 attempts, color-coded green/amber at 70%.

shared/types.ts + client/src/shared/types.ts — additive:
  LearningCategory/LearningCategoriesOk, LearningFeedRow/LearningFeedListOk,
  LearningOption/LearningQuestion/LearningProgressEntry/LearningContentFull/
  LearningContentOk, QuizAnswer/QuizResultEntry/QuizSubmitOk. Keys match
  the wire shape server routes return (snake_case for DB columns).

e2e/tests/learning-react.spec.js — three smoke tests:
  shell renders, feed shows items OR empty-state (no crash on empty DB),
  typing into search fires /api/learning/search.

Client tsc -b, server tsc --noEmit, and vite build all pass locally.
Bundle 435.44 kB / 123.81 kB gzipped (+10 kB over Settings complete).
2026-04-23 23:49:56 +02:00
github-actions[bot]
289db2bd6f Release v6.30.0
Some checks failed
Build & release Android APK / Build signed APK (push) Has been cancelled
Build TWA APK / build-apk (push) Has been cancelled
Build & Push Docker Image / Build linux/amd64 (push) Has been cancelled
Build & Push Docker Image / Build linux/arm64 (push) Has been cancelled
Build & Push Docker Image / Merge manifests (push) Has been cancelled
2026-04-23 21:40:42 +00:00
Daniel
719d0cb7f7 feat(client): port Settings — Voice + Content (8 sub-sections)
Third and final commit of the Settings port. Adds the remaining eight
sub-sections so the React page matches vanilla settings.html 1:1.
After this commit Settings is fully ported; Layout already flipped to
available in commit 1, and the page fills out cleanly for local-auth
and SSO users alike.

Voice Preferences (VoicePreferencesCard)
  GET /api/user/preferences + /api/user/preferences/options populate the
  STT model / TTS voice selectors. Save POSTs /api/user/preferences.
  Preview persists the current TTS selection, then fetches /api/text-to-
  speech (binary blob, bypasses the JSON api wrapper), wraps the blob in
  an Audio element and plays it. A one-shot hydrated flag drives the
  first selection sync; after that the fields are local state.

Browser Whisper (BrowserWhisperCard) — UI-only port
  Persists the enabled flag + model choice under the same localStorage
  keys the vanilla BrowserWhisper module reads, so behavior will light
  up automatically when the recording components port. The preload +
  WASM transcription flow stays in vanilla for this commit — noted in
  the page copy so users aren't surprised.

Web Speech Recognition (WebSpeechCard) — UI-only port
  Same localStorage approach. Enabling surfaces a styled ConfirmModal
  with the HIPAA privacy warning before persisting. Enabling Web Speech
  flips Browser Whisper off automatically (mirrors vanilla priority:
  Web Speech > Browser Whisper > server).

My Templates (TemplatesCard)
  Full Memories CRUD for non-correction entries: category select,
  name, content textarea, Add/Update toggle (in-place edit), per-row
  Delete confirm. Hits /api/memories {GET, POST, PUT, DELETE}.

AI Corrections (CorrectionsCard)
  Read-only list filtered to category starting with 'correction_'.
  Per-row expand reveals the parsed ORIGINAL / CORRECTED TO: split
  (same text delimiter the vanilla parseCorrection() uses). Delete is
  wired through /api/memories/:id.

Audio Backups (AudioBackupsCard)
  Lists /api/audio-backups (server-stored, 24h TTL). Play opens the
  decompressed audio stream in a new tab; Delete hits DELETE
  /api/audio-backups/:id. Retry flow stays in vanilla for this commit —
  it re-submits to /api/transcribe and that integration belongs with
  the recording components.

Saved Encounters (SavedEncountersCard)
  Lists /api/encounters/saved with label / type / expires / preview.
  Delete only — Resume requires the encounter pages to receive
  pre-filled state, which ports alongside those pages.

Compliance (ComplianceCard)
  Static info card — plain JSX, no API.

shared/types.ts + client/src/shared/types.ts — additive only:
  UserPreferencesOk, PreferencesOptionsOk, VoiceOption,
  SavedEncounterRow, SavedEncountersListOk, AudioBackupRow,
  AudioBackupsListOk, MemoryRow, MemoriesOk.

e2e/tests/settings-react-voice-content.spec.js — seven smoke tests
covering control presence, templates empty-save validation, and the
Web Speech privacy-confirm modal (with the no-native-dialog guard).

Client tsc -b, server tsc --noEmit, and vite build all pass locally.
Final bundle 425.42 kB / 121.55 kB gzipped (+21 kB over commit 2).
The e2e container still predates /app/*; running these specs against
it needs a rebuild.
2026-04-23 23:40:33 +02:00
github-actions[bot]
25fa628b83 Release v6.29.0
Some checks failed
Build & release Android APK / Build signed APK (push) Has been cancelled
Build TWA APK / build-apk (push) Has been cancelled
Build & Push Docker Image / Build linux/amd64 (push) Has been cancelled
Build & Push Docker Image / Build linux/arm64 (push) Has been cancelled
Build & Push Docker Image / Merge manifests (push) Has been cancelled
2026-04-23 21:35:21 +00:00
Daniel
520a1f8fb1 feat(client): port Settings — Integrations (Nextcloud + Documents)
Second of three commits porting the vanilla settings.html. This one
delivers the two Integrations sub-sections, rendered below the Security
block and shown to every authenticated user (not gated by canLocalAuth —
SSO users also integrate Nextcloud and manage documents).

client/src/pages/Settings.tsx — NextcloudCard
  Form + status line driven by /api/auth/me. Connect POSTs
  /api/nextcloud/connect (nextcloudUrl / username / appPassword), which
  does a PROPFIND probe against the remote, creates the target folder
  via MKCOL, and encrypts the app password at rest. On success we
  invalidate the ['auth-me'] query so the status line flips to
  "Connected to …" without a reload. Disconnect goes through a
  ConfirmModal (not a native confirm) and POSTs /api/nextcloud/disconnect.

  When connected, a second row exposes the "Learning Hub — Default
  Browse Path" input backed by POST /api/user/webdav-path. (That handler
  lives inline in server.ts, not in userPreferences.ts — a quirk of the
  existing codebase that the port preserves.)

client/src/pages/Settings.tsx — DocumentsCard
  React Query feed off /api/documents. When S3 is not configured the
  server returns { s3_configured: false } and we render a static notice
  instead of the upload area (same branch as vanilla documents.js). The
  upload form bypasses the JSON api wrapper to send multipart FormData
  directly via fetch with credentials: 'include' (cookie auth continues
  to work). Downloads hit /api/documents/:id/download to receive a 5-min
  presigned URL which we open in a new tab. Delete goes through the
  shared ConfirmModal — replaces the vanilla showConfirm({ danger, … }).

  Downloading-state spinner is per-row (useMutation.variables === doc.id)
  so other rows stay clickable while one is in flight.

shared/types.ts + client/src/shared/types.ts
  Additive only:
    - AuthUser gains webdav_learning_path — already returned by
      /api/auth/me but missing from the type.
    - New response shapes: NextcloudConnectOk, UserDocument,
      DocumentsListOk, DocumentUploadOk, DocumentDownloadOk.

e2e/tests/settings-react-integrations.spec.js
  Four smoke tests: field presence, empty-form validation error, the
  S3-configured-or-notice branch renders, and a repeat of the
  no-native-dialog guard covering the Integrations interactions.

Client tsc -b, server tsc --noEmit, and vite build all pass locally.
Bundle 404.56 kB / 117.12 kB gzipped (+9 kB over commit 1). The e2e
container still predates /app/*; running these specs needs a rebuild.
2026-04-23 23:35:12 +02:00
github-actions[bot]
42d951b45e Release v6.28.0
Some checks failed
Build & release Android APK / Build signed APK (push) Has been cancelled
Build TWA APK / build-apk (push) Has been cancelled
Build & Push Docker Image / Build linux/amd64 (push) Has been cancelled
Build & Push Docker Image / Build linux/arm64 (push) Has been cancelled
Build & Push Docker Image / Merge manifests (push) Has been cancelled
2026-04-23 21:31:27 +00:00
Daniel
9dce93cf67 feat(client): port Settings — Security (password, 2FA, sessions)
First of three commits porting the vanilla settings.html (13 sub-sections
total) to the React tree. This commit delivers the Settings page shell
plus the three Security sub-sections. Integrations (Nextcloud, Documents)
and Voice + Content land in the two follow-ups.

client/src/pages/Settings.tsx
  Page shell that fetches /api/auth/me and conditionally renders the
  local-auth sections only when user.canLocalAuth !== false. SSO-only
  users see a brief "managed by your identity provider" notice instead —
  matches vanilla behavior, which hides those cards for SSO accounts.

  Change Password: three-field form (current / new / confirm) with
  client-side validation (8+ chars, match). POSTs /api/auth/change-password.
  On success the server destroys all OTHER sessions, so the component
  invalidates the ['sessions'] query so the Active Sessions card below
  refreshes without a full reload. passwordWarning (pwned-password hint)
  surfaces as a follow-up info toast.

  Two-Factor Auth: status line ("Enabled" / "Not enabled") reads
  user.totp_enabled. Enable button POSTs /api/auth/setup-2fa, renders the
  returned QR + secret, accepts the 6-digit code and POSTs /verify-2fa.
  First-enable shows a one-shot BackupCodesDisplay modal with Copy + close.
  Disable flow is inline (password field + Confirm Disable + Cancel, no
  modal) — matches the vanilla UX. Backup-codes remaining count pulls
  from /api/auth/2fa/backup-codes/count; a Regenerate button opens a
  ConfirmModal with requirePassword=true and POSTs /2fa/backup-codes.

  Active Sessions: useQuery on /api/sessions renders one row per session
  with the current one highlighted. Per-row Revoke opens a ConfirmModal;
  Revoke All Other Sessions opens another ConfirmModal. Both DELETE calls
  invalidate ['sessions'] on success.

client/src/components/ConfirmModal.tsx
  Reusable styled confirmation dialog — replaces vanilla showConfirm().
  Supports a danger variant (destructive button styling) and an optional
  password-input variant for confirm-by-password flows. Escape closes,
  backdrop click closes, Enter in the password field submits. Carries
  data-testid hooks (confirm-modal-ok, confirm-modal-cancel) so Playwright
  can drive it without ever hitting window.confirm().

shared/types.ts + client/src/shared/types.ts
  Additive changes only:
    - AuthUser gains optional canLocalAuth, totp_enabled, email_verified,
      nextcloud_url/user/folder, created_at — all fields the server
      already returns from /api/auth/me but the type had never described.
    - SessionRow reshape to match the wire format the server actually
      returns (snake_case ip_address / device_label / created_at /
      last_activity), replacing the speculative camelCase draft. No
      existing consumer of SessionRow existed outside Settings, so the
      rename is a no-op for current code.
    - New types for 2FA + change-password response shapes (Setup2faOk,
      Verify2faOk, BackupCodesCountOk, RegenBackupCodesOk,
      ChangePasswordOk, RevokeAllSessionsOk).

client/src/App.tsx
  Adds <Route path="/settings" element={<Settings />} />.

client/src/components/Layout.tsx
  Flips the Settings nav entry to available: true.

e2e/tests/settings-react-security.spec.js
  Five smoke tests against /app/settings mirroring the coverage of
  settings-faq-dictation.spec.js for the vanilla tree: field/button
  presence for all three sections, password-mismatch inline error,
  revoke-all click surfaces the styled modal (with an explicit
  page.on('dialog') guard to catch any future regression to native
  confirm()).

Note: the e2e container image currently predates the /app/* route
(its /app/public/app/ directory is absent), so running this spec requires
a rebuild — deferred to Daniel's call. Client tsc -b, server tsc --noEmit,
and vite build all pass locally.
2026-04-23 23:31:14 +02:00
github-actions[bot]
7c4ccd8ae6 Release v6.27.0
Some checks failed
Build & release Android APK / Build signed APK (push) Has been cancelled
Build TWA APK / build-apk (push) Has been cancelled
Build & Push Docker Image / Build linux/amd64 (push) Has been cancelled
Build & Push Docker Image / Build linux/arm64 (push) Has been cancelled
Build & Push Docker Image / Merge manifests (push) Has been cancelled
2026-04-23 20:25:52 +00:00
Daniel
efd3e32574 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
github-actions[bot]
019622f5ec Release v6.26.0
Some checks failed
Build & release Android APK / Build signed APK (push) Has been cancelled
Build TWA APK / build-apk (push) Has been cancelled
Build & Push Docker Image / Build linux/amd64 (push) Has been cancelled
Build & Push Docker Image / Build linux/arm64 (push) Has been cancelled
Build & Push Docker Image / Merge manifests (push) Has been cancelled
2026-04-23 20:21:45 +00:00
Daniel
0f28f9212b feat(client): port Hospital Course, Chart Review, Well Visit
Three more Notes tabs:

  /hospital   → HospitalCourse.tsx
    Textarea with blank-line-separated progress notes (one block =
    one note) + H&P + format selector (auto / prose / day-by-day /
    organ-system). Calls /api/generate-hospital-course.

  /chart      → ChartReview.tsx
    Type selector (outpatient / subspecialty / ED) with a dynamic
    array of visit entries — user can add/remove visits. Each
    visit has date / content / labs. Calls /api/generate-chart-review.

  /wellvisit  → WellVisit.tsx
    Vitals, measurements, parent concerns, transcript, screenings,
    immunizations, note style. Minimum-viable Visit Note port —
    the vanilla tab's milestone / SSHADESS / by-visit sub-panes
    become their own sub-routes in a follow-up. Calls /api/well-visit/note.

All three reuse the established pattern: form state → Zod (where a
schema exists in shared/schemas.ts) → useMutation → display pane
with copy-to-clipboard.

Every Notes group item is now marked available in the Layout sidebar.

Build: 377 kB / 110 kB gzipped (+16 kB over previous).
2026-04-23 22:21:34 +02:00
github-actions[bot]
d976a5928d Release v6.25.0
Some checks failed
Build & release Android APK / Build signed APK (push) Has been cancelled
Build TWA APK / build-apk (push) Has been cancelled
Build & Push Docker Image / Build linux/amd64 (push) Has been cancelled
Build & Push Docker Image / Build linux/arm64 (push) Has been cancelled
Build & Push Docker Image / Merge manifests (push) Has been cancelled
2026-04-23 20:19:22 +00:00
Daniel
d9fd4fcd69 feat(client): port Encounter HPI, SOAP, Sick Visit
Three more tabs behind /app/*:

  /encounter  → Encounter.tsx   — POST /api/generate-hpi-encounter
  /soap       → Soap.tsx        — POST /api/generate-soap (full or
                                  subjective-only output type)
  /sickvisit  → SickVisit.tsx   — POST /api/sick-visit/note (chief
                                  complaint is the only required
                                  field; transcript optional)

All three share the same skeleton: demographic triad + textarea +
Zod-validated submit + copy-to-clipboard output pane. Follow-up work
per tab (save/load, refine, audio capture) lands in subsequent
commits — this first pass proves the AI generate path for each.

Layout sidebar updated: Encounter HPI / SOAP Note / Sick Visit now
marked available. Pending stubs remain for Hospital Course, Chart
Review, Well Visit, and all Clinical Tools + Settings.

Build: 361 kB / 109 kB gzipped (+11 kB over previous, as expected
for three small pages using the existing lib/api + shared schemas).

Typecheck + vite build both green on the client side.
2026-04-23 22:19:12 +02:00
github-actions[bot]
f8abf6e171 Release v6.24.0
Some checks failed
Build & release Android APK / Build signed APK (push) Has been cancelled
Build TWA APK / build-apk (push) Has been cancelled
Build & Push Docker Image / Build linux/amd64 (push) Has been cancelled
Build & Push Docker Image / Build linux/arm64 (push) Has been cancelled
Build & Push Docker Image / Merge manifests (push) Has been cancelled
2026-04-23 20:17:05 +00:00
Daniel
552ead0901 feat(client): port FAQ + Dictation to React, add sidebar Layout
Three new pages behind the /app/* React router:

client/src/components/Layout.tsx
  Sidebar + main content shell. NavLink-based nav with a single
  NAV data structure mirroring the vanilla app's sidebar groups
  (Encounters / Notes / Clinical Tools / Account). Items with
  `available: false` render as greyed-out 'pending' stubs so the
  future tab list is visible during migration without breaking
  clicks. Vanilla-app fallback link is pinned at the top so anyone
  needing a feature not yet ported can jump back to /.

client/src/pages/Faq.tsx
  8 sections, 27 questions ported verbatim from
  public/components/faq.html. Collapsible accordion pattern via
  local useState — no Radix dependency yet. Content lives in
  client/src/data/faq.ts (extracted from the HTML via a one-off
  python parse, so re-extraction is reproducible if the vanilla
  FAQ ever grows).

client/src/pages/Dictation.tsx
  Minimum-viable port of Voice Dictation → HPI. Demographics
  (age / gender / setting), transcript textarea, Zod-validated
  submit to POST /api/generate-hpi-dictation, result pane with
  copy-to-clipboard. Not yet ported from the vanilla tab:
  MediaRecorder audio capture + /api/transcribe upload, save/load
  popover, refine + shorten buttons, Nextcloud export. Each of
  those is its own follow-up.

client/src/App.tsx
  All routes now render inside <Layout />. New routes wired:
  /, /extensions, /dictation, /faq. A catch-all Navigate redirects
  any unknown /app/* path back to home.

Build check:
  client: npx tsc -b     → EXIT 0
  client: npx vite build → 350 kB / 108 kB gzipped
Public bundle at public/app/index-BmpHzFRb.js replaces the previous
one; committed so the next prod rebuild ships it atomically.

Nothing on the backend changed. /api/generate-hpi-dictation and
/api/extensions already exist; the React pages just call them.
2026-04-23 22:16:52 +02:00
github-actions[bot]
7e97b04811 Release v6.23.0
Some checks failed
Build & release Android APK / Build signed APK (push) Has been cancelled
Build TWA APK / build-apk (push) Has been cancelled
Build & Push Docker Image / Build linux/amd64 (push) Has been cancelled
Build & Push Docker Image / Build linux/arm64 (push) Has been cancelled
Build & Push Docker Image / Merge manifests (push) Has been cancelled
2026-04-23 20:09:15 +00:00
Daniel
8d244a167a feat: day 7 — first tab ported to React (Extensions) + Express serves /app/*
End-to-end proof that the full React + Vite + Tailwind + TypeScript
pipeline works against the existing Express API:

client/src/pages/Extensions.tsx
  Minimum-viable port of the Extensions tab. Fetches /api/extensions
  via the typed api wrapper; renders an add form backed by Zod
  validation (ExtensionCreateSchema). Uses @tanstack/react-query for
  server state (queryKey: ['extensions'], invalidate on mutate).
  Full CRUD UI (trash / restore / purge / search) is a follow-up —
  this ships just enough to prove the stack works.

client/src/lib/api.ts
  Thin fetch wrapper. Every React page goes through apiFetch<T>(),
  which narrows ApiResponse<T> to the success shape and throws
  ApiError on failure. Central spot for future request/response
  instrumentation, auth-token refresh, etc.

client/src/App.tsx
  Replaced the Vite starter splash screen with a minimal router:
  BrowserRouter basename='/app', routes for / (landing) and
  /extensions. QueryClientProvider wraps the tree so every page can
  use useQuery/useMutation.

client/src/shared/
  Mirrored copy of /shared/types.ts + schemas.ts. Canonical source
  stays at /shared/ (used by backend). A post-migration task is to
  wire proper TypeScript project references so the client can import
  straight from /shared — TS 6's cross-root bundler-mode paths
  resolution isn't pulling it in cleanly. For now, the mirror is
  header-annotated 'do not edit, mirror only'.

client/src/index.css
  Tailwind v4 @theme block declaring the shadcn design tokens as
  first-class CSS custom properties, which exposes the
  bg-background / text-foreground / border-border utility classes
  the page components use. v4 no longer uses @apply for these —
  the theme block is the idiomatic form.

server.ts
  Added:
    app.get('/app/*splat', ...) → sendFile public/app/index.html
  so React Router deep links (e.g. /app/extensions) resolve
  client-side. Express static middleware below continues to serve
  the hashed /app/assets/*.js + .css.

Build output (checked into public/app/ so the next prod docker
rebuild ships the React bundle without requiring a client/npm
install step in the Dockerfile — that's a day-8 refinement):
  index.html    0.46 kB   gzip  0.29 kB
  index.css     9.51 kB   gzip  2.69 kB
  index.js    329.24 kB   gzip 100.98 kB

Typecheck green on both sides:
  server: npx tsc --noEmit   → EXIT 0
  client: npx tsc -b         → EXIT 0
  client: npx vite build     → 150 modules, 219ms

How to see it live (after Daniel rebuilds prod):
  https://<host>/app/            → React landing page
  https://<host>/app/extensions  → React-rendered Extensions list
  https://<host>/                → unchanged vanilla JS app

Nothing destructive. The vanilla JS /extensions tab still works
identically. The React /app/extensions route talks to the same
/api/extensions backend endpoints. Both render from the same
PostgreSQL rows.

This closes out the 7-day migration scaffolding. The rest is a
port-one-tab-at-a-time grind that Codex (or anyone) can pick up
tab-by-tab with the Playwright suite as the safety net.
2026-04-23 22:09:04 +02:00
github-actions[bot]
03cf4be075 Release v6.22.0
Some checks failed
Build & release Android APK / Build signed APK (push) Has been cancelled
Build TWA APK / build-apk (push) Has been cancelled
Build & Push Docker Image / Build linux/amd64 (push) Has been cancelled
Build & Push Docker Image / Build linux/arm64 (push) Has been cancelled
Build & Push Docker Image / Merge manifests (push) Has been cancelled
2026-04-23 19:58:47 +00:00
Daniel
3222dacc9a feat(client): day 6 — scaffold React 19 + Vite + Tailwind v4 + shadcn/ui
New client/ directory, standalone from the backend: Vite 8 + React
19.2 + TypeScript 6. Tailwind v4 via @tailwindcss/vite plugin (no
postcss config needed). shadcn/ui compatibility wired via
components.json + lib/utils.ts. @tanstack/react-query + react-router-dom
installed for server state + routing.

Vite config essentials:
  base: '/app/'     — Express mounts SPA at /app/*, vanilla JS stays at /
  outDir: '../public/app/' — build lands next to existing static assets
  @/*     → ./src/*      (client-local imports)
  @shared/* → ../shared/* (typed wire protocol shared with backend)
  server.proxy '/api' → localhost:3000 for `npm run dev`

tsconfig.app.json — added baseUrl + paths + include ../shared so the
shared/types.ts + schemas.ts are typechecked on the client side too.

src/index.css — Tailwind v4 @import, shadcn HSL design tokens (light +
dark schemes). Replaces the vite starter template's decorative styles.

No backend touched. Express route serving /app/* lands in Day 7.
2026-04-23 21:58:37 +02:00
Daniel
6d30edf88f build(ts): day 5 — Vitest + Zod + Knip tooling
Adds the three high-ROI tools the planning conversation identified:

  vitest 4.x — fast TS-native unit test runner. Runs against pure
    functions (calculators, validators, prompt builders). Playwright
    stays for e2e. package.json `npm test` now runs Vitest;
    `npm run test:node` preserves the old `node --test` runner
    for the 3 legacy tests under test/.

  zod 4.x — runtime request-body validation at API boundaries. The
    new shared/schemas.ts exports a schema per endpoint request
    (LoginRequestSchema, SoapRequestSchema, PeNarrativeRequestSchema,
    ExtensionCreateSchema, etc.). Routes will adopt these one at a
    time post-migration — usage pattern:
      const body = SoapRequestSchema.parse(req.body);
    Invalid input becomes a structured 400 instead of a silent
    undefined-access crash.

  knip 6.x — dead-code / unused-export detector. knip.json scopes
    it to the backend (client/ excluded since it lives in its own
    workspace). Run with `npm run lint:dead`. Catches the class of
    bug that kept public/js/adminMilestones.js dead-loaded for a
    year — a future orphaned file would fail the lint.

  @vitest/coverage-v8 — coverage reporter backed by v8 profiler.

Shipped schemas.test.ts with 8 example cases to prove the toolchain
(`npx vitest run` green).

Not done on day 5 (punted to post-migration): flipping tsconfig to
`strict: true`. That cascade would light up hundreds of implicit-
any errors in handler signatures that would cost more commit bandwidth
than available in this pass. The Day 4 permissive mode is already
catching the big wins (wrong response shapes, orphan refs, undefined
destructures). Post-migration, Codex/vendor model can flip strict flags
one at a time and fix handler-by-handler.
2026-04-23 19:54:30 +02:00
Daniel
6fa0d87da4 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
92a9a20a32 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
faaff64183 refactor(ts): day 3 batch 5 — billing, nextcloud (+ CPT/ICD10 types) 2026-04-23 19:46:25 +02:00
Daniel
4434df89b2 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
87654b6005 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
004e36cd67 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
c1d61f5a72 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
a2fe1b38d1 build(ts): day 2 — shared/types.ts + server.ts rename (no behavior
change)

Creates the wire-protocol contract every route and every client
component will import from. Renames server.js → server.ts as a pure
rename (zero bytes of logic changed) so the entry point becomes the
first file tsc type-checks against the real compilerOptions.

shared/types.ts — what's in it and why

- ApiResponse<T> envelope: (ApiOk<T> & T) | ApiErr. Every route
  returns one of these; client code narrows on `r.success`.
- One typed "Ok" shape per endpoint, keyed by the actual res.json()
  call in the current handler. Walked every src/routes/*.js file
  and transcribed the literal keys: hpi, soap, note, hospitalCourse,
  review, refined, shortened, questions, narrative+summary, etc.
  No inventive renaming — wire stays identical, only the types are
  new.
- Critical mismatches the types now prevent at compile time:
    /api/refine returns `refined` (not `content` — a past bug)
    /api/sick-visit/note (not /api/generate-sick-visit — past bug)
    /api/generate-hospital-course returns `hospitalCourse` (not
      `narrative` — past bug)
  All three were caught and fixed earlier in Playwright; with the
  shared types they become compile errors for any future regression.

server.ts

- Pure rename via `git mv`. Body unchanged.
- Compiled dist/server.js diffs against the original server.js by
  two lines (TypeScript prepends `"use strict"` and the CommonJS
  export marker). No semantic drift.

tsconfig.json tweak

- Include list adds server.ts alongside server.js so tsc doesn't
  silently skip the entry point during the intermediate state
  where `.js` entries might reappear.
- baseUrl removed (deprecated in TS 6); paths now uses './shared/*'.

package.json

- main: dist/server.js (post-compile entry)
- start: node dist/server.js
- prebuild: rm -rf dist (clean emit every time)
- dev: ts-node-dev for fast TS-aware reloads

The Dockerfile is still unchanged. The deployed prod and e2e
containers still run their baked-in server.js from the previous
image — this migration day has no effect on either until the final
rebuild at the end of day 7.

Next: day 3 renames the 29 route files one-by-one, each adding the
ApiResponse<T> type parameter to its res.json() calls.
2026-04-23 19:21:22 +02:00
Daniel
b89d08e886 build(ts): day 1 — bootstrap TypeScript, zero code changes
Installs TypeScript toolchain and configures it to accept every
existing .js file untouched. tsc --noEmit is green; the app runs
identically and nothing is deployed or rebuilt yet.

Config choices:
- extends @tsconfig/node20 (matches the runtime version)
- allowJs: true, checkJs: false — existing files pass through
- strict: false — tightened progressively on day 5, not day 1
- skipLibCheck: true — node_modules .d.ts quality varies, not our
  job on migration day
- paths: { "@shared/*": ["./shared/*"] } — pre-wired for the
  shared/types.ts file landing on day 2

Dependency notes:
- typescript 6.0.3 (current stable, released late 2025)
- @types/express pinned to ^4 because the app uses Express 4.21;
  the default npm install picked @types/express 5 which doesn't
  match runtime shapes for Request/Response
- @tsconfig/node20 for the known-good strict / module / target
  triple for Node 20 LTS
- ts-node-dev for the new `npm run dev` script — transpile-only
  mode, keeps startup fast

package.json script additions:
- build: tsc (compiles to dist/)
- typecheck: tsc --noEmit (CI-friendly)
- dev: ts-node-dev for TS-aware hot reload
- lint:refs: wraps the existing static reference linter

Left alone (no behavior change):
- start: still node server.js
- Dockerfile unchanged (prod still runs vanilla JS)
- All 54 backend .js files untouched

Day 1 scope matches the migration rule: no runtime behavior changes,
nothing deployed. Next: day 2 creates shared/types.ts and flips
server.js to server.ts.

Reference tag: pre-migration-v1 (commit 447eb78).
2026-04-23 19:18:01 +02:00
github-actions[bot]
8e1046ab7a Release v6.21.0
Some checks failed
Build & release Android APK / Build signed APK (push) Has been cancelled
Build TWA APK / build-apk (push) Has been cancelled
Build & Push Docker Image / Build linux/amd64 (push) Has been cancelled
Build & Push Docker Image / Build linux/arm64 (push) Has been cancelled
Build & Push Docker Image / Merge manifests (push) Has been cancelled
2026-04-23 16:58:52 +00:00
Daniel
bc2580b148 test(lint): static reference linter — catches dead-code + orphan refs
You were right that Playwright has been catching the easy bugs while
the high-signal bugs (lightbox stranded after the Bedside reorg, PVC
clinically wrong, prod not rebuilt) all had to be caught by you as
the user. Adding a static linter so the class of bug that produced
the lightbox regression fails CI next time instead of the app.

scripts/lint-references.js walks public/js and validates every
getElementById('X') and querySelector('#X') resolves to an id that
is defined SOMEWHERE in the repo — either a static HTML attribute,
a .id = 'X' assignment, or an id="X" substring inside a JS template
string. It also walks HTML for asset references (data-img-src,
<img src>, <audio src>, <link href>) and verifies each root-absolute
path maps to a real file on disk.

Running it on the current tree surfaced two real problems:

1. shadess.js:917 reached for #wv-note-transcript when collecting
   refine source context. The actual id is #wv-transcript (no -note-
   infix — the transcript element is shared across well-visit sub-
   panels). The bug meant a generated well-visit note's Refine call
   silently missed the original transcript as source context; the AI
   still "worked" but with less signal and no error. Fixed.

2. public/js/adminMilestones.js (221 lines) referenced 17 ids that
   were removed a year+ ago in commit 3173ce6 ("Remove milestone
   admin UI, add CMS content refresh button"). That commit dropped
   the HTML but forgot the JS, which has been dead-loaded on every
   page view since. All its addEventListener calls are guarded with
   optional chaining, so nothing errored — just silent cruft.
   Deleted the file and dropped the <script defer> tag from
   index.html.

scripts/e2e.sh runs the linter as a preflight before the Playwright
container starts; a broken reference now fails the suite before any
test even boots.

Allowlist is kept small and prefix-based for the handful of id families
that are built dynamically by JS (bedside em-* sections, BP chart
elements, etc.). When a new component is added, its ids get picked up
automatically by the repo-wide collect() pass; the allowlist rarely
needs to grow.

Suite: 294 passed / 0 failed.
2026-04-23 18:58:38 +02:00
Daniel
3e3b4866be test(e2e): +44 tests — sick visit, hospital course, auth screen,
session persistence, per-tab model selector

Brings detailed coverage to sections that were previously only smoke-
tested:

- sickvisit-workflow.spec.js: generate → mocked note, refine bar
  round-trip, load popover, New button clears demographics + transcript.
- hospitalcourse-workflow.spec.js: fill H&P → generate → mocked
  narrative renders via /api/generate-hospital-course, refine updates
  text, load popover open/close.
- auth-screen.spec.js: unauthenticated landing visible, login form
  structure, forgot-password swap + return, register link is
  intentionally display:none on this instance (pinned), register form
  DOM still wired correctly if the link is manually unhidden, reg-
  password has minlength=8 + type=password.
- session-persistence.spec.js: clear cookie simulates logout (UI login
  is gated by Turnstile which can't be completed in the e2e container);
  re-login via loginAs restores the last tab + sub-pill via localStorage.
- model-selector.spec.js: tab-model-select dropdowns render with >0
  options across 7 tabs.

Fixture corrections:
- /api/sick-visit/note and /api/well-visit/note were not being mocked
  at all — the wrong /api/generate-sick-visit pattern was intercepting
  nothing, so tests fell through to the real AI backend. Both now have
  correct patterns and response shapes (sick-visit returns `note`,
  well-visit note returns `note`).
- /api/generate-hospital-course response key updated from `narrative`
  to `hospitalCourse` to match what the frontend actually reads.

wellvisit-workflow: the Visit Note test now asserts a concrete
waitForResponse on /api/well-visit/note + text render, instead of the
previous "hit either endpoint" fallback.

Suite: 294 passed / 0 failed in 5m30s.
2026-04-23 18:58:38 +02:00
Daniel
69dedbb635 fix(pe-guide): remove PVC entry from cardiac sounds library
PVCs are an ECG / rhythm finding, not a routine auscultation sample —
what you actually hear on the stethoscope is an irregular rhythm with a
compensatory pause, which depends on the underlying rate and is not
teachable from a canned audio clip. The card was also backed by a
synthesized sound, not a real recording. Removing both the card and
the pvc.ogg asset.
2026-04-23 18:58:38 +02:00
Daniel
5c1d1619c7 fix(nav): move image lightbox to index.html so NRP + seizure pathways
open from the Bedside tab

The #img-lightbox overlay markup was sitting at the bottom of
calculators.html. Before the reorg it was fine — the calculators tab
was always the only home for bedside, so by the time a user clicked
the seizure or NRP pathway button the lightbox HTML was guaranteed to
be in the DOM. After promoting Bedside to its own tab, a user can
open Bedside -> Seizures without having visited Calculators first;
the lightbox JS's getElementById('img-lightbox') then returns null
and the click silently no-ops.

Moved the overlay markup to the bottom of index.html so it exists
from page load regardless of which tab has been lazy-loaded. The e2e
harness gets a duplicate copy so the existing lightbox smoke test
keeps working.

Added a regression test for NRP (bedside-smoke.spec.js:141) to catch
any future breakage — the prior seizure-only test didn't exercise the
second pathway button and so the neonatal/NRP path had never been
clicked in CI.

Suite: 252 passed / 0 failed.
2026-04-23 18:58:38 +02:00
Daniel
f4140d45c4 feat(ui-state): persist sub-pill selections across reload + sign-out
Tab-level choice (ped_last_tab) already survived sign-out/in via
localStorage, but sub-pill and sub-tab selections inside a loaded tab
lived only in memory — they reset to defaults after a reload or
browser restart. Now the following are persisted under the ped_ui/
namespace:

  - Calculators nav pill (BP / BMI / GCS / …)
  - Bedside sub-pill (neonatal / airway / …)
  - Well Visit sub-tab (byvisit / milestones / shadess / note)
  - Physical Exam Guide age group + system

Implementation:
- Added public/js/ui-state.js — a ~30-line window.UIState wrapper
  around localStorage with a ped_ui/ prefix and try/catch around both
  read and write (Safari private mode + quota errors silently no-op).
- Each tab's click handler now also calls UIState.set; each tab's
  init path calls UIState.get and replays the saved value through
  the same function a click would call — so there is exactly one
  code path for "show this selection", whether it came from the user
  or from a restore. For Bedside, the restore additionally listens
  for tabChanged so the lazy-loaded HTML is guaranteed to exist by
  the time we re-activate the pill.

Tests:
- e2e/tests/ui-state-persistence.spec.js — 5 specs × 2 viewports =
  10 tests. Each clicks the feature, reloads the page, and asserts
  the same pill / subtab / dropdown value is still active. Catches
  any future regression in the persistence wiring.
- e2e/tests/soap-hospital-workflow.spec.js — fills SOAP transcript,
  generates via mocked AI, clears, opens/closes load popovers; also
  smoke-tests the Hospital Course save-bar.

Suite: 250 passed / 0 failed (+ 20 over the last run).
2026-04-23 18:58:38 +02:00
Daniel
1431498fd6 feat(nav): promote Bedside to its own tab; reorganise sidebar sections
Bedside is now a top-level tab instead of a sub-pill inside Calculators —
it's the highest-traffic emergency reference in the app and deserves a
one-click entry. Same DOM structure and JS modules; only the container
moved.

Sidebar reorg:
- Notes: Hospital Course, Chart Review, SOAP Note, Well Visit, Sick Visit
  (Well Visit + Sick Visit relocated from the old "Pediatric" group —
  they're clinical note workflows, not pure reference tools).
- Section rename: "Pediatric" → "Clinical Tools". The section now holds
  Vaccine Schedule, Catch-Up Schedule, Physical Exam Guide, Bedside,
  Calculators, Pagers & Extensions, Learning Hub, Content Manager — mix
  of reference tables + active calculators + utilities, none strictly
  pediatric. "Clinical Tools" reads naturally for the combined set.

Layout changes:
- calculators.html: dropped 480 lines of bedside panel + the Bedside
  nav-pill. The shared age→weight estimator moved with it.
- bedside.html: new component file, contains the full bedside card +
  the age→weight estimator prepended.
- index.html: added bedside-tab section, sidebar restructured.
- e2e-harness.html: renders calculators + bedside side-by-side (not the
  old calc-tab→bedside-pill dance) so the bedside smoke suite still
  works without auth. e2e-bootstrap fetches both with a cache-buster.
- bedside-smoke.spec.js: removed the now-obsolete calc-nav-pill click
  from each test.

Tab persistence is unchanged — ped_last_tab already survives sign-out,
and the lazy-load cache keeps sub-pill state across navigation within a
session. Persistence across browser restart for sub-pills is a separate
follow-up.
2026-04-23 18:58:38 +02:00
Daniel
8e1ab2fea3 test(e2e): +120 tests across 8 new specs; baseline fixes
8 new spec files covering sections previously only smoke-tested:
- ai-endpoints-contract.spec.js  — hits 8 real AI endpoints via request
  context and fails if the response leaks TypeError / ReferenceError /
  'Cannot read properties of undefined' / 'is not defined' / 'is not a
  function'. This is the class of bug that shipped the PE-narrative
  regression to prod because every page-level mock prevented the real
  handler from running.
- encounter-workflow.spec.js — generate HPI, refine, clear transcript.
- encounter-save-load.spec.js — save draft, load popover, repopulate.
- wellvisit-workflow.spec.js — byvisit, milestones, SSHADESS (12+
  reveal), visit note.
- vaxschedule-content.spec.js — schedule + catch-up panels populate
  beyond "Loading".
- chart-review-workflow.spec.js — generate + load popover.
- learning-tab.spec.js — search filter, category pills, feed.
- settings-faq-dictation.spec.js — voice/password/2FA/Nextcloud
  sections, FAQ expand/collapse, dictation generate flow.

Baseline fixes:
- Added CORS_ORIGINS + API_RATE_LIMIT_MAX env overrides so the e2e
  container accepts the browser's Origin header and can absorb the
  full suite's API traffic without tripping the 200/min guard.
- Server's /api/ rate limit is now configurable via
  API_RATE_LIMIT_MAX (default stays 200).
- extensions-crud: replaced native page.on('dialog') listeners with
  #confirm-modal-ok clicks (we moved off native confirm()).
- pe-guide-smoke + extensions-crud: mobile viewport opens the hamburger
  before clicking sidebar tabs.
- fixtures.js: /api/refine mock uses 'refined' (real API shape), not
  'content'. /api/chart-review replaced with /api/generate-chart-review.

Suite: 230 passed / 0 failed in 4m36s.
2026-04-23 18:58:38 +02:00
Daniel
c0cb66ae3e feat(pe-guide): real lung recordings for normal/rhonchi/pleural-rub;
drop grunting + dead synth module

Three lung sounds that were Web-Audio syntheses now play real clinical
recordings sourced from the HLS-CMDS manikin dataset (MIT license):

- normal-vesicular.ogg
- rhonchi.ogg
- pleural-rub.ogg

Dropped expiratory grunting from the library entirely — no
openly-licensed clinical recording located across Wikimedia, Freesound
CC0, SPRSound, Pixabay, Internet Archive, or Littmann/EasyAuscultation
(all proprietary). Card is honest by omission rather than hiding a
synth behind a Play-only UI.

All seven remaining entries now use the same native <audio controls>
player (pause, seek, volume). The synth fallback branch in
renderSoundCard, the stopAllExcept synth reset loop, the script tag,
and the entire public/js/respiratorySounds.js (332 lines of Web Audio)
are removed since nothing references them anymore.
2026-04-23 18:58:38 +02:00