Commit graph

400 commits

Author SHA1 Message Date
Daniel
9811e39cfe feat(client): port Admin panel — all 10 sub-tabs now run in React
Replaces the legacy-link shell at /app/admin with a proper
multi-sub-tab admin panel. Covers every module the vanilla admin.js
exposed: Users, Site settings, Announcement banner, AI models,
TTS / STT providers, SMTP, Email templates, AI prompts, Audit logs.

Priority 1 per Daniel's note: **Users**. Full CRUD flow ported —
list (with live filter), verify, disable/enable, set role
(user/moderator/admin), delete (with confirm modal), admin-side
password reset (inline modal). Self-protection rules preserved:
can't disable/delete yourself, can't demote your own admin role.

client/src/pages/Admin.tsx (rewritten)
  Sub-tab shell. Role check via useQuery(['auth-me']) reusing the
  Layout cache. 10 pills drive which panel renders. Access-denied
  card for non-admins (data-testid='admin-access-denied' unchanged).

client/src/pages/AdminPanels.tsx (new) — batch 1
  • AdminUsersTab — useQuery ['admin-users'] + 6 mutations
    (verify / disable / enable / set role / delete / reset password).
    Color-coded rows (disabled users opacity-60), inline role select,
    inline search filter over email+name.
  • AdminSettingsTab — GET /api/admin/settings → stats (totalUsers /
    totalApiCalls / todayApiCalls) + registration toggle.
  • AdminAnnouncementTab — reads announcement.{enabled,type,text} via
    /api/admin/config/announcement, saves via 3 parallel PUT
    /api/admin/config/<key> calls. Info/Warning/Critical severity
    select; text rendered in the top-of-page banner.

client/src/pages/AdminPanels2.tsx (new) — batch 2
  • AdminSmtpTab — host/port/user/pass/from/secure form + source
    badge (env / database / none). PUT /api/admin/config/smtp,
    DELETE /api/admin/config/smtp (with ConfirmModal). Inline test
    email sender (recipient + template) calling
    POST /api/admin/config/test-email.
  • AdminEmailTab — template selector (verify / reset /
    password-changed), subject + HTML body textarea. Pulls values
    from /api/admin/config, saves via 2 parallel PUT calls.
  • AdminPromptsTab — GET /api/admin/config/prompts populates the
    selector; textarea edits the active prompt; save via
    PUT /api/admin/config/prompt.<key>; reset-to-default via
    POST /api/admin/config/prompts/<key>/reset with ConfirmModal.
  • AdminModelsTab — GET /api/admin/config/models renders the
    provider-scoped model table with per-row Enabled checkbox +
    Default radio. Mutations hit /api/admin/config/models/toggle
    and /default. LiteLLM + model-discovery flows flagged as
    legacy-viewer follow-up.
  • AdminTtsTab / AdminSttTab — show active provider + voice/model
    selector, save default via PUT /api/admin/config/tts.default_voice
    and /stt.default_model respectively.
  • AdminLogsTab — GET /api/admin/logs/all?category=&limit= with
    sticky-header scrollable table. Category filter
    (auth / admin / clinical / export / integration / documents) and
    limit selector (50-500). Renders time / user / category /
    action / detail / IP per row.

shared/types.ts + client/src/shared/types.ts — additive only:
  AdminUser, AdminUsersOk, AdminUserOk, AdminSettingsOk,
  AdminLogEntry, AdminLogsOk, AdminConfigRow, AdminConfigOk,
  AdminAnnouncementOk, AdminPromptRow, AdminPromptsOk,
  AdminSmtpStatusOk, AdminModelRow, AdminModelsOk,
  AdminVoiceProviderOk.

With this commit the React sidebar covers the full legacy nav.
Access-control is preserved (Admin is still role-gated and the nav
link hides for non-admins). Every existing backend endpoint is
reused as-is — no server changes.

