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).
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.
Safety net for upcoming refactors:
- 26 Playwright smoke tests via @playwright/test 1.50.0 in an official
Playwright container (no host Node needed). Covers every Bedside sub-pill,
the age→weight estimator, dose calculators (seizure/sepsis/anaphylaxis/
burns/airway), and interactive widgets (lightbox, vent SVG).
- `npm run e2e` wrapper runs tests inside mcr.microsoft.com/playwright:v1.50.0-noble
on the ped-ai_default Docker network so no host port mapping is needed.
- public/e2e-harness.html + public/js/e2e-bootstrap.js load the calculators
component without the SPA auth wall (scripts external to satisfy CSP).
Server:
- server.js now re-reads public/index.html on mtime change instead of
caching at boot. Fixes the "edit HTML, restart container" friction.
- CSP upgradeInsecureRequests disabled in helmet config; Caddy still
enforces HTTPS at the reverse-proxy layer in production.
Adds `npm run maint:check` (health report) and `npm run maint:reindex`
(REINDEX DATABASE + REFRESH COLLATION VERSION + ANALYZE) for post-
upgrade maintenance, modelled after Nextcloud's occ maintenance.
Documented in README.
Also relaxes postgres image from digest pin back to tag-pin
(pgvector/pgvector:pg16) — the auto-REINDEX-on-drift check in
database.js and the COLLATE "C" protection on critical indexes
make the digest pin redundant while blocking ordinary `compose
pull` updates.
FINAL WORKING SOLUTION:
Previous attempts failed because:
- transformers.js v2.17.2 is ES module-only
- Module workers require complex CSP and external imports
- importScripts() doesn't work with ES modules
Solution:
- Use transformers.js v2.6.2 (has worker-compatible UMD build)
- Bundle library + models, serve entirely from our server
- Classic worker with importScripts() - no CSP issues
What's self-hosted:
- ✅ transformers.min.js (760KB) - at /models/transformers.min.js
- ✅ Whisper models (42MB) - at /models/Xenova/whisper-tiny.en/
Worker loads:
1. importScripts('/models/transformers.min.js') - OUR SERVER
2. Loads models from /models/ - OUR SERVER
3. ZERO external network calls
4. Works in any network (firewalled, air-gapped, etc.)
This is the production-ready, truly offline solution.
Features:
- Admin can add, edit, and delete developmental milestones via dashboard
- Milestones stored in PostgreSQL (developmental_milestones table)
- Client-side loads milestones from API instead of static file
- Import script to migrate existing static data to database
- Organized by age group and domain
- Supports sorting and filtering
Admin UI:
- New section in Admin panel for milestone management
- Filter by age group
- Add/Edit modal with validation
- Delete with confirmation
- Auto-complete for age groups and domains
API Endpoints:
- GET /api/milestones-data - Public endpoint for authenticated users
- GET /api/admin/milestones - List all milestones (admin only)
- GET /api/admin/milestones/meta - Get age groups and domains
- POST /api/admin/milestones - Create milestone
- PUT /api/admin/milestones/:id - Update milestone
- DELETE /api/admin/milestones/:id - Delete milestone
- POST /api/admin/milestones/bulk-import - Bulk import
Usage:
1. Run import script: node scripts/import-milestones.js
2. Access Admin dashboard → Developmental Milestones section
3. Add/Edit/Delete milestones as needed
BREAKING FIX: Browser Whisper now fully self-contained
Previous issue:
- Loaded transformers.js from cdn.jsdelivr.net
- Downloaded models from cdn-lfs.huggingface.co
- Failed in corporate/clinical networks with firewall
- Stuck at "Initializing..." with no progress
Solution:
- Bundle transformers.js library (~876KB)
- Bundle Whisper tiny.en model (~42MB)
- Serve everything from local server
- Works in ANY network environment
Changes:
- whisperWorker.js: Load transformers from /models/ instead of CDN
- Dockerfile: Download models during Docker build
- Add download script for local dev
- Add comprehensive setup documentation
Docker image size: +~42MB (one-time cost, runtime benefit)
Tested: Works on unrestricted and firewalled networks