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.
58 KiB
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:
- No PHI flows through them. Inputs are weight, age, vital signs,
exam findings — not patient identifiers. Nothing here writes the
encountersornotestables. - 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-narrativeto turn a checklist into prose. - 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 withcrypto.jsAES-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
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:
- On click of any
[data-em]pill, hide every#em-<section>div except the active one. Persistpill.dataset.emtowindow.UIStateunder keybedside.pill. - On
tabChangedtobedside, re-apply the saved pill fromUIState.get('bedside.pill'). Also runs once oninit()for the e2e harness where the DOM is synchronously ready.
The SECTIONS array at line 8 is the master list of pill keys:
['neonatal','airway','cardiac','respiratory','ventilation','seizure',
'sepsis','anaphylaxis','sedation','agitation','antiemetics',
'antimicrobials','burns','toxicology','trauma','sutures']
When you add a new section (see section 11) you must add its key here so the show/hide loop knows about it.
bedside/image-lightbox.js
40 lines. Generic delegator: any element with data-img-src opens the
referenced image in #img-lightbox. Closes on ESC, click on backdrop,
or click on #img-lightbox-close. Used by neonatal.js for the NRP
pathway PNG.
Per-section pattern
Every section module follows the same shape:
import { S } from './shared.js';
function calcXxx() {
var wt = parseFloat(document.getElementById('xxx-weight').value);
var el = document.getElementById('xxx-result');
if (!wt) { el.innerHTML = '<p style="color:var(--red);...">Enter weight (kg).</p>'; return; }
var html = '<div ...header...></div>';
html += S.drugTable(...);
html += S.ref('Citation text here.');
el.innerHTML = html;
}
export function init() {
document.addEventListener('click', function(e) {
if (e.target.id === 'btn-xxx-calc' || e.target.closest('#btn-xxx-calc')) calcXxx();
});
}
Because handlers use document.addEventListener + .closest(), they
keep working when the bedside HTML fragment is injected after the
module has loaded.
3. Per-section deep dives
Each subsection lists: input fields it reads, computation, output container, the algorithm/formula, and the citation it prints.
3.1 age-weight.js — top weight estimator
Not a section, but a banner above all sections. Reads #bedside-age (a
free-text age string) and #bedside-formula (apls / bestguess).
Calls window._parseAgeMonths(raw) (exposed by calculators.js line
57) to turn "3y" / "15 days" / "2y5m" into months, then
window._PED_MATH.estimateWeightFromAgeMonths(months) to compute both
APLS and Best Guess weights. Writes the picked one to #bedside-weight
unless the user has typed in that field (dataset.userTyped flag).
The estimator is estimator-only: per-section weight inputs read themselves; there is intentionally no auto-propagation. Daniel's preference — sections own their own weight to avoid surprise overwrites when navigating between pills.
3.2 neonatal.js — Fenton + Apgar + NRP
Three calculators in one section.
Neonatal assessment (neoAssess, lines 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 formulaZ = ((value/M)^L − 1) / (L · S)with the small-L branchZ = 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:
- 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).
- Induction: Ketamine 1.5 mg/kg, Etomidate 0.3 mg/kg, Propofol 2 mg/kg, Midazolam 0.2 mg/kg.
- 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.
- 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.
- 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.
- 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:
- Target SpO2 table (most kids, bronchiolitis, CLD, preterm, term neonate per NRP ladder).
- Escalation ladder — six step-boxes from low-flow NC → simple mask → NRB → HFNC → NIV (CPAP/BiPAP) → intubate.
- BVM how-to: rate by age, TV 6–8 mL/kg, MR SOPA mnemonic.
- Vent starting settings (mode, TV, rate, PEEP, FiO2, I:E, Ti, plateau <30).
- Inline SVG pressure-time waveform with PIP / Plateau / PEEP /
Ti / Te annotated. Hand-drawn
<path>at line 99. - "Adjusting for gas exchange" troubleshooting table — first lever / second lever for low SpO2, ↑PCO2, ↓PCO2, high peak pressure, auto-PEEP.
- 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.stepBoxes: 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:
- 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.
- 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.
LBtable 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).renderBodyPartsbuilds 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-agechange so the bracket-specific max updates.computeTbsasums region% × involvement%. Live-updates#burn-tbsa-liveon input.calcBurnreads#burn-weight, uses computed TBSA (or the#burn-tbsaoverride). Parkland: 4 mL × kg × %TBSA over 24 h (half in first 8 h from time of burn). Maintenance by Holliday-Segar 4-2-1. Includes a per-region breakdown<details>and the ABA burn-center referral criteria yellow box.
Cites ABLS 2018, Baxter 1968 (Parkland), Lund-Browder 1944.
The same LB matrix is exported from calc-math.js (LB,
LB_BRACKETS, lundBrowderTBSA) for unit testing under Node.
3.15 toxicology.js — Topic-driven
#tox-topic dropdown picks one of: approach / acetaminophen /
opioids / iron / tca / bbccb / benzo / organo /
salicylate / etoh / dialyzable. lookupTox reads #tox-weight
and the topic; renders a different layout per topic.
The approach topic is five S.stepBoxes: stabilize → toxidrome
identification (sympathomimetic, anticholinergic, cholinergic, opioid,
sedative-hypnotic) → decontamination (charcoal 1 g/kg max 50 g; WBI;
no ipecac) → enhanced elimination → antidotes.
Topic-specific tables call out: Rumack-Matthew nomogram for acetaminophen + 21-h NAC IV protocol, naloxone titration "to respiratory effort, not consciousness", iron stages 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)
-
Closure plan:
- Default:
Primary closure. heavyorbite_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_dogANDsite == '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").
- Default:
-
Material: starts at the site's
materialFamily. If monofilament-nonabsorbable AND age islt1or1to5AND removalAvoid ==yes, switches to fast-absorbing gut / Vicryl Rapide so no removal visit is needed. -
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.
-
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."
-
Glue alternative appears only if
s.glueOK && tension !== 'high' && contamination === 'clean'. -
Removal day range is nulled if a non-monofilament material was selected.
-
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:
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..t5per sex), age (5 knotsta1..ta5), and the height×age interactionw = (age − 10) × (height − meanHeight)(5 knotstb1..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.
- Restricted cubic splines built on height (5 knots
- 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
renderBPChartplots 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) and35risk / 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 checkwindow._DRUGSlazily at render time.
/data/drugs.json (versioned 1.0, last_reviewed 2026-04-20) contains
five sections matched to bedside modules: anaphylaxis, sedation,
seizure, agitation, antiemetics. Each entry has at minimum
name, dose_mg_per_kg, max_mg, unit, route, notes,
source. Range-based entries (sedation Ketamine, Midaz IV) use
dose_mg_per_kg_low / _high and max_mg_low / _high. Fixed-dose
entries (Nitrous "50:50 mix") use dose_display.
The bedside consumers (anaphylaxis.js, sedation.js, agitation.js,
antiemetics.js, seizure.js) all follow the same merge pattern:
function xxxDrugs() {
var s = window._DRUGS && window._DRUGS.sections && window._DRUGS.sections.xxx;
if (s && s.drugs && s.drugs.length) {
return s.drugs.map(function(dg) {
var fb = XXX_FALLBACK.filter(function(x) { return x.name === dg.name; })[0] || {};
return Object.assign({}, fb, dg); // JSON wins per-field
});
}
return XXX_FALLBACK;
}
This means:
- The bedside section keeps working even if the JSON fetch fails (CORS,
404, offline) —
XXX_FALLBACKis a hard-coded mirror of the JSON shape. - When the JSON loads, individual fields (
notes,route,max_mg) can be updated centrally without editing 5 JS files. - 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:
{ 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.
- Partitions steps into
normal/abnormalarrays. groupByComponentre-groups each partition bystep.componentfor readability.- Builds a structured plain-text payload — one section per partition,
one sub-section per component, bullets with
methodand eitherexpected normalorphysician note. - Picks prompt by
format:PROMPTS.peGuideNarrative(paragraphs) orPROMPTS.peGuideList(structured list). - Calls
callAI([{system: prompt + INJECTION_GUARD}, {user: ... wrapUserText('pe_steps', text) ...}], {model}). TheINJECTION_GUARDwrapUserTextcome fromsrc/utils/promptSafe.jsand bracket the exam text with<UNTRUSTED_pe_steps>...</UNTRUSTED_pe_steps>plus a system-level instruction to ignore any instructions inside.
- Returns
{success, narrative, model, summary: {normal, abnormal, notAssessed}}. - Audits
generate_pe_narrativewithcategory: '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_SCREENINGGROWTH_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) readsVISIT_AGESto 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) iteratesCATCH_UP_SCHEDULEand renders one card per vaccine with: minimum age for dose 1, dose series table (dose # / min age / min interval from previous / notes), andcatchUpNotes(string or array).- The HTML containers are
public/components/vaxschedule.html(7 lines, just#wv-panel-schedule) andcatchup.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:
fetch('/api/milestones-data')— DB-backed; if it returns >0 milestones, use them. If empty or fetch fails, fall back towindow.MILESTONES_DATA_STATIC(defined inmilestonesData.js, line 8 — about 16 age groups × 5 domains).populateAgeGroups()fills#ms-age-groupdropdown.- 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. state[id]tracks{domain, milestone, status: 'yes' | 'no' | null}.updateBadges()updates the per-domainXachieved/Ynot/Z itemscounter 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). PicksPROMPTS.milestoneNarrative(paragraphs) orPROMPTS.milestoneList. CallscallAIwithINJECTION_GUARD+wrapUserText('milestones', ...).POST /api/generate-milestone-summary— takes a previously generated narrative and returns a 3-sentence summary viaPROMPTS.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 coefficientsbmiLMS(line 739) — CDC 2000 BMI LMSwfaLMSand the other growth-chart LMS tables in thegrowthcalculator blockphotoThresholds35..40andphotoThresholds35..38risk— AAP 2022 phototherapyexchangeThresholds35..38and..38risk— AAP 2022 exchangebhutaniZones.p95 / p75 / p40— Bhutani 1999 nomogramRESUS_MEDSdosesEQUIP_DATAandVITALS_DATAlookup 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
SITEStable — site defaults are clinical recommendations (size, removal day, technique, materialFamily, glueOK)recommend()decision rules — everyifbranch 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_SOUNDSandCARDIAC_SOUNDSclinicalandfeaturesstrings (also: don't break thesrcpaths — they map to real audio files)PE_DATA[ageGroup][system]— every step'smethodandnormaltext
public/js/pediatricScheduleData.js
- All twelve exported constants. The whole file is reference data.
public/js/milestonesData.js
MILESTONES_DATA_STATICis 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:
- Land a test vector in
test/first (node --test). - Update the data with the change.
- Confirm tests pass with the new vectors.
- 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.
-
Create the module:
public/js/bedside/<key>.js.// ============================================================ // bedside/<key>.js // <Section name> — <one-line description> // ============================================================ import { S } from './shared.js'; function calcXxx() { var wt = parseFloat(document.getElementById('xxx-weight').value); var el = document.getElementById('xxx-result'); if (!wt) { el.innerHTML = '<p style="color:var(--red);font-size:13px;">Enter weight (kg).</p>'; return; } var html = '<div ...header...>...</div>'; html += S.drugTable(...); html += S.ref('Citation here.'); el.innerHTML = html; } export function init() { document.addEventListener('click', function(e) { if (e.target.id === 'btn-xxx-calc' || e.target.closest('#btn-xxx-calc')) calcXxx(); }); } -
Register in
bedside/index.js:import * as xxx from './xxx.js'; // ... add `xxx,` to the array fed to forEach(m => m.init()) -
Add the key to
SECTIONSinbedside/sub-nav.js(line 8):var SECTIONS = [..., 'sutures', 'xxx']; -
Add the pill button + section div in
public/components/bedside.html:<button class="calc-pill" data-em="xxx"><i class="fas fa-..."></i> Section name</button> ... <div id="em-xxx" class="em-section" style="display:none;"> <h4 ...>...</h4> <input id="xxx-weight" ...> <button id="btn-xxx-calc" class="btn-sm btn-primary">Calculate</button> <div id="xxx-result"></div> </div> -
(Optional) If using JSON-backed drug data: add a
xxxsection topublic/data/drugs.jsonand follow the merge pattern fromsedation.js/agitation.js/antiemetics.js/seizure.js(XXX_FALLBACKinline +xxxDrugs()merger). -
Cite sources at the bottom of the rendered output via
S.ref('...'). Bedside is reference content — every algorithmic recommendation must be traceable. -
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
UIStatelands you on the same pill. -
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.UIStatedirectly — thesub-nav.jsmodule owns persistence for all bedside pills. - Don't put per-section weight in the top
#bedside-weightfield. 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 usedocument.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:
- Its sections are independent — cardiac doesn't reference seizure.
- It has no AI calls and no PHI, so the import surface is small.
- Its only shared dependency (
Sformatting helpers) is a pure namespace, easy to import. - 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:
- Split
calculators.js(2941 lines, 10 calculators) intocalculators/<calc>.jsES modules with a similarindex.jsregistrar. - Move LMS / coefficient tables to
data/*.jsonso the JS is small and the data is easy to diff. - Repeat for
peGuide.js(1714 lines) —peGuide/sounds.js,peGuide/scales.js,peGuide/data/<ageGroup>/<system>.js. - Eventually flip the
<script defer>load order to<script type="module">for everything; remove thewindow._XXXglue.
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