Backend tsc + client tsc + vite build + 136/136 vitest all green.
2026-04-24 02:27:50 +02:00
github-actions[bot]
1037dc68b8 Release v6.44.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-24 00:15:27 +00:00
Daniel
21facd4e1b feat(client): port BP Percentile — all 10 Calculator pills now run in React
Closes the Calculators migration. AAP 2017 BP percentile (Rosner
quantile splines) was the biggest table-driven calculator in the
codebase: 6 height-LMS arrays × 218 entries + 4 spline coefficient
matrices × 99 rows × 13 terms = ~3,500 numeric constants. Every one
ported verbatim. 14 parity tests prove the TS port returns identical
percentile and classification outputs to the vanilla calculators.js.

shared/clinical/bp.ts — 571 lines
  Generated from public/js/calculators.js via awk-extracted lines
  89-94 (LMS) and 97-503 (coefficients), wrapped in TS export
  declarations. No rewriting, no reformatting, no reordering — the
  data bytes are identical to the vanilla source.
  Math (calcHeightPercentile, computeBpPercentile,
  classifyBpFromPercentiles) ported verbatim from calculators.js:
  505-608. Exports a top-level computeBp() helper returning
  { sysPercentile, diaPercentile, heightPercentile, sysClass,
  diaClass, classification }.

scripts/capture-calc-vectors.js — BP cases added
  Uses new Function() to evaluate the raw LMS + coefficient blocks
  from calculators.js directly, then runs the vanilla math against
  14 carefully chosen test cases:
    • Typical pediatric ages (3, 5, 8, 10, 12 years, both sexes)
    • Adult-threshold cross-over (age 13 — uses absolute mmHg cutoffs)
    • Stage 1 / Stage 2 hypertension boundaries
    • Edge-of-domain (age 1, age 17)
    • Tall / short height-percentile outliers
  Fixture regenerated to 14 Bhutani + 58 AAP + 13 Fenton + 8 neonatal
  + 12 BMI + 14 BP = 117 total vectors.

shared/clinical/bp.test.ts — exact-match parity
  sysPercentile / diaPercentile are integer selections from 99
  candidate predicted values, so tests use .toBe() for exact match.
  heightPercentile uses toBeCloseTo(6) (double-precision float).
  Classification strings must match exactly. 14/14 pass.

client/src/pages/CalculatorPanels.tsx — BpPanel added
  Age/sex/height/SBP/DBP inputs, validation (age 1-17, height 50-200
  cm), color-coded overall classification + per-measurement
  percentile and tier (Normal / Elevated / Stage 1 / Stage 2) in a
  4-column result grid with the height percentile for context.

client/src/pages/Calculators.tsx
  Dispatch wires bp → BpPanel. PILLS['bp'].ported = true.
  LegacyPanel is now dead code — every pill has a real implementation.

Final test suite: 5 files, 136 tests green
  (19 calculators + 70 bilirubin + 21 fenton/neonatal + 12 BMI + 14 BP)
2026-04-24 02:15:17 +02:00
github-actions[bot]
8daa5c765d Release v6.43.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-24 00:08:15 +00:00
Daniel
7371cbf13a feat(client): port BMI + Vitals + Resus Meds + Equipment calculators
Nine of the ten Calculator pills now run in React. Only BP Percentile
(Rosner quantile splines, ~3,000 hand-transcribed coefficients across
6 LMS arrays and 4 spline matrices) remains legacy-linked; that port
deserves its own dedicated session with extra care.

