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

1373 lines
58 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`
```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 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 2410 J/kg, sync 0.512 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.10.2 mg/kg procainamide /
amiodarone. Unstable: sync cardioversion 0.512 J/kg.
- **VFib / pulseless VT** Defib 24→≥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.stepBox`es: 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.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 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:
```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.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:
```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 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:
```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 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`.
```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`