# 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 447–479):

```
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`

```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:

```js
['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:

```js
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 96–159). 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 20–37). Female and male tables, GA 22–42 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 Abramowitz–Stegun normal CDF
  approximation.
- AGA/SGA/LGA classification by percentile.

The Fenton LMS values were re-fit empirically from peditools.org —
see comment block lines 11–17. **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.01–0.03 mg/kg (also as mL of 1:10 000), epi ETT
0.05–0.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.05–0.2 mg/kg/hr, Fent 1–4 mcg/kg/hr,
   Ket 0.5–2 mg/kg/hr, Dex 0.2–1.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 6–8 mL/kg, age-banded rate, PEEP 5,
   FiO2 100% titrate to 92–97%, I:E 1:2 (1:3–4 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.5–1→2 J/kg.
- **Asystole** — CPR + epi q3–5 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.5–1→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). 0–3 Mild, 4–7 Moderate,
8–12 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, 3–5 Moderate, 6–11 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 6–8 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 (2–5 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.05–0.5 mg/kg/hr; Pentobarb gtt 1–5 mg/kg/hr; Propofol gtt 2–5 mg/kg/hr (PRIS warning); Ketamine gtt 1–5 mg/kg/hr |

Drug list lives in two places: `SEIZURE_FALLBACK` (lines 33–41, 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 0–8; Rochester WBC 5–15k bands; the
  Step-by-Step decision tree). **Child**: Ceftriaxone + Vanc ±
  Pip-tazo ± Clinda for TSS.
- First-hour bundle as five `S.stepBox`es: 0–5 recognize, 5–15 access
  + cultures, 15–30 fluids 20 mL/kg, 30–60 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
2–6 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.1–1 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 14–22). The 4–6h 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 15–27) merged
in. Two tables:

1. **Sedation agents** — Ketamine IV 1.5–2 mg/kg + IM 4–5 mg/kg,
   Midaz IV 0.05–0.1 + IN/PO 0.5, Propofol 1 mg/kg, Fentanyl IV
   1 mcg/kg + IN 2 mcg/kg, Nitrous 50:50 mix, Dexmed IN
   2–3 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 12–26) 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 10–18). Drug list:

| Drug | Per-kg | Max | Notes |
|---|---|---|---|
| Ondansetron | 0.15 mg/kg | 8 mg | First-line; weight-band: <15 kg 2 mg, 15–30 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 1–5 / child 5–10 / adol 10–15 / 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.stepBox`es: 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 1–5 + deferoxamine
+ "charcoal does NOT bind iron", TCA bicarb to pH 7.45–7.55 +
QRS widening, BB/CCB high-dose insulin (HIE) protocol +
calcium first, organophosphate atropine "double q3–5 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.5–1 g/kg or 3% saline 3–5 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 (1–10 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 | 3–5 d | ✓ Dermabond ≤4 cm linear, low tension |
| `eyelid_eyebrow` | 6-0 | 3–5 d | ✗ ocular irritation |
| `lip_vermilion` | 6-0 | 3–5 d | ✗ — first stitch must align vermilion border |
| `intraoral` | 4-0 / 5-0 absorbable | n/a | ✗ |
| `ear` | 5-0 | 4–5 d | ✗ |
| `scalp` | 3-0 / 4-0 | 7–10 d | ✓ HAT (hair apposition) for clean linear lacs ≤10 cm |
| `neck` | 5-0 | 5–7 d | ✓ superficial only |
| `trunk` | 4-0 / 3-0 | 7–10 d | ✗ tension too variable |
| `upper_ext` | 4-0 | 7–10 d | ✓ ≤5 cm linear, not over joints |
| `lower_ext` | 4-0 / 3-0 | 10–14 d | ✓ same caveats; not on shin |
| `hand` | 5-0 | 10–14 d | ✗ high tension |
| `foot` | 4-0 / 5-0 | 10–14 d | ✗ |
| `joint_surface` | 4-0 | 10–14 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 3–5 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 25–50% 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:

```bash
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.01–0.03 mg/kg range |
| `rsiKetamine` / `rsiRocuronium` / `rsiSuccinylcholine(weightKg)` | RSI doses with caps and the <10 kg succ rule |
| `minSBP(years)` | 70 + 2 × yr (1–10 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 1–17 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 |
| 5–84th | Healthy Weight |
| 85–94th | Overweight |
| ≥95th, %of95<120 | Obese (Class 1) |
| ≥95th, 120–139% 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 0–60 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 1474–1486. **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 / 36–37 / ≥38 — that table underestimated
thresholds by up to 1.0 mg/dL for 39–41 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 2271–2941 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 2271–2941. 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:

```js
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 0–5 |
| `dtr` | Deep-tendon reflex 0–4+ |
| `plantar` | Plantar (Babinski) — down/up/asymmetric |
| `beighton` | Beighton hypermobility 0–9 |
| `atr` | Scoliometer angle of trunk rotation (refer ≥7°) |
| `rr` | RR upper limit by age (WHO tachypnea cutoffs) |
| `spo2` | SpO2 bands (≥95 / 92–94 / <92 / <88) |
| `silverman` | Silverman-Andersen retraction 0–10 |
| `westley` | Westley croup 0–17 |
| `murmurGrade` | Levine 1–6 |
| `pulseAmp` | 0–4+ |
| `capRefill` | <2 / 2–3 / ≥3 sec |

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

```js
{ 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 89–91) 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 20–37) — preterm growth LMS, peditools-validated
  (40 5/7 wk male 3070 g → z = −1.42)

### `public/js/bedside/burns.js`

- `LB` (lines 10–30) — 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`.

   ```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`**:

   ```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):

   ```js
   var SECTIONS = [..., 'sutures', 'xxx'];
   ```

4. **Add the pill button + section div in `public/components/bedside.html`**:

   ```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`
