pediatric-ai-scribe-v3/docs/logic/bedside-and-calculators.md
Daniel 503f5afaad feat: ED multi-stage UX, extensions polish, docs viewer + application-logic docs
Three concurrent themes from this session:

═══════════════════════════════════════════════════════════════════
ED ENCOUNTERS — per-stage cards + consolidate→MDM finalize
═══════════════════════════════════════════════════════════════════

UX redesign per Daniel's feedback ("every stage note should be shown,
if AI is told to modify that particular note then the modified version
is used in final mdm"):

- Each generated stage stays on screen as its own editable card with
  its own embedded "Don't Miss" panel. No more single rolling note
  element that gets replaced on each generation.
- gatherCurrentNotes() reads contenteditable text from each stage card
  before any operation (advance, finalize, persist) so inline edits
  flow into the next AI call and the final consolidate.
- Stage badge is now state-accurate. "Stage N (recording)" with yellow
  background after Add-more before generation; "Stage N" with gray
  after generation. Fixes the bug where the badge flipped to Stage 2
  the moment Add-more was clicked.
- Save & Done now runs TWO server-side AI calls in /finalize:
  1. edConsolidate (new prompt) → polished single final note that
     integrates every stage chronologically (HPI / ROS / PE / ED Course /
     A&P with disposition).
  2. edFinalize (rewritten with full inline 2023 AMA E/M element
     rubric — problems / data / risk definitions, level mapping with
     concrete examples) → MDM JSON.
- Two new cards render after finalize: blue-bordered Final Consolidated
  Note + green-bordered MDM. Stage cards become read-only.
- partial_data on the saved row now stores {stages, finalNote, mdm,
  finalized} so resume re-renders the full state.

Why two-call finalize: a single combined prompt makes the model cut
corners on one task. Two focused calls cost ~2× latency at the very end
of an encounter — acceptable since finalize is a one-time terminal
action, not a per-stage hot path.

Files: public/components/ed-encounter.html, public/js/ed-encounters.js,
src/routes/edEncounters.js, src/utils/prompts.js (edConsolidate added,
edFinalize rewritten).

═══════════════════════════════════════════════════════════════════
EXTENSIONS / PAGERS — visual polish
═══════════════════════════════════════════════════════════════════

Multiple iterations based on Daniel's feedback:

- Layout: align-items:flex-start so action buttons stay pinned top-right
  when long numbers wrap (was align-items:center → buttons drifted into
  the text area, causing visible overlap).
- Number: word-break:break-all + min-width:0 + font-feature-settings:tnum
  so long numbers wrap within their column instead of pushing under the
  buttons. Click-to-copy with a 0.55s green flash + ✓ copied badge.
- Phone/pager Font Awesome icon next to the number in the type color —
  at-a-glance type signal (replacing an earlier 3px left stripe that
  Daniel found visually bulky).
- Name: font-weight 700, font-size 14.5px, color g900, letter-spacing
  -0.012em — scan-target headline typography for long lists.