shared/clinical/bmi.ts — CDC 2000 BMI-for-age
  • bmiLMS table ported byte-for-byte from calculators.js:739
    (74 LMS triples = 37 age points × 2 sexes, 24-240 months).
  • normalCDF (Abramowitz & Stegun), calcBmiPercentile, classifyBMI
    (including the %-of-95th severe-obesity split), and a top-level
    computeBmi helper — all verbatim translations of the vanilla math.
  • classification labels preserved exactly so existing e2e screenshots
    or reporting continue to read the same text ('Class 2 Severe
    Obesity', 'Healthy Weight', etc.).

shared/clinical/bmi.test.ts — 12 captured vectors covering:
  both sexes, edges (2y + 20y), interpolated-between-keys (13m),
  each classification cliff (underweight / healthy / overweight /
  obese / severe class 2 / severe class 3), and the age-clamping
  branches (<24 mo and >240 mo). All 12 pass at 10-decimal precision
  (percentile to 6 places since the vanilla rounds to 2).

scripts/capture-calc-vectors.js — BMI section added
  Same pattern as bilirubin / Fenton: the vanilla data + math are
  inlined verbatim, the script runs 12 chosen cases, and writes to
  e2e/fixtures/calc-vectors.json. Re-run after any upstream change.

client/src/pages/CalculatorPanels.tsx (new)
  • BmiPanel — age/sex/weight/height inputs, calls computeBmi,
    renders color-coded classification badge with BMI, percentile,
    Z, and % of 95th when percentile ≥85.
  • VitalsPanel — 8-band age selector (premie → >12 yr).
    VITALS_DATA ported verbatim from calculators.js:1703-1831 with
    every HR/RR/SBP/DBP/temp/weight/SpO₂ range and clinical notes
    preserved entry-for-entry.
  • ResusPanel — weight input drives all 14 drugs (Adenosine,
    Amiodarone, Atropine, CaCl, Ca-gluconate, Dextrose, Epi,
    Hydrocortisone, Insulin, Lidocaine, Mg, Naloxone, NaHCO₃) with
    calc() closures ported verbatim from RESUS_MEDS lines 1873-2050.
    Category colors + labels preserved.
  • EquipmentPanel — 9-band age selector (premie → 16+ yr).
    EQUIP_DATA ported verbatim from calculators.js:2173-2228 with
    12 equipment sizes per band (BVM, NPA, OPA, blade, ETT, LMA,
    Glidescope, IV, CVL, NGT, chest tube, Foley).

client/src/pages/Calculators.tsx
  Dispatch wires bmi → BmiPanel, vitals → VitalsPanel, resus →
  ResusPanel, equipment → EquipmentPanel. PILLS flags all four as
  ported: true. Only bp remains on LegacyPanel.

Backend tsc + client tsc + vite build + 122/122 vitest (19 calc +
70 bili + 21 fenton/neonatal + 12 bmi) all green.
2026-04-24 02:08:05 +02:00
github-actions[bot]
57d6263daf Release v6.42.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-24 00:01:01 +00:00
Daniel
941b73f9e5 feat(client): port final 5 Bedside panels — all 15 now run in React
Finishes Bedside parity. Neonatal, Respiratory, Ventilation, Sepsis,
and Burns replace their LegacyPanel fallbacks. All 15 vanilla
Bedside sub-modules are now real React components.

shared/clinical/fenton.ts — second, higher-accuracy Fenton table
  fentonLmsPeditools: 21 GA weeks × 2 sexes (22-42 in 1-week steps)
  ported verbatim from public/js/bedside/neonatal.js:20-37. This is
  the peditools-derived table that superseded the older hand-rounded
  version (still used by the Growth Charts calculator). Adds
  calcZNeonatal (|L|<0.001 threshold) + zToPercentileNeonatal
  (Abramowitz & Stegun erf form) so neonatal output matches the
  vanilla Bedside numbers to 3 decimal places.
  neonatalAssess() returns { gaDecimal, gaClass, bwClass,
  weightClass, expectedWeight, z, percentile, L, M, S }.

scripts/capture-calc-vectors.js + e2e/fixtures/calc-vectors.json
  Adds 8 neonatal vectors (including the 40w5d male 3070g validated
  case from the vanilla file's own comment: z = -1.42).

shared/clinical/fenton.test.ts — now covers neonatal too (110 total
vitest cases pass: 19 calculators, 21 fenton, 70 bilirubin).

client/src/pages/BedsidePanels2.tsx — five new panels
  • NeonatalPanel — GA+weight+sex inputs drive the Fenton assessment
    (gestational-age class, weight-for-GA class, birth-weight
    category, Z-score, percentile, expected M). Full NRP pathway
    cards (birth→HR<100→HR<60 escalation), NRP drug table scaled
    by weight (epi IV/IO/ETT, NS bolus, D10), and 5-element Apgar
    scorer with reassuring/moderately-depressed/severely-depressed
    guidance.
  • RespiratoryPanel — four sub-modes: Asthma (mild/moderate/
    severe with full drug tables + "when to intubate" / ABG /
    heliox clinical decision boxes), PRAM scorer (0-12),
    Westley croup scorer (0-17 with severity-tiered treatment),
    Bronchiolitis admission decision tree (age / SpO₂ / hydration
    / distress) with AAP "NOT recommended" list.
  • VentilationPanel — target SpO₂ table by population, 6-step
    escalation ladder (NC → FM → NRB → HFNC → NIV → intubation)
    with live-scaled HFNC flow (1-2 L/kg/min), BVM how-to,
    mechanical vent starting settings (TV, rate by age, PEEP,
    FiO₂, I:E), gas-exchange adjustment table, and the
    oxygenation-vs-ventilation mental model.
  • SepsisPanel — Phoenix Sepsis Criteria (JAMA 2024), red-flag
    list, age-banded workup + empirical therapy (neonate /
    infant / child abx drugs keyed to weight), SSC 2020 first-hour
    bundle (0-5 min recognize → >60 min refractory-shock
    vasoactive), and resuscitation targets.
  • BurnsPanel — full 19-region Lund-Browder age-adjusted table
    (ported verbatim), with per-region % input + live TBSA
    computation + override. Parkland formula (4 × kg × TBSA,
    8h/16h split + per-hr rates), 4-2-1 maintenance, UOP targets,
    pearl list (palm rule, no first-degree, analgesia, tetanus),
    and ABA burn-center referral criteria.

client/src/pages/BedsidePanels.tsx — dispatch extended to all 15
pills. REAL_BEDSIDE_PANELS now contains every Bedside pill id, so
the legacy-link fallback is dead code that can be pruned later.

Client tsc + vite build + 110/110 vitest tests all green.
2026-04-24 02:00:50 +02:00
github-actions[bot]
f5c9202fe3 Release v6.41.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:49:54 +00:00
Daniel
9aa3961459 feat(client): port Bilirubin + Fenton calculators with captured vectors
Ships the AAP 2022 phototherapy/exchange nomograms, the Bhutani 1999
risk zones, and the Fenton 2013 preterm weight-for-GA chart as real
React calculators. These were the high-risk "table-driven" ports the
migration checkpoint flagged as needing captured vectors before
landing — the whole vector-capture workflow now exists and can be
reused for BP / BMI / growth-beyond-Fenton next.

Workflow, so future calculator ports have a template:

scripts/capture-calc-vectors.js
  Standalone Node script. Data tables + math inlined VERBATIM from
  public/js/calculators.js (no rewriting, no reformatting). Picks 83
  carefully chosen test cases — edge-of-domain, table-key-exact,
  interpolated-between-keys, and clinical-decision-boundary values —
  and emits e2e/fixtures/calc-vectors.json with
  { inputs, output } pairs produced by the authoritative math.

e2e/fixtures/calc-vectors.json
  12 Bhutani cases, 58 AAP 2022 cases, 13 Fenton cases. Re-run
  capture-calc-vectors.js whenever the vanilla file changes.

shared/clinical/bilirubin.ts
  • 17 HourTable constants ported byte-for-byte from calculators.js
    lines 1489-1512: photo 35w/36w/37w/38w/39w/40+ (low risk) +
    35w/36w/37w/38+ (medium risk), same 8 for exchange.
  • Bhutani zones (p95/p75/p40) from lines 1644-1651.
  • interpolateThreshold helper (lines 1514-1525).
  • classifyBhutani + classifyAapBili functions mirror the vanilla
    click-handler logic.

shared/clinical/fenton.ts
  • 15-week × 2-sex LMS table from lines 1168-1183 preserved entry-
    for-entry.
  • interpolateLMS + calcZ + zToPercentile (Abramowitz & Stegun
    normal CDF, lines 2299-2326) ported byte-for-byte.
  • fentonWeightForAge returns { L, M, S, z, percentile };
    classifySizeForAge labels SGA/<10 / AGA / LGA/>90.

shared/clinical/bilirubin.test.ts + fenton.test.ts
  Vitest suites that import e2e/fixtures/calc-vectors.json and assert
  classifyBhutani / classifyAapBili / fentonWeightForAge match
  every captured vector to 10 decimal places for threshold values
  and 6 decimals for the Fenton M (grams, so 6 is >= 1e-3 g).
  All 102 tests pass locally (19 prior + 70 bili + 13 Fenton).

client/src/pages/Calculators.tsx
  • BiliPanel with AAP 2022 / Bhutani mode switch, GA + risk-factor
    dropdowns, color-coded status / zone badges, both threshold
    pairs surfaced in the result grid.
  • GrowthPanel runs Fenton with sex + GA + weight inputs, emits
    Z-score, percentile, L/M/S reference, and SGA/AGA/LGA label.
  • PILLS flags bili + growth as ported: true; ActivePanel routes to
    the new components. BP / BMI / vitals / resus / equipment
    remain legacy-linked until their own vectors land.

e2e/tests/calculators-react.spec.js
  Three new parity tests covering above-phototherapy/above-exchange
  transitions (38w 72h TSB 20 → phototherapy; TSB 26 → exchange),
  Bhutani high-risk classification at 36h TSB 13, and Fenton 32w
  male 1795g landing exactly at 50th percentile AGA.

Backend tsc + client tsc + vite build + vitest (102/102) all green.
Bundle 619 kB / 178 kB gz (+10 kB for the bili tables).
2026-04-24 01:49:45 +02:00
github-actions[bot]
1274391224 Release v6.40.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:35:16 +00:00
Daniel
591257a0c7 feat(client): port Sedation + Toxicology Bedside panels
Ten of fifteen Bedside pills now have real React implementations.
Remaining five (neonatal, respiratory, ventilation, sepsis, burns)
stay on LegacyPanel — they each have more complex state (Fenton
LMS, Phoenix criteria, Lund-Browder age-adjusted region tables,
bronchiolitis / asthma severity scoring) best tackled in dedicated
sessions where the clinical data can be verified carefully.

client/src/pages/BedsidePanels.tsx
  • SedationPanel — full procedural-sedation drug list (ketamine IV/IM,
    midazolam IV/IN/PO, propofol, fentanyl IV/IN, nitrous oxide,
    dexmedetomidine IN) plus reversal agents (naloxone, flumazenil).
    Dose cells mirror the vanilla sedDoseCell helper: range-low/high
    with per-kg footer, or single per-kg + max. Pre-sedation checklist
    and citation preserved.
  • ToxicologyPanel — topic selector with 11 panels (general approach,
    acetaminophen, opioids, iron, TCA, β-blocker/CCB, benzo,
    organophosphate, salicylate, toxic alcohols, dialyzable drugs).
    Every drug dose and clinical pearl carried over byte-for-byte.
    Weight-dependent drugs (NAC weight-based loading, naloxone
    range, magnesium for TCA torsades) compute against the entered
    weight; topic panels work with or without a weight value.

Client tsc -b + vite build clean.
2026-04-24 01:35:07 +02:00
github-actions[bot]
d663526511 Release v6.39.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:31:46 +00:00
Daniel
ec418da0d6 feat(client): port Airway, Agitation, Antiemetics, Antimicrobials, Trauma panels
Second batch of Bedside sub-module ports. Eight of the fifteen pills
now run real React (anaphylaxis + cardiac + seizure from the previous
commit, plus these five). Remaining seven — neonatal, respiratory,
ventilation, sepsis, sedation, burns, toxicology — still fall through
to LegacyPanel.

client/src/pages/BedsidePanels.tsx
  • AirwayPanel — weight + age inputs drive the full RSI sheet.
    Pre-medication, induction, paralytics, and maintenance-sedation
    tables with per-kg dosing + max caps preserved from airway.js.
    Equipment sizing (ETT uncuffed/cuffed, depth, blade, LMA, Fr
    suction, NG, chest tube) follows the same age-/weight-keyed
    formulas. Ventilator starting settings carry Vt = 6-8 mL/kg
    computed from the current weight.
  • AgitationPanel — step 1 / 2 / 3 pathway with weight-conditional
    doses for olanzapine (<30 kg vs ≥30 kg) and haloperidol
    (<40 kg vs ≥40 kg). Droperidol keeps the range-prefix + computed
    dose. Drug list matches AGIT_FALLBACK.
  • AntiemeticsPanel — full drug list with the ondansetron weight-
    band display alongside the per-kg fallback. Promethazine <2 yr
    contraindication text preserved.
  • AntimicrobialsPanel — neonate / infant / child × sepsis /
    meningitis / pna / uti / skin / ent / neutropenic / intra-abd /
    bone regimen map ported verbatim from REG. Age + infection
    selectors pick the right cell.
  • TraumaPanel — ABCDE primary survey cards with the 20 mL/kg
    bolus auto-computed from the entered weight. MTP (1:1:1, TXA,
    calcium), NEXUS c-spine, pediatric shock signs, and AMPLE
    secondary-survey content all preserved.

renderBedsideRealPanel + REAL_BEDSIDE_PANELS extended to cover the
five new panels.

Client tsc -b + vite build clean (warnings on chunk size are expected
until we code-split; not blocking).
2026-04-24 01:31:38 +02:00
github-actions[bot]
7acfefac1c Release v6.38.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:27:37 +00:00
Daniel
95a5a5e40e feat(client): port Bedside Anaphylaxis + Cardiac + Seizure panels
Replaces the legacy-viewer fallback for three of the fifteen Bedside
sub-modules with real React implementations. Weight-based dosing now
runs locally for the most time-critical emergencies — the other 12
modules (neonatal, airway, respiratory, ventilation, sepsis, sedation,
agitation, antiemetics, antimicrobials, burns, toxicology, trauma)
still fall through to LegacyPanel.

shared/clinical/calculators.ts
  New formatDose(weightKg, perKg, max?, unit = 'mg') helper that
  mirrors the vanilla S.dStr exactly — same rounding, same cap rule,
  same label format. Returns { value, unit, capped, perKg, max,
  label } so consumers can render rich UI without re-parsing a HTML
  string. Pure function; unit tests in the existing vitest suite
  still pass.

client/src/pages/BedsidePanels.tsx (new)
  Three panels keyed to the Bedside pill id:
    • AnaphylaxisPanel — STEP 1 epi IM callout + full 9-row dosing
      table (fluids, diphenhydramine, ranitidine, dex, methylpred,
      refractory epi gtt, glucagon). Drug per-kg + max values ported
      verbatim from ANAPH_FALLBACK in the vanilla module.
    • CardiacPanel — PALS dosing with 6 sub-views (general,
      asystole/PEA, bradycardia, SVT, VF/pulseless VT, stable VT).
      Every drug row carries the same mg/kg + max values as the
      vanilla cardiac.js (epi, amio, lido, atropine, adenosine ×2,
      bicarb, CaCl/CaGluc, Mg, D10, defib energies). Concentration
      disclaimer + AHA citation preserved.
    • SeizurePanel — full status-epilepticus timeline (0 min →
      40 min refractory) with the vanilla SEIZURE_FALLBACK drug
      table (loraz / midaz / diaz for benzo; levetiracetam /
      fosphenytoin / valproate / phenobarb for 2nd-line; midaz
      pentobarb propofol ketamine infusions for refractory). Key
      points + citation preserved.

  REAL_BEDSIDE_PANELS set + renderBedsideRealPanel(pillId) export so
  Bedside.tsx can dispatch by id without import sprawl.

client/src/pages/Bedside.tsx
  When the active pill is in REAL_BEDSIDE_PANELS, render the real
  panel; otherwise fall back to LegacyPanel. No other changes — the
  sub-nav, age→weight estimator, and 12 legacy-linked modules stay
  exactly as they were.

e2e/tests/bedside-react.spec.js
  Three new parity tests (in addition to the existing shell checks):
    • Anaphylaxis: 20 kg → 0.2 mg epi IM; 70 kg → 0.5 mg (capped).
    • Cardiac: 25 kg → 0.25 mg epi IV in general view and asystole.
    • Seizure: 10 kg → D10W 20-50 mL, Lorazepam 1 mg.

Client tsc -b + vite build clean. Bundle 609 kB / 173 kB gz (+28 kB
for three full panels; vite's 500 kB chunk warning noted for later
code-splitting, not blocking).
2026-04-24 01:27:29 +02:00
github-actions[bot]
fb51ba9cfe Release v6.37.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:21:54 +00:00
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