- Alternating subtle backgrounds by index (white vs #fafbfc) so a long
  list reads as distinct rows.
- Hover: card lifts 1px with a soft shadow; action buttons fade from
  55% to 100% opacity. Cubic-bezier transition on transform.
- Entrance: staggered fade-up animation per card (35ms × index, capped
  at 12). prefers-reduced-motion media query disables motion.
- Empty state: 48px FA icon + heading instead of plain gray text.

Files: public/js/extensions.js, public/css/styles.css.

═══════════════════════════════════════════════════════════════════
DOCS REORGANIZATION + APPLICATION-LOGIC DOCS + ADMIN VIEWER
═══════════════════════════════════════════════════════════════════

Document moves (preserving git history via git mv):
  BROWSER_WHISPER_SETUP.md          → docs/browser-whisper-setup.md
  BROWSER_WHISPER_TROUBLESHOOTING.md → docs/browser-whisper-troubleshooting.md
  DEVELOPER_GUIDE.md                → docs/developer-guide-extended.md
  EMBEDDINGS_SETUP.md               → docs/embeddings-setup.md
  FEATURES_EXPLAINED.md             → docs/features-explained.md
  IMPROVEMENTS.md                   → docs/improvements.md
  OPENID_SETUP.md                   → docs/openid-setup.md
  TRANSCRIPTION_OPTIONS.md          → docs/transcription-options.md
README.md updated with the new paths + a Documentation section that
links to docs/logic/ at the top.

New application-logic doc series (~8,300 lines total) at docs/logic/.
Built with 5 parallel doc-writing agents per Daniel's "use multiple
agents" directive. Each doc explains how a part of the app actually
works — application logic, data flow, design decisions, sacred zones,
how-to-extend recipes — at a depth that lets a new dev (or an AI
assistant) modify the code confidently.

  docs/logic/README.md                — index + recommended reading order
  docs/logic/architecture.md (2166 L) — frontend IIFE pattern, lazy tab
                                         load, backend route convention,
                                         schema, encryption, deployment
  docs/logic/clinical-notes.md (1546L) — every note tab + helper trio
  docs/logic/bedside-and-calculators.md (1373L) — bedside ES module
                                         pocket + calculators + PE Guide
                                         + suture selector
  docs/logic/auth-admin-learning.md (1281L) — auth (local+OIDC+2FA) +
                                         admin panel + Learning Hub
                                         (Quiz engine logic at sub-detail
                                         only — TODO follow-up)
  docs/logic/ai-and-voice.md (1128 L) — callAI 5-provider routing,
                                         prompts, voice/STT, helper trio
  docs/logic/ed-encounters.md (821 L) — multi-stage ED + MDM (this
                                         session's worked example)

Admin-only docs viewer:
- New route /api/admin/docs/{tree,file}: recursively walks docs/, returns
  the tree as JSON; /file?path=X validates path stays inside docs/ and
  renders markdown via marked. Both gated by req.user.role==='admin'.
- New tab "Docs" (book icon) in the sidebar, hidden by default and
  revealed in auth.js when user.role==='admin' (same pattern as the
  existing Admin and CMS tabs).
- New component public/components/admin-docs.html: split-pane layout
  with a tree sidebar + filter input + a markdown reader pane.
- New module public/js/admin-docs.js: lazy-loads the tree on first tab
  activation, renders collapsible folders, persists expanded state and
  last-opened path via UIState. Server-rendered HTML so no client
  markdown parser needed.
- CSS for the viewer (responsive split-pane, code-block styling, table
  scrolling, etc.).
- Mounted at /api/admin/docs (NOT /api) — important: mounting a router
  with router.use(authMiddleware) at /api accidentally 401s every other
  /api/* path (caught and fixed during testing — /api/health was 401'ing).

Files: docs/* (moved + new), README.md, public/components/admin-docs.html
(new), public/js/admin-docs.js (new), src/routes/adminDocs.js (new),
public/index.html (tab + section + script), public/js/auth.js (admin
gate + logout cleanup), public/css/styles.css (viewer styles), server.js
(mount).

═══════════════════════════════════════════════════════════════════
KNOWN GAPS (TODO follow-ups)
═══════════════════════════════════════════════════════════════════

- Learning Hub quiz engine (MCQ / multi-select / T-F scoring + attempt
  tracking + progress dashboard) is covered at the architectural level
  in docs/logic/auth-admin-learning.md but not drilled into the quiz
  data model and scoring flow. Worth a focused follow-up doc.
- ED finalize: if MDM step JSON parse fails, server returns 502 with
  the consolidated finalNote in the error payload, but client doesn't
  surface the partial result. Add a "MDM failed, retry" affordance.
- No e2e Playwright coverage for ED encounters or the new docs viewer.
2026-04-28 03:09:38 +02:00

58 KiB
Raw Blame History

Bedside, Calculators, PE Guide, Vax Schedule, Milestones

Dev-deep walkthrough of the reference / decision-support half of the PedScribe SPA. These modules differ from the rest of the app in three ways:

  1. No PHI flows through them. Inputs are weight, age, vital signs, exam findings — not patient identifiers. Nothing here writes the encounters or notes tables.
  2. Most of them never call AI. Bedside, Calculators and the static Vax/Catchup tables are pure JS. Only PE Guide and Milestones call /api/generate-pe-narrative / /api/generate-milestone-narrative to turn a checklist into prose.
  3. Bedside is the one corner of the frontend that uses native ES modules. Everything else is IIFE. See section 12.

The general rule: this is the part Daniel uses at the bedside in the ED / clinic / NICU. Speed and trust matter — every dose string shows the per-kg + max so the prescriber can re-verify the math without leaving the screen, and every clinical formula has a citation in the same render.


1. Overview

Module File(s) Loaded as Talks to backend?
Bedside emergencies public/js/bedside/* <script type="module"> No
Calculators public/js/calc-math.js + public/js/calculators.js + public/js/drugs-loader.js IIFE (defer) Fetches /data/drugs.json once at boot
PE Guide public/js/peGuide.js + src/routes/peGuide.js IIFE (defer) POST /api/generate-pe-narrative
Vax Schedule + Catchup public/js/pediatricScheduleData.js consumed by public/js/wellVisit.js IIFE (defer) No
Milestones public/js/milestonesData.js + public/js/milestones.js + src/routes/milestones.js + src/routes/adminMilestones.js IIFE (defer) GET /api/milestones-data (DB-backed, static fallback), POST /api/generate-milestone-narrative, POST /api/generate-milestone-summary

Reference content vs the rest of the app:

  • The clinical-documentation half of PedScribe (Notes / SOAP / HPI / Encounters / SickVisit / WellVisit / Hospital / ED) takes free-text input and runs it through callAI() to produce structured output. PHI is encrypted at rest with crypto.js AES-256-GCM.
  • The reference half documented here takes structured input (weight, age, score components, exam checkboxes) and produces rendered HTML. No persistence. No PHI. Refresh the page and it's gone.

Loader order in public/index.html (see lines 447479):

447  milestonesData.js          // static fallback for MILESTONES_DATA_STATIC
448  pediatricScheduleData.js   // VISIT_AGES, PERIODICITY, CATCH_UP_SCHEDULE, ...
464  milestones.js              // wires up wellvisit milestones tab
465  peGuide.js
469  wellVisit.js               // consumes pediatricScheduleData
476  calc-math.js               // pure math, dual-export node+window
477  drugs-loader.js            // fetches /data/drugs.json
478  calculators.js             // 2941-line UI wiring
479  bedside/index.js           // type="module"  ← the only ES-module entrypoint

Bedside loads last and as a module so the import graph (bedside/index.js → ./shared.js + each section module) resolves without polluting window.


2. Bedside architecture

Bedside is the single tree under public/js/bedside/. 21 files. index.js is 49 lines and does only two things: import every section module and call init() on each.

bedside/index.js

import './shared.js';           // side-effect: window._EM = S
import * as ageWeight from './age-weight.js';
import * as subNav from './sub-nav.js';
// ... 17 more sections ...
[ageWeight, subNav, lightbox, neonatal, ..., sutures]
  .forEach(m => { if (m && typeof m.init === 'function') m.init(); });

Every section module exports an init() that registers a document.addEventListener('click', ...) handler. They use event delegation because the bedside HTML is injected lazily on first tab activation — by the time the user clicks a button, the section's div exists, but the handlers were registered before that.

bedside/shared.js

42 lines. Exports S, a record of pure formatting helpers used by every section. Also assigns window._EM = S for back-compat — there are no known callers of window._EM in the current tree, but the assignment is kept as a safety net.

Helper Purpose
S.d(wt, perKg, max) rounded dose number (no formatting)
S.dStr(wt, perKg, max, unit) formatted dose string with (perKg/kg, max ...) footer; appends · capped if max hit
S.drugTable(rows) wraps <tr> rows in a horizontally-scrollable table with Drug/Dose/Route/Notes columns
S.drugRow(name, dose, route, notes) one <tr>
S.stepBox(color, title, body) a coloured left-border card used for pathway steps (NRP, status epilepticus, sepsis bundle)
S.arrow down-arrow separator between step boxes
S.badge(label, color) severity pill
S.ref(text) small grey italic footnote (used for citations at the end of every result)

Why the dose footer matters: S.dStr(30, 0.1, 4) renders 3 mg (0.1 mg/kg, max 4 mg). S.dStr(60, 0.1, 4) renders 4 mg (0.1 mg/kg, max 4 mg · capped). The prescriber sees both the final answer and the math behind it without leaving the screen.

bedside/sub-nav.js

44 lines. Owns the pill-row across the top of the Bedside tab. Two responsibilities:

  1. On click of any [data-em] pill, hide every #em-<section> div except the active one. Persist pill.dataset.em to window.UIState under key bedside.pill.
  2. On tabChanged to bedside, re-apply the saved pill from UIState.get('bedside.pill'). Also runs once on init() for the e2e harness where the DOM is synchronously ready.

The SECTIONS array at line 8 is the master list of pill keys:

['neonatal','airway','cardiac','respiratory','ventilation','seizure',
 'sepsis','anaphylaxis','sedation','agitation','antiemetics',
 'antimicrobials','burns','toxicology','trauma','sutures']

When you add a new section (see section 11) you must add its key here so the show/hide loop knows about it.

bedside/image-lightbox.js

40 lines. Generic delegator: any element with data-img-src opens the referenced image in #img-lightbox. Closes on ESC, click on backdrop, or click on #img-lightbox-close. Used by neonatal.js for the NRP pathway PNG.

Per-section pattern

Every section module follows the same shape:

import { S } from './shared.js';

function calcXxx() {
  var wt = parseFloat(document.getElementById('xxx-weight').value);
  var el = document.getElementById('xxx-result');
  if (!wt) { el.innerHTML = '<p style="color:var(--red);...">Enter weight (kg).</p>'; return; }
  var html = '<div ...header...></div>';
  html += S.drugTable(...);
  html += S.ref('Citation text here.');
  el.innerHTML = html;
}

export function init() {
  document.addEventListener('click', function(e) {
    if (e.target.id === 'btn-xxx-calc' || e.target.closest('#btn-xxx-calc')) calcXxx();
  });
}

Because handlers use document.addEventListener + .closest(), they keep working when the bedside HTML fragment is injected after the module has loaded.


3. Per-section deep dives

Each subsection lists: input fields it reads, computation, output container, the algorithm/formula, and the citation it prints.

3.1 age-weight.js — top weight estimator

Not a section, but a banner above all sections. Reads #bedside-age (a free-text age string) and #bedside-formula (apls / bestguess). Calls window._parseAgeMonths(raw) (exposed by calculators.js line 57) to turn "3y" / "15 days" / "2y5m" into months, then window._PED_MATH.estimateWeightFromAgeMonths(months) to compute both APLS and Best Guess weights. Writes the picked one to #bedside-weight unless the user has typed in that field (dataset.userTyped flag).

The estimator is estimator-only: per-section weight inputs read themselves; there is intentionally no auto-propagation. Daniel's preference — sections own their own weight to avoid surprise overwrites when navigating between pills.

3.2 neonatal.js — Fenton + Apgar + NRP

Three calculators in one section.

Neonatal assessment (neoAssess, lines 96159). Reads #neo-ga-weeks, #neo-ga-days, #neo-weight (grams), #neo-sex. Computes:

  • GA classification (classifyGA): Extremely Preterm < 28 wk, Very < 32, Moderate < 34, Late Preterm < 37, Early Term < 39, Full Term < 41, Late Term < 42, Post Term ≥ 42.
  • Birth-weight category (classifyBirthWeight): ELBW < 1000 g, VLBW < 1500, LBW < 2500, Normal ≤ 4000, Macrosomia > 4000.
  • Z-score and percentile via the Fenton 2013 LMS table (fentonLMS, lines 2037). Female and male tables, GA 2242 wk, each row {L, M, S}. Linear interpolation between integer GA weeks (interpolateLMS). Z-score by the canonical LMS formula Z = ((value/M)^L 1) / (L · S) with the small-L branch Z = ln(value/M) / S. Z → percentile via AbramowitzStegun normal CDF approximation.
  • AGA/SGA/LGA classification by percentile.

The Fenton LMS values were re-fit empirically from peditools.org — see comment block lines 1117. Validated test case: 40+5/7 wk male 3070 g → z = 1.42 (matches Epic and peditools to 2 decimals).

Renders into #neo-result. Cites Fenton TR, Kim JH. BMC Pediatrics 2013;13:59.

NRP pathway (renderPathway, line 163). Static visual pathway built from S.stepBox calls — Birth → Assess → HR<100/apneic → HR<100 after PPV → HR<60 → Epi. Includes a 3-card grid for SpO2 ladder, ETT size by weight band, and ETT depth = 6 + weight(kg) cm. Renders into #nrp-pathway-static exactly once (dataset.rendered guard).

NRP drug calc (calcNRP, line 185). Reads #nrp-weight. Computes epi IV/IO 0.010.03 mg/kg (also as mL of 1:10 000), epi ETT 0.050.1 mg/kg, NS 10 mL/kg, D10 2 mL/kg. Renders into #nrp-result. Includes a yellow note about 1:10 000 vs 1:1000 because they're the ones that get mixed up.

Apgar (calcApgar, line 210). Sums #apgar-{appearance,pulse,grimace, activity,respiration} and routes to one of three guidance blocks (≥7 reassuring, ≥4 moderate, <4 severe). Renders into #apgar-result.

3.3 airway.js — RSI

calcAirway reads #airway-weight, optional #airway-age. Renders five tables into #airway-result:

  1. Pre-medication: Atropine 0.02 mg/kg (max 1, min 0.1), Lidocaine 1.5 mg/kg (max 100), Fentanyl 2 mcg/kg (max 150).
  2. Induction: Ketamine 1.5 mg/kg, Etomidate 0.3 mg/kg, Propofol 2 mg/kg, Midazolam 0.2 mg/kg.
  3. Paralytics: Roc 1.2 mg/kg (max 100), Succ 2 mg/kg if <10 kg else 1.5 mg/kg (max 150), Vec 0.1 mg/kg.
  4. Maintenance gtt: Midaz 0.050.2 mg/kg/hr, Fent 14 mcg/kg/hr, Ket 0.52 mg/kg/hr, Dex 0.21.4 mcg/kg/hr.
  5. Equipment sizing: ETT uncuffed = age/4 + 4, cuffed = age/4 + 3.5, depth at lip = ETT × 3. Blade by age band, LMA by weight band, suction = ETT × 2 Fr, NG = ETT × 2, chest tube = ETT × 4.
  6. Vent starting settings: TV 68 mL/kg, age-banded rate, PEEP 5, FiO2 100% titrate to 9297%, I:E 1:2 (1:34 for obstructive).

Cites Harriet Lane 23e / PALS 2020.

3.4 cardiac.js — PALS

Pill-driven sub-nav inside the section. [data-cardiac] buttons for general / asystole / brady / svt / vfib / vt. cardiacShow(kind) reads #cardiac-weight and renders the appropriate table.

  • General — full PALS drug list: epi 1:10 000 IV (0.01 mg/kg max 1) and ETT (0.1 mg/kg), amiodarone 5 mg/kg (max 300), lidocaine 1 mg/kg, atropine 0.02 mg/kg (min 0.1, max 0.5), adenosine 0.1 → 0.2 mg/kg (max 6 → 12), bicarb 1 mEq/kg, CaCl 20 mg/kg, Ca-gluc 60 mg/kg, MgSO4 50 mg/kg, D10 2 mL/kg, defib 2→4→10 J/kg, sync 0.51→2 J/kg.
  • Asystole — CPR + epi q35 min + reversible-causes grid (H's & T's).
  • Brady — Step-box: CPR if HR<60 with poor perfusion, then epi / atropine / pacing.
  • SVT — Stable: vagal → adenosine 0.1→0.2 mg/kg → procainamide / amiodarone. Unstable: sync cardioversion 0.51→2 J/kg.
  • VFib / pulseless VT — Defib 2→4→≥4 J/kg (max 10), epi after 2nd shock, amiodarone 5 mg/kg, lidocaine 1 mg/kg, MgSO4 50 mg/kg.
  • Stable VT — Amiodarone or procainamide; sync if it becomes unstable.

Cites AHA PALS 2020.

3.5 respiratory.js — Asthma + Bronchiolitis + Croup

Three sub-pills ([data-resp]) inside the section.

Asthma (asthmaManagement(severity)). [data-asthma-severity] buttons for mild/moderate/severe. Reads #resp-weight. Each severity renders its own drug table (see file:34, 43, 53 for exact dose strings). Severe also includes three side-by-side cards: when to obtain ABG (with the "rising PCO2 = impending failure" pearl in red), when to intubate (ketamine + roc as the RSI pair), and heliox notes.

PRAM (calcPRAM, line 102). Sums five #pram-* selects (SpO2, retractions, scalene, air entry, wheeze). 03 Mild, 47 Moderate, 812 Severe. Note: the same PRAM math also lives in calc-math.js:pramScore for unit testing.

Croup (calcCroup, line 111). Sums five #croup-* selects. Westley score: ≤2 Mild, 35 Moderate, 611 Severe, ≥12 Impending failure. Treatment table per band. Cites Westley 1978.

Bronchiolitis (calcBronchiolitis, line 125). Reads age, SpO2, hydration, distress dropdowns. Branches to admit-or-discharge. Always prints the AAP 2014/2023 "do NOT use" list (albuterol, epi, steroids, abx, chest PT) — that reminder is the most useful part of the section.

3.6 ventilation.js — O2 ladder + vent settings

Pure reference, no branching on weight. showVent (#btn-vent-show) renders into #vent-result:

  1. Target SpO2 table (most kids, bronchiolitis, CLD, preterm, term neonate per NRP ladder).
  2. Escalation ladder — six step-boxes from low-flow NC → simple mask → NRB → HFNC → NIV (CPAP/BiPAP) → intubate.
  3. BVM how-to: rate by age, TV 68 mL/kg, MR SOPA mnemonic.
  4. Vent starting settings (mode, TV, rate, PEEP, FiO2, I:E, Ti, plateau <30).
  5. Inline SVG pressure-time waveform with PIP / Plateau / PEEP / Ti / Te annotated. Hand-drawn <path> at line 99.
  6. "Adjusting for gas exchange" troubleshooting table — first lever / second lever for low SpO2, ↑PCO2, ↓PCO2, high peak pressure, auto-PEEP.
  7. Mental-model summary: oxygenation = FiO2+PEEP; ventilation = rate+TV.

Cites AAP / PALS / AARC.

3.7 seizure.js — Status epilepticus pathway

calcSeizure reads #seizure-weight and emits a time-anchored visual pathway via the local stepRow(time, color, title, body) helper. Each row is a colored time-pill (0 / 5 / 10 / 20 / 30 / 40 min) connected by a vertical line to a step card.

Time Step Drugs
0 Stabilize ABCs, glucose, labs D10W if BG<60 (25 mL/kg)
5 1st benzo Lorazepam 0.1 mg/kg IV (max 4); Midaz 0.2 mg/kg IM/IN/buccal (max 10); Diaz 0.5 mg/kg PR (max 20)
10 2nd benzo (same agent)
20 2nd-line Levetiracetam 60 mg/kg (max 4500) preferred; Fosphenytoin 20 mg/kg PE (max 1500); Valproate 40 mg/kg (max 3000); Phenobarb 20 mg/kg (max 1000) — last choice
30 2nd 2nd-line OR refractory
40 Refractory: intubate + cEEG Midaz gtt 0.050.5 mg/kg/hr; Pentobarb gtt 15 mg/kg/hr; Propofol gtt 25 mg/kg/hr (PRIS warning); Ketamine gtt 15 mg/kg/hr

Drug list lives in two places: SEIZURE_FALLBACK (lines 3341, inline fallback) and window._DRUGS.sections.seizure from /data/drugs.json. seizureDrugs() merges them — JSON wins per-field, fallback fills the gaps. Refractory-infusion ranges are kept inline because the JSON schema doesn't model bolus → gtt range cleanly.

Key-points box at the bottom encodes the ESETT trial finding (levetiracetam = fosphenytoin = valproate). Cites AES 2016 + Kapur 2019 NEJM.

3.8 sepsis.js — Phoenix + febrile-infant rules + first-hour bundle

showSepsis reads #sepsis-weight, #sepsis-age (neonate / infant / child).

  • Definition box: Phoenix 2024 (JAMA) sepsis criteria — score ≥2 organ-dysfunction. Notes that previous SIRS-based criteria (Goldstein 2005) are now superseded.
  • Red-flags grid.
  • Age-specific empiric abx tables. Neonate: Amp 100 mg/kg + Gent 4 mg/kg ± Cefotaxime + Acyclovir 20 mg/kg. Infant: Ceftriaxone 75 mg/kg ± Amp ± Vanc ± Acyclovir, plus an embedded <details> block with four validated febrile-infant rules — PECARN 2019, Aronson 2019, Rochester 1994, Step-by-Step 2016. Each has its own colored card with the actual cutoffs (PECARN ANC ≤4090, procal ≤0.5; Aronson scoring 08; Rochester WBC 515k bands; the Step-by-Step decision tree). Child: Ceftriaxone + Vanc ± Pip-tazo ± Clinda for TSS.
  • First-hour bundle as five S.stepBoxes: 05 recognize, 515 access
    • cultures, 1530 fluids 20 mL/kg, 3060 abx, >60 vasopressors (epi for cold shock, norepi for warm) + stress-dose hydrocortisone 2 mg/kg.
  • Resuscitation targets list.

Cites Schlapbach JAMA 2024, SSC Peds 2020, AAP.

3.9 anaphylaxis.js

calcAnaphylaxis reads #anaph-weight. Step 1 box is always epinephrine first: 1:1000 (1 mg/mL) 0.01 mg/kg IM (max 0.5 mg) to lateral thigh, with auto-injector recommendation by weight (<10 kg draw manually, ≤25 kg EpiPen Jr 0.15 mg, else EpiPen 0.3 mg). Steps 26 in a single combined table: position, O2, NS 20 mL/kg, DPH 1.25 mg/kg, ranitidine, dex 0.6 mg/kg or methylpred 2 mg/kg. Refractory section: epi gtt 0.11 mcg/kg/min, glucagon 0.02 mg/kg for beta-blocker patients.

Drug doses are looked up via anaphDrug(name) which prefers window._DRUGS.sections.anaphylaxis and falls back to ANAPH_FALLBACK (lines 1422). The 46h observation note for biphasic reactions is the load-bearing teaching point. Cites ASCIA 2021, WAO 2020, AAP/ACAAI.

3.10 sedation.js — Procedural sedation

JSON-backed via sedDrugs() with SED_FALLBACK (lines 1527) merged in. Two tables:

  1. Sedation agents — Ketamine IV 1.52 mg/kg + IM 45 mg/kg, Midaz IV 0.050.1 + IN/PO 0.5, Propofol 1 mg/kg, Fentanyl IV 1 mcg/kg + IN 2 mcg/kg, Nitrous 50:50 mix, Dexmed IN 23 mcg/kg.
  2. Reversal agents — Naloxone 0.1 mg/kg IV/IM/IN (max 2), Flumazenil 0.01 mg/kg (max 0.2 single, 1 mg total) with seizure warning.

sedDoseCell(wt, dg) handles three dose shapes: low/high range, single, or fixed dose_display. Pre-sedation checklist box at the bottom lists NPO, monitoring (pulse ox + capnography + BP), resuscitation equipment, IV access. Cites AAP sedation guideline + ASA.

3.11 agitation.js — Step 1/2/3

AGIT_FALLBACK (lines 1226) split by step: 2 (PO/IN — cooperative) and step: 3 (IM/IV — severe).

  • Step 1: non-pharm only (rule out hypoglycemia, hypoxia, pain, ↑ICP, toxic, infection).
  • Step 2: Lorazepam 0.05 mg/kg PO (max 2), Midaz 0.5 mg/kg PO or 0.3 mg/kg IN, Olanzapine ODT (weight-banded), DPH 1 mg/kg.
  • Step 3: Midaz 0.1 mg/kg IV/IM, Lorazepam 0.1 mg/kg, Haloperidol (avoid <3 yr; QT/EPS/NMS warning; weight-band dose), Olanzapine IM (avoid benzo combo for resp depression), Ketamine 4 mg/kg IM as rescue, Droperidol 0.05 mg/kg with ECG.

agitRowFor(wt, dg) handles the weight-conditional dose strings via dose_display_if_under_30kg / _30kg_or_more and the "prefix = X mg" formatting for Droperidol. Cites AAP + ACEP.

3.12 antiemetics.js

JSON-backed via EMET_FALLBACK (lines 1018). Drug list:

Drug Per-kg Max Notes
Ondansetron 0.15 mg/kg 8 mg First-line; weight-band: <15 kg 2 mg, 1530 kg 4 mg, ≥30 kg 8 mg; QT
Metoclopramide 0.15 mg/kg 10 mg Give with DPH for EPS prevention
Dimenhydrinate 1.25 mg/kg 50 mg ≥2 yr
DPH 1 mg/kg 50 mg Sedating adjunct
Promethazine 0.25 mg/kg 25 mg CONTRAINDICATED <2 yr
Dex 0.15 mg/kg 10 mg Adjunct, esp chemo
Scopolamine fixed 1.5 mg patch ≥12 yr; motion sickness

emetRows(wt, drugs) renders Ondansetron with both the weight-band dose AND the per-kg dose so the prescriber can pick. Pearls box at the bottom flags QT-stacking risk. Cites AAP / WHO / Lexicomp.

3.13 antimicrobials.js — Empiric by age × infection

The only section that does no dose math. REG[age][infection] is a 3 × 9 matrix of {first, alt, duration, notes} strings. Age = neonate / infant / child. Infection = sepsis / meningitis / pna / uti / skin / ent / neutropenic / ic / bone. lookupAbx reads #abx-age, #abx-infection, looks up REG[age][inf] and renders four boxes: First-line (green), Alternative/add, Duration, Notes (yellow). Cites Red Book / IDSA / Harriet Lane.

3.14 burns.js — Lund-Browder + Parkland

The most interactive section.

  • LB table at line 10 — 19 body regions × 5 age brackets (infant / young 15 / child 510 / adol 1015 / adult >15). Head shrinks 18→7%, thigh grows 5.5→9.5% with age — those are the only age-sensitive rows (ageSensitive: true).
  • renderBodyParts builds an input grid with one number field per region. Each input shows the region's max % for the selected age in parentheses. Re-renders on #burn-age change so the bracket-specific max updates.
  • computeTbsa sums region% × involvement%. Live-updates #burn-tbsa-live on input.
  • calcBurn reads #burn-weight, uses computed TBSA (or the #burn-tbsa override). Parkland: 4 mL × kg × %TBSA over 24 h (half in first 8 h from time of burn). Maintenance by Holliday-Segar 4-2-1. Includes a per-region breakdown <details> and the ABA burn-center referral criteria yellow box.

Cites ABLS 2018, Baxter 1968 (Parkland), Lund-Browder 1944.

The same LB matrix is exported from calc-math.js (LB, LB_BRACKETS, lundBrowderTBSA) for unit testing under Node.

3.15 toxicology.js — Topic-driven

#tox-topic dropdown picks one of: approach / acetaminophen / opioids / iron / tca / bbccb / benzo / organo / salicylate / etoh / dialyzable. lookupTox reads #tox-weight and the topic; renders a different layout per topic.

The approach topic is five S.stepBoxes: stabilize → toxidrome identification (sympathomimetic, anticholinergic, cholinergic, opioid, sedative-hypnotic) → decontamination (charcoal 1 g/kg max 50 g; WBI; no ipecac) → enhanced elimination → antidotes.

Topic-specific tables call out: Rumack-Matthew nomogram for acetaminophen + 21-h NAC IV protocol, naloxone titration "to respiratory effort, not consciousness", iron stages 15 + deferoxamine

  • "charcoal does NOT bind iron", TCA bicarb to pH 7.457.55 + QRS widening, BB/CCB high-dose insulin (HIE) protocol + calcium first, organophosphate atropine "double q35 min until bronchial secretions dry" + 2-PAM, salicylate urinary alkalinization to urine pH >7.5, fomepizole for toxic alcohols, ISTUMBLE mnemonic for dialyzable drugs.

Always cites Poison Control 1-800-222-1222 + Goldfrank's + EXTRIP.

3.16 trauma.js — ABCDE + MTP

showTrauma reads #trauma-weight. Renders five ABCDE step-boxes (A airway+c-spine, B breathing, C circulation 20 mL/kg, D disability + ICP rescue with mannitol 0.51 g/kg or 3% saline 35 mL/kg, E exposure). Then:

  • MTP table: 1:1:1 ratio activated at ≥40 mL/kg or ongoing hemorrhage; TXA 15 mg/kg (max 1 g) within 3 h then 2 mg/kg/hr × 8 h (CRASH-2); calcium per unit citrated blood.
  • C-spine clearance reminder.
  • Pediatric shock signs box — emphasizes hypotension is late, minimum SBP = 70 + 2×age (110 yr).
  • Secondary-survey AMPLE mnemonic.

Cites ATLS 10e, PALS 2020, PECARN c-spine, CRASH-2.

3.17 sutures.js — covered in detail in section 4


4. Suture selector deep-dive

Newest Bedside section (435 lines, last commit 2026-04-26 per the ls listing). Decision-engine model rather than a calculator — given inputs, it returns a structured recommendation.

Inputs

Read by readInput() (line 253):

Field DOM id Values
Site sut-site one of 15 (see below)
Age sut-age lt1 / 1to5 / school / adolescent
Tension sut-tension low / moderate / high
Cosmetic priority sut-cosmetic yes / no
Contamination sut-contam clean / heavy / bite_dog / bite_cat / bite_human / bite_other
Hours since injury sut-hours lt12 / >12
Avoid removal sut-removal-avoid yes / no

15 anatomic sites

Defined in the SITES table (line 19). Each entry carries a materialFamily (monofilament-nonabsorbable / absorbable-chromic-or-vicryl), default sutureSize, default removalDays, glueOK boolean + glueNote, default techniqueDefault, and free-text notes.

Site Default size Removal Glue OK
face 6-0 35 d ✓ Dermabond ≤4 cm linear, low tension
eyelid_eyebrow 6-0 35 d ✗ ocular irritation
lip_vermilion 6-0 35 d ✗ — first stitch must align vermilion border
intraoral 4-0 / 5-0 absorbable n/a
ear 5-0 45 d
scalp 3-0 / 4-0 710 d ✓ HAT (hair apposition) for clean linear lacs ≤10 cm
neck 5-0 57 d ✓ superficial only
trunk 4-0 / 3-0 710 d ✗ tension too variable
upper_ext 4-0 710 d ✓ ≤5 cm linear, not over joints
lower_ext 4-0 / 3-0 1014 d ✓ same caveats; not on shin
hand 5-0 1014 d ✗ high tension
foot 4-0 / 5-0 1014 d
joint_surface 4-0 1014 d ✗ flexion separates the bond
genitalia 5-0 / 4-0 absorbable n/a
fingertip 6-0 absorbable n/a

Decision logic (recommend(input), line 173)

  1. Closure plan:

    • Default: Primary closure.
    • heavy or bite_other: Consider DELAYED primary closure (cleanse, pack, reassess in 35 d) + warning.
    • bite_human / bite_cat: Generally DO NOT close primarily + warning naming the organism (Pasteurella for cat, Eikenella for human) and prescribing amox-clav prophylaxis.
    • bite_dog AND site == 'hand': warning that many EPs do not close primarily; loose approximation only.
    • hours == '>12' AND site is not face/scalp: switches to delayed primary closure with a soft note ("judgment-based; clean linear wounds in well-perfused areas can still be closed").
  2. Material: starts at the site's materialFamily. If monofilament-nonabsorbable AND age is lt1 or 1to5 AND removalAvoid == yes, switches to fast-absorbing gut / Vicryl Rapide so no removal visit is needed.

  3. Size adjustments:

    • High tension bumps size up one (3-0 → larger needle: 4-0→3-0, 5-0→4-0, 6-0→5-0).
    • Cosmetic + low tension on a 4-0 site bumps it down to 5-0.
  4. Technique: defaults from the site. High tension promotes to vertical mattress (or simple interrupted with deep dermal absorbable layer). Cosmetic + low tension on face/eyelid suggests subcuticular "if skill permits."

  5. Glue alternative appears only if s.glueOK && tension !== 'high' && contamination === 'clean'.

  6. Removal day range is nulled if a non-monofilament material was selected.

  7. Tetanus string is the same for every result: Tdap if last booster >5 y for tetanus-prone (contaminated, deep, crush, burn, bite); >10 y for clean. TIG if unimmunized + tetanus-prone.

Special cases worth highlighting

  • Subungual hematoma trephination threshold — fingertip notes: trephinate "especially if 2550% or more of the nail" provided the nail is intact and there's no displaced distal-phalanx fracture.
  • Lip vermilion alignment — site notes: "vermilion misalignment as small as 1 mm is visible" + close orbicularis oris with 5-0 absorbable first; intraoral side with 4-0 chromic / Vicryl Rapide.
  • HAT on scalp — twist hair across the wound and secure with a drop of glue, requires hair ≥3 cm.
  • Plantar puncture — foot notes: do NOT close primarily; consider Pseudomonas coverage if shoe-puncture.

References printed inline

Roberts & Hedges Clinical Procedures in Emergency Medicine 7e (2019) Ch. 36 · Fleisher & Ludwig Pediatric Emergency Medicine 8e (2021) · AAP Section on Emergency Medicine — Pediatric laceration repair · UpToDate Pope JV — Skin laceration repair with sutures.

Render attaches to change events on any #sut-* field so the result updates live as the user changes any input.


5. Calculators (calc-math.js + calculators.js)

The two-file split is deliberate.

calc-math.js (207 lines) — pure math, dual-exported

IIFE that builds an API object and exposes it on window._PED_MATH and on module.exports. The module.exports half is what lets you unit-test the formulas under Node:

node --test test/

Functions exposed:

Function Purpose
roundTo(v, places) rounding utility
estimateWeightFromAgeMonths(months) APLS + Best Guess; returns picked weight, label, and all: {apls, bestGuess}
parkland(weightKg, tbsaPct) total mL, first-8-h split, hourly rates
maintenance421(weightKg) Holliday-Segar
pramScore(spo2, retractions, scalene, airEntry, wheeze) total + severity
westleyScore(...) total + severity
epiAnaphylaxisIM(weightKg) 0.01 mg/kg cap 0.5 mg, with 1:1000 volume
epiArrestIV(weightKg) 0.01 mg/kg cap 1 mg, with 1:10 000 volume
nrpEpiIV(weightKg) 0.010.03 mg/kg range
rsiKetamine / rsiRocuronium / rsiSuccinylcholine(weightKg) RSI doses with caps and the <10 kg succ rule
minSBP(years) 70 + 2 × yr (110 yr); 70 if <1; 90 if >10
ettSize(ageYears) uncuffed = age/4+4, cuffed = age/4+3.5, depth = uncuffed × 3
lundBrowderTBSA(ageBracket, involvements) sums regional %; same LB table as bedside/burns.js

Why does this exist alongside the bedside section files which already do the same math? Because the bedside files render HTML strings, which can't be unit-tested. calc-math.js is the testable layer and the canonical source. When the two diverge it's a bug in the bedside file.

calculators.js (2941 lines) — UI wiring for the Calculators tab

Single IIFE. Initialises on first tabChanged to calculators (line 7). Top-level pill nav (line 60) toggles .calc-panel divs.

Calculators in public/components/calculators.html (10 pills):

data-calc="bp" / "bmi" / "growth" / "bili" / "vitals"
         / "bsa" / "dose" / "resus" / "gcs" / "equipment"

5.1 BP percentile (line 83) — AAP 2017 + Rosner LMS splines

The most numerically intensive calculator in the app. Computes the exact percentile for systolic and diastolic BP given age, sex, and height — no lookup tables, regression-derived.

  • Height percentile uses a Z calculation from the 218-entry _htLMS_F_* and _htLMS_M_* arrays (one entry per month from age 24 to 240 months). LMS formula: Z = ((ht/M)^L 1) / (L · S). Index = round(age × 12) 24, clamped to [0, 217].
  • BP regression (computeBPPercentile, line 514) implements the BCM/Rosner quantile-spline model:
    • Restricted cubic splines built on height (5 knots t1..t5 per sex), age (5 knots ta1..ta5), and the height×age interaction w = (age 10) × (height meanHeight) (5 knots tb1..tb5).
    • Per percentile (1..99) the predicted BP is a 13-term linear combination using coefficient row coeff[i] from _bpCoeff_{F|M}_{SYS|DIA}. Each coefficient table is 99 × 13 (see line 97 onwards for the female systolic table — 99 rows).
    • Patient's percentile = the percentile whose predicted value is closest to the entered BP.
  • Classification (classifyBPFromPercentiles, line 578) per AAP 2017:
    • Age ≥13: absolute thresholds (120/80 elevated, 130/80 stage 1, 140/90 stage 2).
    • Age 1<13: percentile thresholds with absolute caps (≥90th elevated, ≥95th stage 1, ≥95+12 stage 2). Overall = max(sys, dia).
  • Render (line 617) shows classification badge, sys/dia percentiles, height percentile, and reference 50th / 90th / 95th BPs at this child's height + sex. Then renderBPChart plots Chart.js curves of the three reference percentiles across ages 117 with the patient as a dot.

The _bpCoeff_* arrays are load-bearing reference data. See section 10.

5.2 BMI percentile (line 734) — CDC 2000

bmiLMS (line 739) — every-3-month LMS table for both sexes from 24 to 240 months. interpolateLMS linearly interpolates between neighbouring rows. calcBMIPercentile returns Z + percentile.

classifyBMI (line 1000) implements the AAP 2023 "Class 1 / 2 / 3 obesity" extension on top of percentile classes:

Condition Class
<5th Underweight
584th Healthy Weight
8594th Overweight
≥95th, %of95<120 Obese (Class 1)
≥95th, 120139% of 95 Class 2 Severe
≥95th, ≥140% of 95 Class 3 Severe

%of95 = bmi / bmi95 × 100 where bmi95 is the LMS-derived 95th percentile BMI for this age/sex. Render shows BMI, percentile, Z-score, and (if ≥85th) the % of 95th and the 95th-BMI.

5.3 Growth charts (line 1143) — WHO + CDC + Fenton

wfaLMS (WHO weight-for-age 060 mo) at line 1150 plus parallel tables for length, head circumference, height, weight-2-20, etc. renderGrowthChart uses Chart.js with generatePercentileCurves to draw 3rd / 10th / 25th / 50th / 75th / 90th / 97th LMS curves and overlay the patient point. calcMidParentalHeight (line 992) implements Tanner's mid-parental height (boys: ((mom + dad + 13) / 2); girls: ((mom + dad 13) / 2); ±8.5 cm target range).

5.4 Bilirubin — AAP 2022 Phototherapy + Bhutani (line 1408)

The phototherapy and exchange thresholds came directly from the peditools.org/bili2022 API — see comment block lines 14741486. 1190 values cross-validated 1:1 against peditools (RMSE = 0).

Per-week tables for phototherapy:

  • No-risk: photoThresholds35 / 36 / 37 / 38 / 39 / 40 (40 also covers 41+).
  • With-risk: photoThresholds35risk / 36 / 37 / 38 (38+ identical per AAP 2022).

Per-week tables for exchange transfusion:

  • exchangeThresholds35 / 36 / 37 / 38 (38+ identical) and 35risk / 36 / 37 / 38risk.

The keys are integer hours-of-life (12 to 96). interpolateThreshold linearly interpolates between adjacent hours. The previous implementation grouped 35 / 3637 / ≥38 — that table underestimated thresholds by up to 1.0 mg/dL for 3941 wk infants and could miss phototherapy indications. Do not regress to bucketed tables.

renderBiliChart plots the phototherapy threshold (red, filled below)

  • exchange threshold (dark red, dashed) + patient as a scatter point.

Bhutani nomogram (line 1644) — bhutaniZones.p95 / p75 / p40 hour-specific risk percentile boundaries for ≥35 wk GA infants. Digitised from Figure 2 of Bhutani et al., Pediatrics 1999;103(1):6-14 via the codingace.net JS arrays + cross-checked against the AAP 2004 CPG reproduction. Zone semantics:

  • TSB ≥ p95 → High-risk
  • p75 ≤ TSB < p95 → High-intermediate
  • p40 ≤ TSB < p75 → Low-intermediate
  • TSB < p40 → Low-risk

The Bhutani tab is for risk stratification only — the AAP 2022 tab is the source of truth for treatment thresholds.

5.5 Vital signs (line 1698)

Static VITALS_DATA keyed by age band (premie / newborn / infant / toddler / preschool / school / adolescent). Each entry has HR awake/sleeping ranges, RR, BP, temperature route ranges. Cites Harriet Lane 23e.

5.6 BSA (line 1069) — Mosteller

bsa = sqrt(height × weight / 3600). Three lines.

5.7 Weight-based dosing (line 1095)

Generic helper: weight × dose-per-kg, divided by frequency, capped at optional max, optionally divided by concentration to give volume. Doesn't know any specific drug.

5.8 Resus meds (line 1869)

RESUS_MEDS array of 13 drugs (Adenosine, Amiodarone, Atropine, CaCl, Ca-gluc, Dextrose, Epinephrine, Hydrocortisone, Insulin, Lidocaine, MgSO4, Naloxone, Bicarb). Each row has a calc(weight) closure returning {dose, extra, max}. Categorised cardiac / metabolic / reversal with colour coding. The Dextrose row branches by weight (D10 for <5 kg, D25 for <45 kg, D50 for adult).

5.9 GCS (line 2109)

Sub-pill nav for adult vs pediatric verbal score. Sums eye + verbal + motor selects. The pediatric verbal subscale uses age-appropriate wording (coos/babbles, irritable cry, etc.).

5.10 Equipment (line 2169)

EQUIP_DATA static lookup by weight/age band: BVM size, nasal/oral airway, blade, ETT (uncuffed and cuffed), LMA, GlideScope, IV gauge, central-line, NGT, chest tube, Foley. Cites Harriet Lane.

Stale code in calculators.js

Lines 22712941 contain a now-redundant duplicate of the bedside sections (Neonatal (function(){...})() IIFE, Respiratory, Seizure, Anaphylaxis, Sedation). These ran before the section modules were extracted to public/js/bedside/. They still execute (the IIFE runs at script load) but the bedside HTML buttons (#btn-neo-assess, #btn-pram-calc, etc.) are now also handled by the ES-module sections. Since both handlers use e.target.id checks and call the same calculation endpoints, the user sees the result of whichever one ran last. Cleanup task: delete lines 22712941. Out of scope for this doc — flagged for the improvement-roadmap file.


6. drugs-loader.js — JSON-backed drug references

31 lines. Fetches /data/drugs.json once at boot, exposes:

  • window._DRUGS — the parsed JSON (or {version: null, sections: {}} before fetch resolves; never null).
  • window._DRUGS_READY — Promise resolving to the JSON. Not currently awaited by any consumer; bedside sections check window._DRUGS lazily at render time.

/data/drugs.json (versioned 1.0, last_reviewed 2026-04-20) contains five sections matched to bedside modules: anaphylaxis, sedation, seizure, agitation, antiemetics. Each entry has at minimum name, dose_mg_per_kg, max_mg, unit, route, notes, source. Range-based entries (sedation Ketamine, Midaz IV) use dose_mg_per_kg_low / _high and max_mg_low / _high. Fixed-dose entries (Nitrous "50:50 mix") use dose_display.

The bedside consumers (anaphylaxis.js, sedation.js, agitation.js, antiemetics.js, seizure.js) all follow the same merge pattern:

function xxxDrugs() {
  var s = window._DRUGS && window._DRUGS.sections && window._DRUGS.sections.xxx;
  if (s && s.drugs && s.drugs.length) {
    return s.drugs.map(function(dg) {
      var fb = XXX_FALLBACK.filter(function(x) { return x.name === dg.name; })[0] || {};
      return Object.assign({}, fb, dg);  // JSON wins per-field
    });
  }
  return XXX_FALLBACK;
}

This means:

  1. The bedside section keeps working even if the JSON fetch fails (CORS, 404, offline) — XXX_FALLBACK is a hard-coded mirror of the JSON shape.
  2. When the JSON loads, individual fields (notes, route, max_mg) can be updated centrally without editing 5 JS files.
  3. The JSON is authoritative when present; the inline fallback exists only for resilience.

7. PE Guide (peGuide.js + src/routes/peGuide.js)

Interactive OSCE-style physical-exam reference + AI narrative generator.

Frontend (public/js/peGuide.js, 1714 lines)

Single IIFE, gated on first tabChanged to peguide. Three top-level selectors in pe-guide.html (#pe-age-group, #pe-system pills) + demographics fields + report-style and model dropdowns.

SCALES (line 23)

Universal grading scales rendered at the top of each system view. All load-bearing reference data — see section 10.

Key Title
mrc MRC strength grade 05
dtr Deep-tendon reflex 04+
plantar Plantar (Babinski) — down/up/asymmetric
beighton Beighton hypermobility 09
atr Scoliometer angle of trunk rotation (refer ≥7°)
rr RR upper limit by age (WHO tachypnea cutoffs)
spo2 SpO2 bands (≥95 / 9294 / <92 / <88)
silverman Silverman-Andersen retraction 010
westley Westley croup 017
murmurGrade Levine 16
pulseAmp 04+
capRefill <2 / 23 / ≥3 sec

SYSTEM_SCALES (line 150) maps system → which scales appear:

{ msk: ['atr','beighton'], neuro: ['mrc','dtr','plantar'],
  resp: ['rr','spo2','silverman','westley'],
  cv:   ['murmurGrade','pulseAmp','capRefill'] }

Sound libraries (lines 252, 286)

RESP_SOUNDS (8 sounds) and CARDIAC_SOUNDS (6 sounds). Each entry has key, src (.ogg / .wav under /audio/), title, where, features, clinical. Rendered as cards with native <audio controls> for cross-platform play/pause/seek. See the project_respiratory_audio.md memory file for which sounds are real recordings vs synth (only "grunting" is still synth at time of writing).

PE_DATA (line 316)

The big tree. Six age groups (newborn / infant / toddler / preschool / school / adolescent) × four systems (msk / neuro / resp / cv). Each system has:

{ overview: '...',
  components: [
    { name: '...',
      steps: [{ label, method, normal }, ...],
      abnormalHints: [...] }
  ] }

Each step is one row in the OSCE checklist with three buttons (Normal / Abnormal / Skip). State stored in state[componentKey-stepIdx] with {component, label, method, normal, status: 'normal'|'abnormal'|null, note?: string}.

Report generation (line 1660)

generate() filters state to only the current system's steps, then POSTs to /api/generate-pe-narrative with {steps, ageGroup, system, patientAge, patientGender, model, format} where format is narrative | list. Renders the response into #pe-narrative-text (contenteditable) plus a summary bar showing normal/abnormal/skipped counts.

Backend (src/routes/peGuide.js, 104 lines)

Single endpoint POST /api/generate-pe-narrative.

  1. Partitions steps into normal / abnormal arrays.
  2. groupByComponent re-groups each partition by step.component for readability.
  3. Builds a structured plain-text payload — one section per partition, one sub-section per component, bullets with method and either expected normal or physician note.
  4. Picks prompt by format: PROMPTS.peGuideNarrative (paragraphs) or PROMPTS.peGuideList (structured list).
  5. Calls callAI([{system: prompt + INJECTION_GUARD}, {user: ... wrapUserText('pe_steps', text) ...}], {model}). The INJECTION_GUARD
    • wrapUserText come from src/utils/promptSafe.js and bracket the exam text with <UNTRUSTED_pe_steps>...</UNTRUSTED_pe_steps> plus a system-level instruction to ignore any instructions inside.
  6. Returns {success, narrative, model, summary: {normal, abnormal, notAssessed}}.
  7. Audits generate_pe_narrative with category: 'clinical'.

8. Vax Schedule + Catchup

Data (pediatricScheduleData.js, 2120 lines)

Single static-data file. Twelve top-level constants (see line 2104 export list):

  • VISIT_AGES (32 visits from prenatal → 21 yr)
  • WELL_VISIT_CODES (ICD-10 + CPT per visit)
  • VACCINES (master list with brand names, dose count, CVX codes)
  • PERIODICITY (the AAP 2025 Bright Futures table — what to do at each visit: vaccines, screening, anticipatory guidance)
  • CATCH_UP_SCHEDULE (per-vaccine minimum age + minimum interval + catchUpNotes for each dose)
  • ANTICIPATORY_GUIDANCE (talking points by age group)
  • AAP_FOOTNOTES (verbatim 2025 periodicity-table footnotes)
  • DYSLIPIDEMIA_SCHEDULE (AAP/NHLBI)
  • NEWBORN_SCREENING
  • GROWTH_REFERENCE (expected gains by age)
  • REFLEXES_REFERENCE (primitive + age of disappearance)
  • BMI_CLASSIFICATION (AAP 2023 + CDC Extended BMI)

Sources cited at the top of the file: AAP/Bright Futures Feb 2025; CDC Child & Adolescent Immunization Schedule 2025.

UI consumers

  • WellVisit tab (public/js/wellVisit.js) reads VISIT_AGES to populate the visit dropdown, PERIODICITY[visitId] to show what's due.
  • renderFullSchedule() (line 663) builds the full immunization matrix for #wv-panel-schedule — vaccine on the y-axis, visit ages on the x-axis, cells marked with dose number (#1, #2, …) or special tokens (annual, catch-up).
  • renderCatchUp() (line 619) iterates CATCH_UP_SCHEDULE and renders one card per vaccine with: minimum age for dose 1, dose series table (dose # / min age / min interval from previous / notes), and catchUpNotes (string or array).
  • The HTML containers are public/components/vaxschedule.html (7 lines, just #wv-panel-schedule) and catchup.html (7 lines, just #wv-panel-catchup).

There is no catch-up "calculator" — the rendering is the reference. Catchup logic that would convert a partial vaccination history into a forward schedule is currently in the user's head, not the code.


9. Milestones

Two distinct milestone systems live in this app:

a) Developmental milestones (this section)

Reference data + AI narrative for well-child visits. Source-of-truth is the developmental_milestones table; static fallback in milestonesData.js.

b) User-tracked milestones

Personal-task feature (e.g., "patient X had abnormal CBC, follow up 3 weeks") — owned by a different module entirely, not covered here.

Frontend (milestones.js, 240 lines)

Gated on first tabChanged to wellvisit. On init:

  1. fetch('/api/milestones-data') — DB-backed; if it returns >0 milestones, use them. If empty or fetch fails, fall back to window.MILESTONES_DATA_STATIC (defined in milestonesData.js, line 8 — about 16 age groups × 5 domains).
  2. populateAgeGroups() fills #ms-age-group dropdown.
  3. On age-group change, renderChecklist() builds one section per domain (Gross Motor / Fine Motor / Language / Social/Emotional / Cognitive). Each milestone is a row with a ✓ / ✗ toggle pair.
  4. state[id] tracks {domain, milestone, status: 'yes' | 'no' | null}.
  5. updateBadges() updates the per-domain Xachieved/Ynot/Z items counter live.

Generation

Two endpoints, both routed through src/routes/milestones.js:

  • POST /api/generate-milestone-narrative — takes the assessed milestones array, partitions into achieved / not-yet-achieved / not-assessed (the not-assessed list is explicitly told to be omitted from the narrative; only assessed items appear in the output). Picks PROMPTS.milestoneNarrative (paragraphs) or PROMPTS.milestoneList. Calls callAI with INJECTION_GUARD + wrapUserText('milestones', ...).
  • POST /api/generate-milestone-summary — takes a previously generated narrative and returns a 3-sentence summary via PROMPTS.milestoneSummary. Capped at 500 max tokens.

Admin endpoints (src/routes/adminMilestones.js, 149 lines)

Full CRUD for the developmental_milestones table, all behind adminMiddleware:

Method Path Purpose
GET /api/admin/milestones list all (filterable by ?age_group=)
GET /api/admin/milestones/meta distinct age_groups + distinct domains
POST /api/admin/milestones create one
PUT /api/admin/milestones/:id update one
DELETE /api/admin/milestones/:id delete one
POST /api/admin/milestones/bulk-import bulk insert with optional clearExisting

Schema: id, age_group, domain, milestone_text, sort_order, updated_at. Bulk-import is how MILESTONES_DATA_STATIC was seeded into the DB.


10. Sacred zones — do not modify without test vectors

These data structures and functions encode validated clinical references. Every one of them was either pulled from a regulatory body or fit against a peer-reviewed validator (peditools, Epic). Changing a single number can shift a borderline case across a decision boundary and produce wrong clinical guidance. Do not touch any of the following without a corresponding test vector and explicit approval.

public/js/calc-math.js

  • LB — Lund-Browder regional %
  • All formula functions (parkland, maintenance421, pramScore, westleyScore, epi*, nrpEpiIV, rsi*, minSBP, ettSize, lundBrowderTBSA)

public/js/calculators.js

  • _htLMS_F_L / _htLMS_F_M / _htLMS_F_S (lines 8991) and the matching _htLMS_M_* arrays — 218 entries each, height LMS
  • _bpCoeff_F_SYS / _bpCoeff_F_DIA / _bpCoeff_M_SYS / _bpCoeff_M_DIA — 99 × 13 each, BCM/Rosner BP coefficients
  • bmiLMS (line 739) — CDC 2000 BMI LMS
  • wfaLMS and the other growth-chart LMS tables in the growth calculator block
  • photoThresholds35..40 and photoThresholds35..38risk — AAP 2022 phototherapy
  • exchangeThresholds35..38 and ..38risk — AAP 2022 exchange
  • bhutaniZones.p95 / p75 / p40 — Bhutani 1999 nomogram
  • RESUS_MEDS doses
  • EQUIP_DATA and VITALS_DATA lookup tables
  • The functions calcHeightPctile, computeBPPercentile, classifyBPFromPercentiles, calcBMIPercentile, valueFromLMS, interpolateLMS, interpolateThreshold, classifyBMI

public/js/bedside/neonatal.js

  • fentonLMS (lines 2037) — preterm growth LMS, peditools-validated (40 5/7 wk male 3070 g → z = 1.42)

public/js/bedside/burns.js

  • LB (lines 1030) — same as calc-math.js. If you update one, update both.

public/js/bedside/sutures.js

  • SITES table — site defaults are clinical recommendations (size, removal day, technique, materialFamily, glueOK)
  • recommend() decision rules — every if branch encodes a clinical judgment (cat/human bites do not get primary closure; >12 h non- face/scalp triggers delayed closure; etc.)

public/js/peGuide.js

  • SCALES — every grading scale (MRC, DTRs, Beighton, Levine, Westley, Silverman, etc.)
  • RESP_SOUNDS and CARDIAC_SOUNDS clinical and features strings (also: don't break the src paths — they map to real audio files)
  • PE_DATA[ageGroup][system] — every step's method and normal text

public/js/pediatricScheduleData.js

  • All twelve exported constants. The whole file is reference data.

public/js/milestonesData.js

  • MILESTONES_DATA_STATIC is the static fallback; the live data lives in the DB. Edit through the admin endpoints, not the static file.

For all of the above: if you really do need to update a value (e.g., peditools releases a new Fenton fit, or AAP issues a 2027 bilirubin update), do it via:

  1. Land a test vector in test/ first (node --test).
  2. Update the data with the change.
  3. Confirm tests pass with the new vectors.
  4. Cross-check at least 3 boundary cases against the upstream source (peditools / Epic / the AAP figure).

11. How to add a new bedside section

Concrete checklist using burns.js and sutures.js as templates.

  1. Create the module: public/js/bedside/<key>.js.

    // ============================================================
    // bedside/<key>.js
    // <Section name> — <one-line description>
    // ============================================================
    
    import { S } from './shared.js';
    
    function calcXxx() {
      var wt = parseFloat(document.getElementById('xxx-weight').value);
      var el = document.getElementById('xxx-result');
      if (!wt) { el.innerHTML = '<p style="color:var(--red);font-size:13px;">Enter weight (kg).</p>'; return; }
      var html = '<div ...header...>...</div>';
      html += S.drugTable(...);
      html += S.ref('Citation here.');
      el.innerHTML = html;
    }
    
    export function init() {
      document.addEventListener('click', function(e) {
        if (e.target.id === 'btn-xxx-calc' || e.target.closest('#btn-xxx-calc')) calcXxx();
      });
    }
    
  2. Register in bedside/index.js:

    import * as xxx from './xxx.js';
    // ... add `xxx,` to the array fed to forEach(m => m.init())
    
  3. Add the key to SECTIONS in bedside/sub-nav.js (line 8):

    var SECTIONS = [..., 'sutures', 'xxx'];
    
  4. Add the pill button + section div in public/components/bedside.html:

    <button class="calc-pill" data-em="xxx"><i class="fas fa-..."></i> Section name</button>
    ...
    <div id="em-xxx" class="em-section" style="display:none;">
      <h4 ...>...</h4>
      <input id="xxx-weight" ...>
      <button id="btn-xxx-calc" class="btn-sm btn-primary">Calculate</button>
      <div id="xxx-result"></div>
    </div>
    
  5. (Optional) If using JSON-backed drug data: add a xxx section to public/data/drugs.json and follow the merge pattern from sedation.js / agitation.js / antiemetics.js / seizure.js (XXX_FALLBACK inline + xxxDrugs() merger).

  6. Cite sources at the bottom of the rendered output via S.ref('...'). Bedside is reference content — every algorithmic recommendation must be traceable.

  7. Test the full flow: sign out → sign in → click Bedside tab → click the new pill → enter weight → click Calculate. Then refresh the page and confirm UIState lands you on the same pill.

  8. Update the SECTIONS list in this doc under section 4's site table or section 3's per-section list as appropriate.

Things to NOT do:

  • Don't import anything that touches window.UIState directly — the sub-nav.js module owns persistence for all bedside pills.
  • Don't put per-section weight in the top #bedside-weight field. Sections own their own weight; the top is estimator-only.
  • Don't rely on $('#xxx').click(...) style direct binding — the bedside HTML is lazy-injected, so any direct binding before injection will silently fail. Always use document.addEventListener + .closest().

12. Why bedside uses ES modules

History: every other JS file in public/js/ is an IIFE ((function() { ... })();) or a bare top-level script. The pattern is consistent — modules talk via window globals and CustomEvent on document. No bundler, no framework, no build step.

The original calculators.js ate the bedside emergencies sections, so it grew to several thousand lines and was difficult to navigate. The "calculators" navigation pill for Bedside was eventually promoted to its own top-level tab (<section id="bedside-tab" data-component="bedside"> at index.html:309). At that point Daniel asked for bedside to be extracted into per-section files — partly for readability, partly as the proof-of-concept for the planned migration to ES modules.

Bedside was chosen because:

  1. Its sections are independent — cardiac doesn't reference seizure.
  2. It has no AI calls and no PHI, so the import surface is small.
  3. Its only shared dependency (S formatting helpers) is a pure namespace, easy to import.
  4. The HTML container is one component (bedside.html), not scattered.

The result is the structure documented here: bedside/index.js is the sole <script type="module"> in index.html. Everything else still loads as IIFE so nothing breaks.

This is the migration target shape referenced in memory/project_migration_checkpoint.md. The next steps (per docs/improvements.md) are:

  1. Split calculators.js (2941 lines, 10 calculators) into calculators/<calc>.js ES modules with a similar index.js registrar.
  2. Move LMS / coefficient tables to data/*.json so the JS is small and the data is easy to diff.
  3. Repeat for peGuide.js (1714 lines) — peGuide/sounds.js, peGuide/scales.js, peGuide/data/<ageGroup>/<system>.js.
  4. Eventually flip the <script defer> load order to <script type="module"> for everything; remove the window._XXX glue.

Until then: bedside is the only ES-module island. New sections under bedside/ should follow the ES-module convention (import / export function init()); new code outside bedside/ should still be IIFE so the loader order keeps working.


File index (absolute paths)

Bedside:

  • /home/danvics/docker/ped-ai/public/js/bedside/index.js
  • /home/danvics/docker/ped-ai/public/js/bedside/shared.js
  • /home/danvics/docker/ped-ai/public/js/bedside/sub-nav.js
  • /home/danvics/docker/ped-ai/public/js/bedside/image-lightbox.js
  • /home/danvics/docker/ped-ai/public/js/bedside/age-weight.js
  • /home/danvics/docker/ped-ai/public/js/bedside/neonatal.js
  • /home/danvics/docker/ped-ai/public/js/bedside/airway.js
  • /home/danvics/docker/ped-ai/public/js/bedside/cardiac.js
  • /home/danvics/docker/ped-ai/public/js/bedside/respiratory.js
  • /home/danvics/docker/ped-ai/public/js/bedside/ventilation.js
  • /home/danvics/docker/ped-ai/public/js/bedside/seizure.js
  • /home/danvics/docker/ped-ai/public/js/bedside/sepsis.js
  • /home/danvics/docker/ped-ai/public/js/bedside/anaphylaxis.js
  • /home/danvics/docker/ped-ai/public/js/bedside/sedation.js
  • /home/danvics/docker/ped-ai/public/js/bedside/agitation.js
  • /home/danvics/docker/ped-ai/public/js/bedside/antiemetics.js
  • /home/danvics/docker/ped-ai/public/js/bedside/antimicrobials.js
  • /home/danvics/docker/ped-ai/public/js/bedside/burns.js
  • /home/danvics/docker/ped-ai/public/js/bedside/toxicology.js
  • /home/danvics/docker/ped-ai/public/js/bedside/trauma.js
  • /home/danvics/docker/ped-ai/public/js/bedside/sutures.js
  • /home/danvics/docker/ped-ai/public/components/bedside.html

Calculators:

  • /home/danvics/docker/ped-ai/public/js/calc-math.js
  • /home/danvics/docker/ped-ai/public/js/calculators.js
  • /home/danvics/docker/ped-ai/public/js/drugs-loader.js
  • /home/danvics/docker/ped-ai/public/components/calculators.html
  • /home/danvics/docker/ped-ai/public/data/drugs.json

PE Guide:

  • /home/danvics/docker/ped-ai/public/js/peGuide.js
  • /home/danvics/docker/ped-ai/src/routes/peGuide.js
  • /home/danvics/docker/ped-ai/public/components/pe-guide.html

Vax Schedule + Catchup:

  • /home/danvics/docker/ped-ai/public/js/pediatricScheduleData.js
  • /home/danvics/docker/ped-ai/public/js/wellVisit.js (consumer)
  • /home/danvics/docker/ped-ai/public/components/vaxschedule.html
  • /home/danvics/docker/ped-ai/public/components/catchup.html

Milestones:

  • /home/danvics/docker/ped-ai/public/js/milestones.js
  • /home/danvics/docker/ped-ai/public/js/milestonesData.js
  • /home/danvics/docker/ped-ai/src/routes/milestones.js
  • /home/danvics/docker/ped-ai/src/routes/adminMilestones.js