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.
1373 lines
58 KiB
Markdown
1373 lines
58 KiB
Markdown
# Bedside, Calculators, PE Guide, Vax Schedule, Milestones
|
||
|
||
Dev-deep walkthrough of the **reference / decision-support** half of the
|
||
PedScribe SPA. These modules differ from the rest of the app in three ways:
|
||
|
||
1. **No PHI** flows through them. Inputs are weight, age, vital signs,
|
||
exam findings — not patient identifiers. Nothing here writes the
|
||
`encounters` or `notes` tables.
|
||
2. **Most of them never call AI.** Bedside, Calculators and the static
|
||
Vax/Catchup tables are pure JS. Only **PE Guide** and **Milestones**
|
||
call `/api/generate-pe-narrative` / `/api/generate-milestone-narrative`
|
||
to turn a checklist into prose.
|
||
3. **Bedside is the one corner of the frontend that uses native ES
|
||
modules.** Everything else is IIFE. See section 12.
|
||
|
||
The general rule: this is the part Daniel uses **at the bedside** in the
|
||
ED / clinic / NICU. Speed and trust matter — every dose string shows the
|
||
per-kg + max so the prescriber can re-verify the math without leaving the
|
||
screen, and every clinical formula has a citation in the same render.
|
||
|
||
---
|
||
|
||
## 1. Overview
|
||
|
||
| Module | File(s) | Loaded as | Talks to backend? |
|
||
|---|---|---|---|
|
||
| Bedside emergencies | `public/js/bedside/*` | `<script type="module">` | No |
|
||
| Calculators | `public/js/calc-math.js` + `public/js/calculators.js` + `public/js/drugs-loader.js` | IIFE (`defer`) | Fetches `/data/drugs.json` once at boot |
|
||
| PE Guide | `public/js/peGuide.js` + `src/routes/peGuide.js` | IIFE (`defer`) | `POST /api/generate-pe-narrative` |
|
||
| Vax Schedule + Catchup | `public/js/pediatricScheduleData.js` consumed by `public/js/wellVisit.js` | IIFE (`defer`) | No |
|
||
| Milestones | `public/js/milestonesData.js` + `public/js/milestones.js` + `src/routes/milestones.js` + `src/routes/adminMilestones.js` | IIFE (`defer`) | `GET /api/milestones-data` (DB-backed, static fallback), `POST /api/generate-milestone-narrative`, `POST /api/generate-milestone-summary` |
|
||
|
||
Reference content vs the rest of the app:
|
||
|
||
- The **clinical-documentation** half of PedScribe (Notes / SOAP / HPI /
|
||
Encounters / SickVisit / WellVisit / Hospital / ED) takes free-text
|
||
input and runs it through `callAI()` to produce structured output. PHI
|
||
is encrypted at rest with `crypto.js` AES-256-GCM.
|
||
- The **reference** half documented here takes structured input
|
||
(weight, age, score components, exam checkboxes) and produces
|
||
rendered HTML. No persistence. No PHI. Refresh the page and it's gone.
|
||
|
||
Loader order in `public/index.html` (see lines 447–479):
|
||
|
||
```
|
||
447 milestonesData.js // static fallback for MILESTONES_DATA_STATIC
|
||
448 pediatricScheduleData.js // VISIT_AGES, PERIODICITY, CATCH_UP_SCHEDULE, ...
|
||
464 milestones.js // wires up wellvisit milestones tab
|
||
465 peGuide.js
|
||
469 wellVisit.js // consumes pediatricScheduleData
|
||
476 calc-math.js // pure math, dual-export node+window
|
||
477 drugs-loader.js // fetches /data/drugs.json
|
||
478 calculators.js // 2941-line UI wiring
|
||
479 bedside/index.js // type="module" ← the only ES-module entrypoint
|
||
```
|
||
|
||
Bedside loads last and as a module so the import graph
|
||
(`bedside/index.js → ./shared.js + each section module`) resolves
|
||
without polluting `window`.
|
||
|
||
---
|
||
|
||
## 2. Bedside architecture
|
||
|
||
Bedside is the single tree under `public/js/bedside/`. 21 files.
|
||
`index.js` is 49 lines and does only two things: import every section
|
||
module and call `init()` on each.
|
||
|
||
### `bedside/index.js`
|
||
|
||
```js
|
||
import './shared.js'; // side-effect: window._EM = S
|
||
import * as ageWeight from './age-weight.js';
|
||
import * as subNav from './sub-nav.js';
|
||
// ... 17 more sections ...
|
||
[ageWeight, subNav, lightbox, neonatal, ..., sutures]
|
||
.forEach(m => { if (m && typeof m.init === 'function') m.init(); });
|
||
```
|
||
|
||
Every section module exports an `init()` that registers a
|
||
`document.addEventListener('click', ...)` handler. They use **event
|
||
delegation** because the bedside HTML is injected lazily on first tab
|
||
activation — by the time the user clicks a button, the section's div
|
||
exists, but the handlers were registered before that.
|
||
|
||
### `bedside/shared.js`
|
||
|
||
42 lines. Exports `S`, a record of pure formatting helpers used by every
|
||
section. Also assigns `window._EM = S` for back-compat — there are no
|
||
known callers of `window._EM` in the current tree, but the assignment is
|
||
kept as a safety net.
|
||
|
||
| Helper | Purpose |
|
||
|---|---|
|
||
| `S.d(wt, perKg, max)` | rounded dose number (no formatting) |
|
||
| `S.dStr(wt, perKg, max, unit)` | formatted dose string with `(perKg/kg, max ...)` footer; appends `· capped` if max hit |
|
||
| `S.drugTable(rows)` | wraps `<tr>` rows in a horizontally-scrollable table with Drug/Dose/Route/Notes columns |
|
||
| `S.drugRow(name, dose, route, notes)` | one `<tr>` |
|
||
| `S.stepBox(color, title, body)` | a coloured left-border card used for pathway steps (NRP, status epilepticus, sepsis bundle) |
|
||
| `S.arrow` | down-arrow separator between step boxes |
|
||
| `S.badge(label, color)` | severity pill |
|
||
| `S.ref(text)` | small grey italic footnote (used for citations at the end of every result) |
|
||
|
||
**Why the dose footer matters**: `S.dStr(30, 0.1, 4)` renders
|
||
`3 mg (0.1 mg/kg, max 4 mg)`. `S.dStr(60, 0.1, 4)` renders
|
||
`4 mg (0.1 mg/kg, max 4 mg · capped)`. The prescriber sees both the
|
||
final answer and the math behind it without leaving the screen.
|
||
|
||
### `bedside/sub-nav.js`
|
||
|
||
44 lines. Owns the pill-row across the top of the Bedside tab. Two
|
||
responsibilities:
|
||
|
||
1. On click of any `[data-em]` pill, hide every `#em-<section>` div
|
||
except the active one. Persist `pill.dataset.em` to
|
||
`window.UIState` under key `bedside.pill`.
|
||
2. On `tabChanged` to `bedside`, re-apply the saved pill from
|
||
`UIState.get('bedside.pill')`. Also runs once on `init()` for the e2e
|
||
harness where the DOM is synchronously ready.
|
||
|
||
The `SECTIONS` array at line 8 is the master list of pill keys:
|
||
|
||
```js
|
||
['neonatal','airway','cardiac','respiratory','ventilation','seizure',
|
||
'sepsis','anaphylaxis','sedation','agitation','antiemetics',
|
||
'antimicrobials','burns','toxicology','trauma','sutures']
|
||
```
|
||
|
||
When you add a new section (see section 11) you must add its key here so
|
||
the show/hide loop knows about it.
|
||
|
||
### `bedside/image-lightbox.js`
|
||
|
||
40 lines. Generic delegator: any element with `data-img-src` opens the
|
||
referenced image in `#img-lightbox`. Closes on ESC, click on backdrop,
|
||
or click on `#img-lightbox-close`. Used by `neonatal.js` for the NRP
|
||
pathway PNG.
|
||
|
||
### Per-section pattern
|
||
|
||
Every section module follows the same shape:
|
||
|
||
```js
|
||
import { S } from './shared.js';
|
||
|
||
function calcXxx() {
|
||
var wt = parseFloat(document.getElementById('xxx-weight').value);
|
||
var el = document.getElementById('xxx-result');
|
||
if (!wt) { el.innerHTML = '<p style="color:var(--red);...">Enter weight (kg).</p>'; return; }
|
||
var html = '<div ...header...></div>';
|
||
html += S.drugTable(...);
|
||
html += S.ref('Citation text here.');
|
||
el.innerHTML = html;
|
||
}
|
||
|
||
export function init() {
|
||
document.addEventListener('click', function(e) {
|
||
if (e.target.id === 'btn-xxx-calc' || e.target.closest('#btn-xxx-calc')) calcXxx();
|
||
});
|
||
}
|
||
```
|
||
|
||
Because handlers use `document.addEventListener` + `.closest()`, they
|
||
keep working when the bedside HTML fragment is injected after the
|
||
module has loaded.
|
||
|
||
---
|
||
|
||
## 3. Per-section deep dives
|
||
|
||
Each subsection lists: input fields it reads, computation, output
|
||
container, the algorithm/formula, and the citation it prints.
|
||
|
||
### 3.1 `age-weight.js` — top weight estimator
|
||
|
||
Not a section, but a banner above all sections. Reads `#bedside-age` (a
|
||
free-text age string) and `#bedside-formula` (`apls` / `bestguess`).
|
||
Calls `window._parseAgeMonths(raw)` (exposed by `calculators.js` line
|
||
57) to turn `"3y"` / `"15 days"` / `"2y5m"` into months, then
|
||
`window._PED_MATH.estimateWeightFromAgeMonths(months)` to compute both
|
||
APLS and Best Guess weights. Writes the picked one to `#bedside-weight`
|
||
unless the user has typed in that field (`dataset.userTyped` flag).
|
||
|
||
The estimator is **estimator-only**: per-section weight inputs read
|
||
themselves; there is intentionally no auto-propagation. Daniel's
|
||
preference — sections own their own weight to avoid surprise overwrites
|
||
when navigating between pills.
|
||
|
||
### 3.2 `neonatal.js` — Fenton + Apgar + NRP
|
||
|
||
Three calculators in one section.
|
||
|
||
**Neonatal assessment** (`neoAssess`, lines 96–159). Reads `#neo-ga-weeks`,
|
||
`#neo-ga-days`, `#neo-weight` (grams), `#neo-sex`. Computes:
|
||
|
||
- GA classification (`classifyGA`): Extremely Preterm < 28 wk, Very
|
||
< 32, Moderate < 34, Late Preterm < 37, Early Term < 39, Full Term
|
||
< 41, Late Term < 42, Post Term ≥ 42.
|
||
- Birth-weight category (`classifyBirthWeight`): ELBW < 1000 g, VLBW <
|
||
1500, LBW < 2500, Normal ≤ 4000, Macrosomia > 4000.
|
||
- Z-score and percentile via the **Fenton 2013 LMS table** (`fentonLMS`,
|
||
lines 20–37). Female and male tables, GA 22–42 wk, each row
|
||
`{L, M, S}`. Linear interpolation between integer GA weeks
|
||
(`interpolateLMS`). Z-score by the canonical LMS formula
|
||
`Z = ((value/M)^L − 1) / (L · S)` with the small-L branch
|
||
`Z = ln(value/M) / S`. Z → percentile via Abramowitz–Stegun normal CDF
|
||
approximation.
|
||
- AGA/SGA/LGA classification by percentile.
|
||
|
||
The Fenton LMS values were re-fit empirically from peditools.org —
|
||
see comment block lines 11–17. **Validated test case**: 40+5/7 wk male
|
||
3070 g → z = −1.42 (matches Epic and peditools to 2 decimals).
|
||
|
||
Renders into `#neo-result`. Cites Fenton TR, Kim JH. *BMC Pediatrics*
|
||
2013;13:59.
|
||
|
||
**NRP pathway** (`renderPathway`, line 163). Static visual pathway built
|
||
from `S.stepBox` calls — Birth → Assess → HR<100/apneic → HR<100 after
|
||
PPV → HR<60 → Epi. Includes a 3-card grid for SpO2 ladder, ETT size by
|
||
weight band, and ETT depth = 6 + weight(kg) cm. Renders into
|
||
`#nrp-pathway-static` exactly once (`dataset.rendered` guard).
|
||
|
||
**NRP drug calc** (`calcNRP`, line 185). Reads `#nrp-weight`. Computes
|
||
epi IV/IO 0.01–0.03 mg/kg (also as mL of 1:10 000), epi ETT
|
||
0.05–0.1 mg/kg, NS 10 mL/kg, D10 2 mL/kg. Renders into `#nrp-result`.
|
||
Includes a yellow note about 1:10 000 vs 1:1000 because they're the
|
||
ones that get mixed up.
|
||
|
||
**Apgar** (`calcApgar`, line 210). Sums `#apgar-{appearance,pulse,grimace,
|
||
activity,respiration}` and routes to one of three guidance blocks
|
||
(≥7 reassuring, ≥4 moderate, <4 severe). Renders into `#apgar-result`.
|
||
|
||
### 3.3 `airway.js` — RSI
|
||
|
||
`calcAirway` reads `#airway-weight`, optional `#airway-age`. Renders
|
||
five tables into `#airway-result`:
|
||
|
||
1. **Pre-medication**: Atropine 0.02 mg/kg (max 1, min 0.1), Lidocaine
|
||
1.5 mg/kg (max 100), Fentanyl 2 mcg/kg (max 150).
|
||
2. **Induction**: Ketamine 1.5 mg/kg, Etomidate 0.3 mg/kg, Propofol
|
||
2 mg/kg, Midazolam 0.2 mg/kg.
|
||
3. **Paralytics**: Roc 1.2 mg/kg (max 100), Succ 2 mg/kg if <10 kg else
|
||
1.5 mg/kg (max 150), Vec 0.1 mg/kg.
|
||
4. **Maintenance gtt**: Midaz 0.05–0.2 mg/kg/hr, Fent 1–4 mcg/kg/hr,
|
||
Ket 0.5–2 mg/kg/hr, Dex 0.2–1.4 mcg/kg/hr.
|
||
5. **Equipment sizing**: ETT uncuffed = age/4 + 4, cuffed = age/4 + 3.5,
|
||
depth at lip = ETT × 3. Blade by age band, LMA by weight band,
|
||
suction = ETT × 2 Fr, NG = ETT × 2, chest tube = ETT × 4.
|
||
6. **Vent starting settings**: TV 6–8 mL/kg, age-banded rate, PEEP 5,
|
||
FiO2 100% titrate to 92–97%, I:E 1:2 (1:3–4 for obstructive).
|
||
|
||
Cites Harriet Lane 23e / PALS 2020.
|
||
|
||
### 3.4 `cardiac.js` — PALS
|
||
|
||
Pill-driven sub-nav inside the section. `[data-cardiac]` buttons for
|
||
`general` / `asystole` / `brady` / `svt` / `vfib` / `vt`. `cardiacShow(kind)`
|
||
reads `#cardiac-weight` and renders the appropriate table.
|
||
|
||
- **General** — full PALS drug list: epi 1:10 000 IV (0.01 mg/kg max 1)
|
||
and ETT (0.1 mg/kg), amiodarone 5 mg/kg (max 300), lidocaine 1 mg/kg,
|
||
atropine 0.02 mg/kg (min 0.1, max 0.5), adenosine 0.1 → 0.2 mg/kg
|
||
(max 6 → 12), bicarb 1 mEq/kg, CaCl 20 mg/kg, Ca-gluc 60 mg/kg,
|
||
MgSO4 50 mg/kg, D10 2 mL/kg, defib 2→4→10 J/kg, sync 0.5–1→2 J/kg.
|
||
- **Asystole** — CPR + epi q3–5 min + reversible-causes grid (H's & T's).
|
||
- **Brady** — Step-box: CPR if HR<60 with poor perfusion, then epi /
|
||
atropine / pacing.
|
||
- **SVT** — Stable: vagal → adenosine 0.1→0.2 mg/kg → procainamide /
|
||
amiodarone. Unstable: sync cardioversion 0.5–1→2 J/kg.
|
||
- **VFib / pulseless VT** — Defib 2→4→≥4 J/kg (max 10), epi after 2nd
|
||
shock, amiodarone 5 mg/kg, lidocaine 1 mg/kg, MgSO4 50 mg/kg.
|
||
- **Stable VT** — Amiodarone or procainamide; sync if it becomes
|
||
unstable.
|
||
|
||
Cites AHA PALS 2020.
|
||
|
||
### 3.5 `respiratory.js` — Asthma + Bronchiolitis + Croup
|
||
|
||
Three sub-pills (`[data-resp]`) inside the section.
|
||
|
||
**Asthma** (`asthmaManagement(severity)`). `[data-asthma-severity]`
|
||
buttons for mild/moderate/severe. Reads `#resp-weight`. Each severity
|
||
renders its own drug table (see file:34, 43, 53 for exact dose
|
||
strings). Severe also includes three side-by-side cards: when to obtain
|
||
ABG (with the "rising PCO2 = impending failure" pearl in red), when to
|
||
intubate (ketamine + roc as the RSI pair), and heliox notes.
|
||
|
||
**PRAM** (`calcPRAM`, line 102). Sums five `#pram-*` selects (SpO2,
|
||
retractions, scalene, air entry, wheeze). 0–3 Mild, 4–7 Moderate,
|
||
8–12 Severe. Note: the same PRAM math also lives in
|
||
`calc-math.js:pramScore` for unit testing.
|
||
|
||
**Croup** (`calcCroup`, line 111). Sums five `#croup-*` selects.
|
||
Westley score: ≤2 Mild, 3–5 Moderate, 6–11 Severe, ≥12 Impending
|
||
failure. Treatment table per band. Cites Westley 1978.
|
||
|
||
**Bronchiolitis** (`calcBronchiolitis`, line 125). Reads age, SpO2,
|
||
hydration, distress dropdowns. Branches to admit-or-discharge. Always
|
||
prints the AAP 2014/2023 "do NOT use" list (albuterol, epi, steroids,
|
||
abx, chest PT) — that reminder is the most useful part of the section.
|
||
|
||
### 3.6 `ventilation.js` — O2 ladder + vent settings
|
||
|
||
Pure reference, no branching on weight. `showVent` (`#btn-vent-show`)
|
||
renders into `#vent-result`:
|
||
|
||
1. Target SpO2 table (most kids, bronchiolitis, CLD, preterm, term
|
||
neonate per NRP ladder).
|
||
2. **Escalation ladder** — six step-boxes from low-flow NC →
|
||
simple mask → NRB → HFNC → NIV (CPAP/BiPAP) → intubate.
|
||
3. BVM how-to: rate by age, TV 6–8 mL/kg, MR SOPA mnemonic.
|
||
4. Vent starting settings (mode, TV, rate, PEEP, FiO2, I:E, Ti, plateau
|
||
<30).
|
||
5. **Inline SVG pressure-time waveform** with PIP / Plateau / PEEP /
|
||
Ti / Te annotated. Hand-drawn `<path>` at line 99.
|
||
6. "Adjusting for gas exchange" troubleshooting table — first lever /
|
||
second lever for low SpO2, ↑PCO2, ↓PCO2, high peak pressure,
|
||
auto-PEEP.
|
||
7. Mental-model summary: oxygenation = FiO2+PEEP; ventilation = rate+TV.
|
||
|
||
Cites AAP / PALS / AARC.
|
||
|
||
### 3.7 `seizure.js` — Status epilepticus pathway
|
||
|
||
`calcSeizure` reads `#seizure-weight` and emits a time-anchored visual
|
||
pathway via the local `stepRow(time, color, title, body)` helper. Each
|
||
row is a colored time-pill (0 / 5 / 10 / 20 / 30 / 40 min) connected by
|
||
a vertical line to a step card.
|
||
|
||
| Time | Step | Drugs |
|
||
|---|---|---|
|
||
| 0 | Stabilize ABCs, glucose, labs | D10W if BG<60 (2–5 mL/kg) |
|
||
| 5 | 1st benzo | Lorazepam 0.1 mg/kg IV (max 4); Midaz 0.2 mg/kg IM/IN/buccal (max 10); Diaz 0.5 mg/kg PR (max 20) |
|
||
| 10 | 2nd benzo (same agent) | — |
|
||
| 20 | 2nd-line | Levetiracetam 60 mg/kg (max 4500) preferred; Fosphenytoin 20 mg/kg PE (max 1500); Valproate 40 mg/kg (max 3000); Phenobarb 20 mg/kg (max 1000) — last choice |
|
||
| 30 | 2nd 2nd-line OR refractory | — |
|
||
| 40 | Refractory: intubate + cEEG | Midaz gtt 0.05–0.5 mg/kg/hr; Pentobarb gtt 1–5 mg/kg/hr; Propofol gtt 2–5 mg/kg/hr (PRIS warning); Ketamine gtt 1–5 mg/kg/hr |
|
||
|
||
Drug list lives in two places: `SEIZURE_FALLBACK` (lines 33–41, inline
|
||
fallback) and `window._DRUGS.sections.seizure` from `/data/drugs.json`.
|
||
`seizureDrugs()` merges them — JSON wins per-field, fallback fills the
|
||
gaps. Refractory-infusion ranges are kept inline because the JSON
|
||
schema doesn't model `bolus → gtt range` cleanly.
|
||
|
||
Key-points box at the bottom encodes the ESETT trial finding
|
||
(levetiracetam = fosphenytoin = valproate). Cites AES 2016 + Kapur 2019
|
||
NEJM.
|
||
|
||
### 3.8 `sepsis.js` — Phoenix + febrile-infant rules + first-hour bundle
|
||
|
||
`showSepsis` reads `#sepsis-weight`, `#sepsis-age` (`neonate` / `infant` /
|
||
`child`).
|
||
|
||
- Definition box: **Phoenix 2024 (JAMA)** sepsis criteria — score ≥2
|
||
organ-dysfunction. Notes that previous SIRS-based criteria
|
||
(Goldstein 2005) are now superseded.
|
||
- Red-flags grid.
|
||
- Age-specific empiric abx tables. **Neonate**: Amp 100 mg/kg + Gent
|
||
4 mg/kg ± Cefotaxime + Acyclovir 20 mg/kg. **Infant**: Ceftriaxone
|
||
75 mg/kg ± Amp ± Vanc ± Acyclovir, plus an embedded
|
||
`<details>` block with **four validated febrile-infant rules** —
|
||
PECARN 2019, Aronson 2019, Rochester 1994, Step-by-Step 2016. Each
|
||
has its own colored card with the actual cutoffs (PECARN ANC ≤4090,
|
||
procal ≤0.5; Aronson scoring 0–8; Rochester WBC 5–15k bands; the
|
||
Step-by-Step decision tree). **Child**: Ceftriaxone + Vanc ±
|
||
Pip-tazo ± Clinda for TSS.
|
||
- First-hour bundle as five `S.stepBox`es: 0–5 recognize, 5–15 access
|
||
+ cultures, 15–30 fluids 20 mL/kg, 30–60 abx, >60 vasopressors
|
||
(epi for cold shock, norepi for warm) + stress-dose hydrocortisone
|
||
2 mg/kg.
|
||
- Resuscitation targets list.
|
||
|
||
Cites Schlapbach JAMA 2024, SSC Peds 2020, AAP.
|
||
|
||
### 3.9 `anaphylaxis.js`
|
||
|
||
`calcAnaphylaxis` reads `#anaph-weight`. Step 1 box is **always
|
||
epinephrine first**: 1:1000 (1 mg/mL) 0.01 mg/kg IM (max 0.5 mg) to
|
||
lateral thigh, with auto-injector recommendation by weight (<10 kg
|
||
draw manually, ≤25 kg EpiPen Jr 0.15 mg, else EpiPen 0.3 mg). Steps
|
||
2–6 in a single combined table: position, O2, NS 20 mL/kg, DPH
|
||
1.25 mg/kg, ranitidine, dex 0.6 mg/kg or methylpred 2 mg/kg. Refractory
|
||
section: epi gtt 0.1–1 mcg/kg/min, glucagon 0.02 mg/kg for
|
||
beta-blocker patients.
|
||
|
||
Drug doses are looked up via `anaphDrug(name)` which prefers
|
||
`window._DRUGS.sections.anaphylaxis` and falls back to
|
||
`ANAPH_FALLBACK` (lines 14–22). The 4–6h observation note for biphasic
|
||
reactions is the load-bearing teaching point. Cites ASCIA 2021, WAO
|
||
2020, AAP/ACAAI.
|
||
|
||
### 3.10 `sedation.js` — Procedural sedation
|
||
|
||
JSON-backed via `sedDrugs()` with `SED_FALLBACK` (lines 15–27) merged
|
||
in. Two tables:
|
||
|
||
1. **Sedation agents** — Ketamine IV 1.5–2 mg/kg + IM 4–5 mg/kg,
|
||
Midaz IV 0.05–0.1 + IN/PO 0.5, Propofol 1 mg/kg, Fentanyl IV
|
||
1 mcg/kg + IN 2 mcg/kg, Nitrous 50:50 mix, Dexmed IN
|
||
2–3 mcg/kg.
|
||
2. **Reversal agents** — Naloxone 0.1 mg/kg IV/IM/IN (max 2),
|
||
Flumazenil 0.01 mg/kg (max 0.2 single, 1 mg total) with seizure
|
||
warning.
|
||
|
||
`sedDoseCell(wt, dg)` handles three dose shapes: low/high range,
|
||
single, or fixed `dose_display`. Pre-sedation checklist box at the
|
||
bottom lists NPO, monitoring (pulse ox + capnography + BP),
|
||
resuscitation equipment, IV access. Cites AAP sedation guideline + ASA.
|
||
|
||
### 3.11 `agitation.js` — Step 1/2/3
|
||
|
||
`AGIT_FALLBACK` (lines 12–26) split by `step: 2` (PO/IN —
|
||
cooperative) and `step: 3` (IM/IV — severe).
|
||
|
||
- Step 1: non-pharm only (rule out hypoglycemia, hypoxia, pain, ↑ICP,
|
||
toxic, infection).
|
||
- Step 2: Lorazepam 0.05 mg/kg PO (max 2), Midaz 0.5 mg/kg PO or
|
||
0.3 mg/kg IN, Olanzapine ODT (weight-banded), DPH 1 mg/kg.
|
||
- Step 3: Midaz 0.1 mg/kg IV/IM, Lorazepam 0.1 mg/kg, Haloperidol
|
||
(avoid <3 yr; QT/EPS/NMS warning; weight-band dose), Olanzapine IM
|
||
(avoid benzo combo for resp depression), Ketamine 4 mg/kg IM as
|
||
rescue, Droperidol 0.05 mg/kg with ECG.
|
||
|
||
`agitRowFor(wt, dg)` handles the weight-conditional dose strings via
|
||
`dose_display_if_under_30kg` / `_30kg_or_more` and the "prefix = X mg"
|
||
formatting for Droperidol. Cites AAP + ACEP.
|
||
|
||
### 3.12 `antiemetics.js`
|
||
|
||
JSON-backed via `EMET_FALLBACK` (lines 10–18). Drug list:
|
||
|
||
| Drug | Per-kg | Max | Notes |
|
||
|---|---|---|---|
|
||
| Ondansetron | 0.15 mg/kg | 8 mg | First-line; weight-band: <15 kg 2 mg, 15–30 kg 4 mg, ≥30 kg 8 mg; QT |
|
||
| Metoclopramide | 0.15 mg/kg | 10 mg | Give with DPH for EPS prevention |
|
||
| Dimenhydrinate | 1.25 mg/kg | 50 mg | ≥2 yr |
|
||
| DPH | 1 mg/kg | 50 mg | Sedating adjunct |
|
||
| Promethazine | 0.25 mg/kg | 25 mg | **CONTRAINDICATED <2 yr** |
|
||
| Dex | 0.15 mg/kg | 10 mg | Adjunct, esp chemo |
|
||
| Scopolamine | fixed 1.5 mg patch | — | ≥12 yr; motion sickness |
|
||
|
||
`emetRows(wt, drugs)` renders Ondansetron with both the weight-band
|
||
dose AND the per-kg dose so the prescriber can pick. Pearls box at the
|
||
bottom flags QT-stacking risk. Cites AAP / WHO / Lexicomp.
|
||
|
||
### 3.13 `antimicrobials.js` — Empiric by age × infection
|
||
|
||
The only section that does no dose math. `REG[age][infection]` is a
|
||
3 × 9 matrix of `{first, alt, duration, notes}` strings. Age =
|
||
`neonate` / `infant` / `child`. Infection = `sepsis / meningitis / pna /
|
||
uti / skin / ent / neutropenic / ic / bone`. `lookupAbx` reads
|
||
`#abx-age`, `#abx-infection`, looks up `REG[age][inf]` and renders four
|
||
boxes: First-line (green), Alternative/add, Duration, Notes (yellow).
|
||
Cites Red Book / IDSA / Harriet Lane.
|
||
|
||
### 3.14 `burns.js` — Lund-Browder + Parkland
|
||
|
||
The most interactive section.
|
||
|
||
- `LB` table at line 10 — 19 body regions × 5 age brackets (infant /
|
||
young 1–5 / child 5–10 / adol 10–15 / adult >15). Head shrinks
|
||
18→7%, thigh grows 5.5→9.5% with age — those are the only
|
||
age-sensitive rows (`ageSensitive: true`).
|
||
- `renderBodyParts` builds an input grid with one number field per
|
||
region. Each input shows the region's max % for the selected age in
|
||
parentheses. Re-renders on `#burn-age` change so the bracket-specific
|
||
max updates.
|
||
- `computeTbsa` sums region% × involvement%. Live-updates
|
||
`#burn-tbsa-live` on input.
|
||
- `calcBurn` reads `#burn-weight`, uses computed TBSA (or the
|
||
`#burn-tbsa` override). **Parkland**: 4 mL × kg × %TBSA over 24 h
|
||
(half in first 8 h from time of burn). **Maintenance** by
|
||
Holliday-Segar 4-2-1. Includes a per-region breakdown
|
||
`<details>` and the ABA burn-center referral criteria yellow box.
|
||
|
||
Cites ABLS 2018, Baxter 1968 (Parkland), Lund-Browder 1944.
|
||
|
||
The same LB matrix is exported from `calc-math.js` (`LB`,
|
||
`LB_BRACKETS`, `lundBrowderTBSA`) for unit testing under Node.
|
||
|
||
### 3.15 `toxicology.js` — Topic-driven
|
||
|
||
`#tox-topic` dropdown picks one of: `approach` / `acetaminophen` /
|
||
`opioids` / `iron` / `tca` / `bbccb` / `benzo` / `organo` /
|
||
`salicylate` / `etoh` / `dialyzable`. `lookupTox` reads `#tox-weight`
|
||
and the topic; renders a different layout per topic.
|
||
|
||
The `approach` topic is five `S.stepBox`es: stabilize → toxidrome
|
||
identification (sympathomimetic, anticholinergic, cholinergic, opioid,
|
||
sedative-hypnotic) → decontamination (charcoal 1 g/kg max 50 g; WBI;
|
||
no ipecac) → enhanced elimination → antidotes.
|
||
|
||
Topic-specific tables call out: Rumack-Matthew nomogram for
|
||
acetaminophen + 21-h NAC IV protocol, naloxone titration "to
|
||
respiratory effort, not consciousness", iron stages 1–5 + deferoxamine
|
||
+ "charcoal does NOT bind iron", TCA bicarb to pH 7.45–7.55 +
|
||
QRS widening, BB/CCB high-dose insulin (HIE) protocol +
|
||
calcium first, organophosphate atropine "double q3–5 min until
|
||
bronchial secretions dry" + 2-PAM, salicylate urinary alkalinization to
|
||
urine pH >7.5, fomepizole for toxic alcohols, ISTUMBLE mnemonic for
|
||
dialyzable drugs.
|
||
|
||
Always cites Poison Control 1-800-222-1222 + Goldfrank's + EXTRIP.
|
||
|
||
### 3.16 `trauma.js` — ABCDE + MTP
|
||
|
||
`showTrauma` reads `#trauma-weight`. Renders five ABCDE step-boxes
|
||
(A airway+c-spine, B breathing, C circulation 20 mL/kg, D disability +
|
||
ICP rescue with mannitol 0.5–1 g/kg or 3% saline 3–5 mL/kg, E
|
||
exposure). Then:
|
||
|
||
- **MTP** table: 1:1:1 ratio activated at ≥40 mL/kg or ongoing
|
||
hemorrhage; TXA 15 mg/kg (max 1 g) within 3 h then 2 mg/kg/hr × 8 h
|
||
(CRASH-2); calcium per unit citrated blood.
|
||
- C-spine clearance reminder.
|
||
- Pediatric shock signs box — emphasizes hypotension is **late**,
|
||
minimum SBP = 70 + 2×age (1–10 yr).
|
||
- Secondary-survey AMPLE mnemonic.
|
||
|
||
Cites ATLS 10e, PALS 2020, PECARN c-spine, CRASH-2.
|
||
|
||
### 3.17 `sutures.js` — covered in detail in section 4
|
||
|
||
---
|
||
|
||
## 4. Suture selector deep-dive
|
||
|
||
Newest Bedside section (435 lines, last commit 2026-04-26 per the
|
||
ls listing). Decision-engine model rather than a calculator — given
|
||
inputs, it returns a structured recommendation.
|
||
|
||
### Inputs
|
||
|
||
Read by `readInput()` (line 253):
|
||
|
||
| Field | DOM id | Values |
|
||
|---|---|---|
|
||
| Site | `sut-site` | one of 15 (see below) |
|
||
| Age | `sut-age` | `lt1` / `1to5` / `school` / `adolescent` |
|
||
| Tension | `sut-tension` | `low` / `moderate` / `high` |
|
||
| Cosmetic priority | `sut-cosmetic` | `yes` / `no` |
|
||
| Contamination | `sut-contam` | `clean` / `heavy` / `bite_dog` / `bite_cat` / `bite_human` / `bite_other` |
|
||
| Hours since injury | `sut-hours` | `lt12` / `>12` |
|
||
| Avoid removal | `sut-removal-avoid` | `yes` / `no` |
|
||
|
||
### 15 anatomic sites
|
||
|
||
Defined in the `SITES` table (line 19). Each entry carries a
|
||
`materialFamily` (`monofilament-nonabsorbable` / `absorbable-chromic-or-vicryl`),
|
||
default `sutureSize`, default `removalDays`, `glueOK` boolean +
|
||
`glueNote`, default `techniqueDefault`, and free-text `notes`.
|
||
|
||
| Site | Default size | Removal | Glue OK |
|
||
|---|---|---|---|
|
||
| `face` | 6-0 | 3–5 d | ✓ Dermabond ≤4 cm linear, low tension |
|
||
| `eyelid_eyebrow` | 6-0 | 3–5 d | ✗ ocular irritation |
|
||
| `lip_vermilion` | 6-0 | 3–5 d | ✗ — first stitch must align vermilion border |
|
||
| `intraoral` | 4-0 / 5-0 absorbable | n/a | ✗ |
|
||
| `ear` | 5-0 | 4–5 d | ✗ |
|
||
| `scalp` | 3-0 / 4-0 | 7–10 d | ✓ HAT (hair apposition) for clean linear lacs ≤10 cm |
|
||
| `neck` | 5-0 | 5–7 d | ✓ superficial only |
|
||
| `trunk` | 4-0 / 3-0 | 7–10 d | ✗ tension too variable |
|
||
| `upper_ext` | 4-0 | 7–10 d | ✓ ≤5 cm linear, not over joints |
|
||
| `lower_ext` | 4-0 / 3-0 | 10–14 d | ✓ same caveats; not on shin |
|
||
| `hand` | 5-0 | 10–14 d | ✗ high tension |
|
||
| `foot` | 4-0 / 5-0 | 10–14 d | ✗ |
|
||
| `joint_surface` | 4-0 | 10–14 d | ✗ flexion separates the bond |
|
||
| `genitalia` | 5-0 / 4-0 absorbable | n/a | ✗ |
|
||
| `fingertip` | 6-0 absorbable | n/a | ✗ |
|
||
|
||
### Decision logic (`recommend(input)`, line 173)
|
||
|
||
1. **Closure plan**:
|
||
- Default: `Primary closure`.
|
||
- `heavy` or `bite_other`: `Consider DELAYED primary closure
|
||
(cleanse, pack, reassess in 3–5 d)` + warning.
|
||
- `bite_human` / `bite_cat`: `Generally DO NOT close primarily` +
|
||
warning naming the organism (Pasteurella for cat, Eikenella for
|
||
human) and prescribing amox-clav prophylaxis.
|
||
- `bite_dog` AND `site == 'hand'`: warning that many EPs do not
|
||
close primarily; loose approximation only.
|
||
- `hours == '>12'` AND site is not face/scalp: switches to delayed
|
||
primary closure with a soft note ("judgment-based; clean linear
|
||
wounds in well-perfused areas can still be closed").
|
||
|
||
2. **Material**: starts at the site's `materialFamily`. If
|
||
monofilament-nonabsorbable AND age is `lt1` or `1to5` AND
|
||
removalAvoid == `yes`, switches to fast-absorbing gut / Vicryl
|
||
Rapide so no removal visit is needed.
|
||
|
||
3. **Size adjustments**:
|
||
- High tension bumps size up one (3-0 → larger needle: 4-0→3-0,
|
||
5-0→4-0, 6-0→5-0).
|
||
- Cosmetic + low tension on a 4-0 site bumps it down to 5-0.
|
||
|
||
4. **Technique**: defaults from the site. High tension promotes to
|
||
vertical mattress (or simple interrupted with deep dermal absorbable
|
||
layer). Cosmetic + low tension on face/eyelid suggests subcuticular
|
||
"if skill permits."
|
||
|
||
5. **Glue alternative** appears only if `s.glueOK && tension !== 'high'
|
||
&& contamination === 'clean'`.
|
||
|
||
6. **Removal day range** is nulled if a non-monofilament material was
|
||
selected.
|
||
|
||
7. **Tetanus** string is the same for every result: Tdap if last
|
||
booster >5 y for tetanus-prone (contaminated, deep, crush, burn,
|
||
bite); >10 y for clean. TIG if unimmunized + tetanus-prone.
|
||
|
||
### Special cases worth highlighting
|
||
|
||
- **Subungual hematoma trephination threshold** — fingertip notes:
|
||
trephinate "especially if 25–50% or more of the nail" provided the
|
||
nail is intact and there's no displaced distal-phalanx fracture.
|
||
- **Lip vermilion alignment** — site notes: "vermilion misalignment as
|
||
small as 1 mm is visible" + close orbicularis oris with 5-0
|
||
absorbable first; intraoral side with 4-0 chromic / Vicryl Rapide.
|
||
- **HAT on scalp** — twist hair across the wound and secure with a
|
||
drop of glue, requires hair ≥3 cm.
|
||
- **Plantar puncture** — foot notes: do NOT close primarily; consider
|
||
Pseudomonas coverage if shoe-puncture.
|
||
|
||
### References printed inline
|
||
|
||
Roberts & Hedges *Clinical Procedures in Emergency Medicine* 7e (2019)
|
||
Ch. 36 · Fleisher & Ludwig *Pediatric Emergency Medicine* 8e (2021) ·
|
||
AAP Section on Emergency Medicine — Pediatric laceration repair ·
|
||
UpToDate Pope JV — Skin laceration repair with sutures.
|
||
|
||
Render attaches to `change` events on any `#sut-*` field so the result
|
||
updates live as the user changes any input.
|
||
|
||
---
|
||
|
||
## 5. Calculators (`calc-math.js` + `calculators.js`)
|
||
|
||
The two-file split is deliberate.
|
||
|
||
### `calc-math.js` (207 lines) — pure math, dual-exported
|
||
|
||
IIFE that builds an `API` object and exposes it on `window._PED_MATH`
|
||
**and** on `module.exports`. The `module.exports` half is what lets you
|
||
unit-test the formulas under Node:
|
||
|
||
```bash
|
||
node --test test/
|
||
```
|
||
|
||
Functions exposed:
|
||
|
||
| Function | Purpose |
|
||
|---|---|
|
||
| `roundTo(v, places)` | rounding utility |
|
||
| `estimateWeightFromAgeMonths(months)` | APLS + Best Guess; returns picked weight, label, and `all: {apls, bestGuess}` |
|
||
| `parkland(weightKg, tbsaPct)` | total mL, first-8-h split, hourly rates |
|
||
| `maintenance421(weightKg)` | Holliday-Segar |
|
||
| `pramScore(spo2, retractions, scalene, airEntry, wheeze)` | total + severity |
|
||
| `westleyScore(...)` | total + severity |
|
||
| `epiAnaphylaxisIM(weightKg)` | 0.01 mg/kg cap 0.5 mg, with 1:1000 volume |
|
||
| `epiArrestIV(weightKg)` | 0.01 mg/kg cap 1 mg, with 1:10 000 volume |
|
||
| `nrpEpiIV(weightKg)` | 0.01–0.03 mg/kg range |
|
||
| `rsiKetamine` / `rsiRocuronium` / `rsiSuccinylcholine(weightKg)` | RSI doses with caps and the <10 kg succ rule |
|
||
| `minSBP(years)` | 70 + 2 × yr (1–10 yr); 70 if <1; 90 if >10 |
|
||
| `ettSize(ageYears)` | uncuffed = age/4+4, cuffed = age/4+3.5, depth = uncuffed × 3 |
|
||
| `lundBrowderTBSA(ageBracket, involvements)` | sums regional %; same `LB` table as `bedside/burns.js` |
|
||
|
||
Why does this exist alongside the bedside section files which already
|
||
do the same math? Because the bedside files render HTML strings, which
|
||
can't be unit-tested. `calc-math.js` is the testable layer and the
|
||
canonical source. When the two diverge it's a bug in the bedside file.
|
||
|
||
### `calculators.js` (2941 lines) — UI wiring for the Calculators tab
|
||
|
||
Single IIFE. Initialises on first `tabChanged` to `calculators` (line
|
||
7). Top-level pill nav (line 60) toggles `.calc-panel` divs.
|
||
|
||
Calculators in `public/components/calculators.html` (10 pills):
|
||
|
||
```
|
||
data-calc="bp" / "bmi" / "growth" / "bili" / "vitals"
|
||
/ "bsa" / "dose" / "resus" / "gcs" / "equipment"
|
||
```
|
||
|
||
#### 5.1 BP percentile (line 83) — AAP 2017 + Rosner LMS splines
|
||
|
||
The most numerically intensive calculator in the app. Computes the
|
||
exact percentile for systolic and diastolic BP given age, sex, and
|
||
height — no lookup tables, regression-derived.
|
||
|
||
- **Height percentile** uses a Z calculation from the 218-entry
|
||
`_htLMS_F_*` and `_htLMS_M_*` arrays (one entry per month from age 24
|
||
to 240 months). LMS formula: `Z = ((ht/M)^L − 1) / (L · S)`. Index =
|
||
`round(age × 12) − 24`, clamped to `[0, 217]`.
|
||
- **BP regression** (`computeBPPercentile`, line 514) implements the
|
||
BCM/Rosner quantile-spline model:
|
||
- Restricted cubic splines built on height (5 knots `t1..t5` per
|
||
sex), age (5 knots `ta1..ta5`), and the height×age interaction
|
||
`w = (age − 10) × (height − meanHeight)` (5 knots `tb1..tb5`).
|
||
- Per percentile (1..99) the predicted BP is a 13-term linear
|
||
combination using coefficient row `coeff[i]` from
|
||
`_bpCoeff_{F|M}_{SYS|DIA}`. Each coefficient table is 99 × 13 (see
|
||
line 97 onwards for the female systolic table — 99 rows).
|
||
- Patient's percentile = the percentile whose predicted value is
|
||
closest to the entered BP.
|
||
- **Classification** (`classifyBPFromPercentiles`, line 578) per AAP
|
||
2017:
|
||
- Age ≥13: absolute thresholds (120/80 elevated, 130/80 stage 1,
|
||
140/90 stage 2).
|
||
- Age 1–<13: percentile thresholds with absolute caps (≥90th elevated,
|
||
≥95th stage 1, ≥95+12 stage 2). Overall = max(sys, dia).
|
||
- Render (line 617) shows classification badge, sys/dia percentiles,
|
||
height percentile, and reference 50th / 90th / 95th BPs at this
|
||
child's height + sex. Then `renderBPChart` plots Chart.js curves of
|
||
the three reference percentiles across ages 1–17 with the patient as
|
||
a dot.
|
||
|
||
The `_bpCoeff_*` arrays are **load-bearing reference data**. See
|
||
section 10.
|
||
|
||
#### 5.2 BMI percentile (line 734) — CDC 2000
|
||
|
||
`bmiLMS` (line 739) — every-3-month LMS table for both sexes from 24
|
||
to 240 months. `interpolateLMS` linearly interpolates between
|
||
neighbouring rows. `calcBMIPercentile` returns Z + percentile.
|
||
|
||
`classifyBMI` (line 1000) implements the AAP 2023 "Class 1 / 2 / 3
|
||
obesity" extension on top of percentile classes:
|
||
|
||
| Condition | Class |
|
||
|---|---|
|
||
| <5th | Underweight |
|
||
| 5–84th | Healthy Weight |
|
||
| 85–94th | Overweight |
|
||
| ≥95th, %of95<120 | Obese (Class 1) |
|
||
| ≥95th, 120–139% of 95 | Class 2 Severe |
|
||
| ≥95th, ≥140% of 95 | Class 3 Severe |
|
||
|
||
`%of95 = bmi / bmi95 × 100` where `bmi95` is the LMS-derived 95th
|
||
percentile BMI for this age/sex. Render shows BMI, percentile,
|
||
Z-score, and (if ≥85th) the % of 95th and the 95th-BMI.
|
||
|
||
#### 5.3 Growth charts (line 1143) — WHO + CDC + Fenton
|
||
|
||
`wfaLMS` (WHO weight-for-age 0–60 mo) at line 1150 plus parallel
|
||
tables for length, head circumference, height, weight-2-20, etc.
|
||
`renderGrowthChart` uses Chart.js with `generatePercentileCurves` to
|
||
draw 3rd / 10th / 25th / 50th / 75th / 90th / 97th LMS curves and
|
||
overlay the patient point. `calcMidParentalHeight` (line 992) implements
|
||
Tanner's mid-parental height (boys: ((mom + dad + 13) / 2); girls:
|
||
((mom + dad − 13) / 2); ±8.5 cm target range).
|
||
|
||
#### 5.4 Bilirubin — AAP 2022 Phototherapy + Bhutani (line 1408)
|
||
|
||
The phototherapy and exchange thresholds came directly from the
|
||
peditools.org/bili2022 API — see comment block lines 1474–1486. **1190
|
||
values cross-validated 1:1 against peditools (RMSE = 0)**.
|
||
|
||
Per-week tables for phototherapy:
|
||
|
||
- No-risk: `photoThresholds35 / 36 / 37 / 38 / 39 / 40` (40 also covers
|
||
41+).
|
||
- With-risk: `photoThresholds35risk / 36 / 37 / 38` (38+ identical per
|
||
AAP 2022).
|
||
|
||
Per-week tables for exchange transfusion:
|
||
|
||
- `exchangeThresholds35 / 36 / 37 / 38` (38+ identical) and
|
||
`35risk / 36 / 37 / 38risk`.
|
||
|
||
The keys are integer hours-of-life (12 to 96). `interpolateThreshold`
|
||
linearly interpolates between adjacent hours. The previous
|
||
implementation grouped 35 / 36–37 / ≥38 — that table underestimated
|
||
thresholds by up to 1.0 mg/dL for 39–41 wk infants and could miss
|
||
phototherapy indications. **Do not regress to bucketed tables.**
|
||
|
||
`renderBiliChart` plots the phototherapy threshold (red, filled below)
|
||
+ exchange threshold (dark red, dashed) + patient as a scatter point.
|
||
|
||
**Bhutani nomogram** (line 1644) — `bhutaniZones.p95 / p75 / p40`
|
||
hour-specific risk percentile boundaries for ≥35 wk GA infants.
|
||
Digitised from Figure 2 of Bhutani et al., *Pediatrics* 1999;103(1):6-14
|
||
via the codingace.net JS arrays + cross-checked against the AAP 2004
|
||
CPG reproduction. Zone semantics:
|
||
|
||
- TSB ≥ p95 → High-risk
|
||
- p75 ≤ TSB < p95 → High-intermediate
|
||
- p40 ≤ TSB < p75 → Low-intermediate
|
||
- TSB < p40 → Low-risk
|
||
|
||
The Bhutani tab is for **risk stratification only** — the AAP 2022 tab
|
||
is the source of truth for treatment thresholds.
|
||
|
||
#### 5.5 Vital signs (line 1698)
|
||
|
||
Static `VITALS_DATA` keyed by age band (premie / newborn / infant /
|
||
toddler / preschool / school / adolescent). Each entry has HR
|
||
awake/sleeping ranges, RR, BP, temperature route ranges. Cites Harriet
|
||
Lane 23e.
|
||
|
||
#### 5.6 BSA (line 1069) — Mosteller
|
||
|
||
`bsa = sqrt(height × weight / 3600)`. Three lines.
|
||
|
||
#### 5.7 Weight-based dosing (line 1095)
|
||
|
||
Generic helper: weight × dose-per-kg, divided by frequency, capped at
|
||
optional max, optionally divided by concentration to give volume.
|
||
Doesn't know any specific drug.
|
||
|
||
#### 5.8 Resus meds (line 1869)
|
||
|
||
`RESUS_MEDS` array of 13 drugs (Adenosine, Amiodarone, Atropine, CaCl,
|
||
Ca-gluc, Dextrose, Epinephrine, Hydrocortisone, Insulin, Lidocaine,
|
||
MgSO4, Naloxone, Bicarb). Each row has a `calc(weight)` closure
|
||
returning `{dose, extra, max}`. Categorised cardiac / metabolic /
|
||
reversal with colour coding. The Dextrose row branches by weight (D10
|
||
for <5 kg, D25 for <45 kg, D50 for adult).
|
||
|
||
#### 5.9 GCS (line 2109)
|
||
|
||
Sub-pill nav for adult vs pediatric verbal score. Sums eye + verbal +
|
||
motor selects. The pediatric verbal subscale uses age-appropriate
|
||
wording (coos/babbles, irritable cry, etc.).
|
||
|
||
#### 5.10 Equipment (line 2169)
|
||
|
||
`EQUIP_DATA` static lookup by weight/age band: BVM size, nasal/oral
|
||
airway, blade, ETT (uncuffed and cuffed), LMA, GlideScope, IV gauge,
|
||
central-line, NGT, chest tube, Foley. Cites Harriet Lane.
|
||
|
||
### Stale code in `calculators.js`
|
||
|
||
Lines 2271–2941 contain a now-redundant duplicate of the bedside
|
||
sections (Neonatal `(function(){...})()` IIFE, Respiratory, Seizure,
|
||
Anaphylaxis, Sedation). These ran before the section modules were
|
||
extracted to `public/js/bedside/`. They still execute (the IIFE
|
||
runs at script load) but the bedside HTML buttons (`#btn-neo-assess`,
|
||
`#btn-pram-calc`, etc.) are now also handled by the ES-module sections.
|
||
Since both handlers use `e.target.id` checks and call the same
|
||
calculation endpoints, the user sees the result of whichever one ran
|
||
last. **Cleanup task**: delete lines 2271–2941. Out of scope for this
|
||
doc — flagged for the improvement-roadmap file.
|
||
|
||
---
|
||
|
||
## 6. `drugs-loader.js` — JSON-backed drug references
|
||
|
||
31 lines. Fetches `/data/drugs.json` once at boot, exposes:
|
||
|
||
- `window._DRUGS` — the parsed JSON (or `{version: null, sections: {}}`
|
||
before fetch resolves; never null).
|
||
- `window._DRUGS_READY` — Promise resolving to the JSON. Not currently
|
||
awaited by any consumer; bedside sections check `window._DRUGS` lazily
|
||
at render time.
|
||
|
||
`/data/drugs.json` (versioned 1.0, last_reviewed 2026-04-20) contains
|
||
five sections matched to bedside modules: `anaphylaxis`, `sedation`,
|
||
`seizure`, `agitation`, `antiemetics`. Each entry has at minimum
|
||
`name`, `dose_mg_per_kg`, `max_mg`, `unit`, `route`, `notes`,
|
||
`source`. Range-based entries (sedation Ketamine, Midaz IV) use
|
||
`dose_mg_per_kg_low` / `_high` and `max_mg_low` / `_high`. Fixed-dose
|
||
entries (Nitrous "50:50 mix") use `dose_display`.
|
||
|
||
The bedside consumers (`anaphylaxis.js`, `sedation.js`, `agitation.js`,
|
||
`antiemetics.js`, `seizure.js`) all follow the same merge pattern:
|
||
|
||
```js
|
||
function xxxDrugs() {
|
||
var s = window._DRUGS && window._DRUGS.sections && window._DRUGS.sections.xxx;
|
||
if (s && s.drugs && s.drugs.length) {
|
||
return s.drugs.map(function(dg) {
|
||
var fb = XXX_FALLBACK.filter(function(x) { return x.name === dg.name; })[0] || {};
|
||
return Object.assign({}, fb, dg); // JSON wins per-field
|
||
});
|
||
}
|
||
return XXX_FALLBACK;
|
||
}
|
||
```
|
||
|
||
This means:
|
||
|
||
1. The bedside section keeps working even if the JSON fetch fails (CORS,
|
||
404, offline) — `XXX_FALLBACK` is a hard-coded mirror of the JSON
|
||
shape.
|
||
2. When the JSON loads, individual fields (`notes`, `route`, `max_mg`)
|
||
can be updated centrally without editing 5 JS files.
|
||
3. **The JSON is authoritative when present**; the inline fallback
|
||
exists only for resilience.
|
||
|
||
---
|
||
|
||
## 7. PE Guide (`peGuide.js` + `src/routes/peGuide.js`)
|
||
|
||
Interactive OSCE-style physical-exam reference + AI narrative
|
||
generator.
|
||
|
||
### Frontend (`public/js/peGuide.js`, 1714 lines)
|
||
|
||
Single IIFE, gated on first `tabChanged` to `peguide`. Three top-level
|
||
selectors in `pe-guide.html` (`#pe-age-group`, `#pe-system` pills) +
|
||
demographics fields + report-style and model dropdowns.
|
||
|
||
#### `SCALES` (line 23)
|
||
|
||
Universal grading scales rendered at the top of each system view. **All
|
||
load-bearing reference data — see section 10.**
|
||
|
||
| Key | Title |
|
||
|---|---|
|
||
| `mrc` | MRC strength grade 0–5 |
|
||
| `dtr` | Deep-tendon reflex 0–4+ |
|
||
| `plantar` | Plantar (Babinski) — down/up/asymmetric |
|
||
| `beighton` | Beighton hypermobility 0–9 |
|
||
| `atr` | Scoliometer angle of trunk rotation (refer ≥7°) |
|
||
| `rr` | RR upper limit by age (WHO tachypnea cutoffs) |
|
||
| `spo2` | SpO2 bands (≥95 / 92–94 / <92 / <88) |
|
||
| `silverman` | Silverman-Andersen retraction 0–10 |
|
||
| `westley` | Westley croup 0–17 |
|
||
| `murmurGrade` | Levine 1–6 |
|
||
| `pulseAmp` | 0–4+ |
|
||
| `capRefill` | <2 / 2–3 / ≥3 sec |
|
||
|
||
`SYSTEM_SCALES` (line 150) maps system → which scales appear:
|
||
|
||
```js
|
||
{ msk: ['atr','beighton'], neuro: ['mrc','dtr','plantar'],
|
||
resp: ['rr','spo2','silverman','westley'],
|
||
cv: ['murmurGrade','pulseAmp','capRefill'] }
|
||
```
|
||
|
||
#### Sound libraries (lines 252, 286)
|
||
|
||
`RESP_SOUNDS` (8 sounds) and `CARDIAC_SOUNDS` (6 sounds). Each entry
|
||
has `key`, `src` (`.ogg` / `.wav` under `/audio/`), `title`, `where`,
|
||
`features`, `clinical`. Rendered as cards with native `<audio
|
||
controls>` for cross-platform play/pause/seek. See the
|
||
`project_respiratory_audio.md` memory file for which sounds are real
|
||
recordings vs synth (only "grunting" is still synth at time of
|
||
writing).
|
||
|
||
#### `PE_DATA` (line 316)
|
||
|
||
The big tree. Six age groups (`newborn` / `infant` / `toddler` /
|
||
`preschool` / `school` / `adolescent`) × four systems (`msk` / `neuro` /
|
||
`resp` / `cv`). Each system has:
|
||
|
||
```
|
||
{ overview: '...',
|
||
components: [
|
||
{ name: '...',
|
||
steps: [{ label, method, normal }, ...],
|
||
abnormalHints: [...] }
|
||
] }
|
||
```
|
||
|
||
Each step is one row in the OSCE checklist with three buttons
|
||
(Normal / Abnormal / Skip). State stored in `state[componentKey-stepIdx]`
|
||
with `{component, label, method, normal, status: 'normal'|'abnormal'|null,
|
||
note?: string}`.
|
||
|
||
#### Report generation (line 1660)
|
||
|
||
`generate()` filters `state` to only the current system's steps, then
|
||
POSTs to `/api/generate-pe-narrative` with `{steps, ageGroup, system,
|
||
patientAge, patientGender, model, format}` where `format` is
|
||
`narrative` | `list`. Renders the response into `#pe-narrative-text`
|
||
(contenteditable) plus a summary bar showing normal/abnormal/skipped
|
||
counts.
|
||
|
||
### Backend (`src/routes/peGuide.js`, 104 lines)
|
||
|
||
Single endpoint `POST /api/generate-pe-narrative`.
|
||
|
||
1. Partitions steps into `normal` / `abnormal` arrays.
|
||
2. `groupByComponent` re-groups each partition by `step.component`
|
||
for readability.
|
||
3. Builds a structured plain-text payload — one section per partition,
|
||
one sub-section per component, bullets with `method` and either
|
||
`expected normal` or `physician note`.
|
||
4. Picks prompt by `format`: `PROMPTS.peGuideNarrative` (paragraphs)
|
||
or `PROMPTS.peGuideList` (structured list).
|
||
5. Calls `callAI([{system: prompt + INJECTION_GUARD}, {user: ...
|
||
wrapUserText('pe_steps', text) ...}], {model})`. The `INJECTION_GUARD`
|
||
+ `wrapUserText` come from `src/utils/promptSafe.js` and bracket the
|
||
exam text with `<UNTRUSTED_pe_steps>...</UNTRUSTED_pe_steps>` plus a
|
||
system-level instruction to ignore any instructions inside.
|
||
6. Returns `{success, narrative, model, summary: {normal, abnormal,
|
||
notAssessed}}`.
|
||
7. Audits `generate_pe_narrative` with `category: 'clinical'`.
|
||
|
||
---
|
||
|
||
## 8. Vax Schedule + Catchup
|
||
|
||
### Data (`pediatricScheduleData.js`, 2120 lines)
|
||
|
||
Single static-data file. Twelve top-level constants
|
||
(see line 2104 export list):
|
||
|
||
- `VISIT_AGES` (32 visits from prenatal → 21 yr)
|
||
- `WELL_VISIT_CODES` (ICD-10 + CPT per visit)
|
||
- `VACCINES` (master list with brand names, dose count, CVX codes)
|
||
- `PERIODICITY` (the AAP 2025 Bright Futures table — what to do at
|
||
each visit: vaccines, screening, anticipatory guidance)
|
||
- `CATCH_UP_SCHEDULE` (per-vaccine minimum age + minimum interval +
|
||
catchUpNotes for each dose)
|
||
- `ANTICIPATORY_GUIDANCE` (talking points by age group)
|
||
- `AAP_FOOTNOTES` (verbatim 2025 periodicity-table footnotes)
|
||
- `DYSLIPIDEMIA_SCHEDULE` (AAP/NHLBI)
|
||
- `NEWBORN_SCREENING`
|
||
- `GROWTH_REFERENCE` (expected gains by age)
|
||
- `REFLEXES_REFERENCE` (primitive + age of disappearance)
|
||
- `BMI_CLASSIFICATION` (AAP 2023 + CDC Extended BMI)
|
||
|
||
Sources cited at the top of the file: AAP/Bright Futures Feb 2025; CDC
|
||
Child & Adolescent Immunization Schedule 2025.
|
||
|
||
### UI consumers
|
||
|
||
- **WellVisit tab** (`public/js/wellVisit.js`) reads `VISIT_AGES` to
|
||
populate the visit dropdown, `PERIODICITY[visitId]` to show what's
|
||
due.
|
||
- **`renderFullSchedule()`** (line 663) builds the full immunization
|
||
matrix for `#wv-panel-schedule` — vaccine on the y-axis, visit ages
|
||
on the x-axis, cells marked with dose number (`#1`, `#2`, …) or
|
||
special tokens (`annual`, `catch-up`).
|
||
- **`renderCatchUp()`** (line 619) iterates `CATCH_UP_SCHEDULE` and
|
||
renders one card per vaccine with: minimum age for dose 1, dose
|
||
series table (dose # / min age / min interval from previous / notes),
|
||
and `catchUpNotes` (string or array).
|
||
- The HTML containers are `public/components/vaxschedule.html` (7
|
||
lines, just `#wv-panel-schedule`) and `catchup.html` (7 lines, just
|
||
`#wv-panel-catchup`).
|
||
|
||
There is no catch-up "calculator" — the rendering is the reference.
|
||
Catchup logic that would convert a partial vaccination history into a
|
||
forward schedule is currently in the user's head, not the code.
|
||
|
||
---
|
||
|
||
## 9. Milestones
|
||
|
||
Two distinct milestone systems live in this app:
|
||
|
||
### a) Developmental milestones (this section)
|
||
|
||
Reference data + AI narrative for well-child visits. Source-of-truth is
|
||
the `developmental_milestones` table; static fallback in
|
||
`milestonesData.js`.
|
||
|
||
### b) User-tracked milestones
|
||
|
||
Personal-task feature (e.g., "patient X had abnormal CBC, follow up
|
||
3 weeks") — owned by a different module entirely, not covered here.
|
||
|
||
### Frontend (`milestones.js`, 240 lines)
|
||
|
||
Gated on first `tabChanged` to `wellvisit`. On init:
|
||
|
||
1. `fetch('/api/milestones-data')` — DB-backed; if it returns >0
|
||
milestones, use them. If empty or fetch fails, fall back to
|
||
`window.MILESTONES_DATA_STATIC` (defined in `milestonesData.js`,
|
||
line 8 — about 16 age groups × 5 domains).
|
||
2. `populateAgeGroups()` fills `#ms-age-group` dropdown.
|
||
3. On age-group change, `renderChecklist()` builds one section per
|
||
domain (Gross Motor / Fine Motor / Language / Social/Emotional /
|
||
Cognitive). Each milestone is a row with a ✓ / ✗ toggle pair.
|
||
4. `state[id]` tracks `{domain, milestone, status: 'yes' | 'no' | null}`.
|
||
5. `updateBadges()` updates the per-domain `Xachieved/Ynot/Z items`
|
||
counter live.
|
||
|
||
### Generation
|
||
|
||
Two endpoints, both routed through `src/routes/milestones.js`:
|
||
|
||
- `POST /api/generate-milestone-narrative` — takes the assessed
|
||
milestones array, partitions into achieved / not-yet-achieved /
|
||
not-assessed (the not-assessed list is **explicitly told to be
|
||
omitted** from the narrative; only assessed items appear in the
|
||
output). Picks `PROMPTS.milestoneNarrative` (paragraphs) or
|
||
`PROMPTS.milestoneList`. Calls `callAI` with `INJECTION_GUARD` +
|
||
`wrapUserText('milestones', ...)`.
|
||
- `POST /api/generate-milestone-summary` — takes a previously generated
|
||
narrative and returns a 3-sentence summary via
|
||
`PROMPTS.milestoneSummary`. Capped at 500 max tokens.
|
||
|
||
### Admin endpoints (`src/routes/adminMilestones.js`, 149 lines)
|
||
|
||
Full CRUD for the `developmental_milestones` table, all behind
|
||
`adminMiddleware`:
|
||
|
||
| Method | Path | Purpose |
|
||
|---|---|---|
|
||
| GET | `/api/admin/milestones` | list all (filterable by `?age_group=`) |
|
||
| GET | `/api/admin/milestones/meta` | distinct age_groups + distinct domains |
|
||
| POST | `/api/admin/milestones` | create one |
|
||
| PUT | `/api/admin/milestones/:id` | update one |
|
||
| DELETE | `/api/admin/milestones/:id` | delete one |
|
||
| POST | `/api/admin/milestones/bulk-import` | bulk insert with optional `clearExisting` |
|
||
|
||
Schema: `id`, `age_group`, `domain`, `milestone_text`, `sort_order`,
|
||
`updated_at`. Bulk-import is how `MILESTONES_DATA_STATIC` was seeded into
|
||
the DB.
|
||
|
||
---
|
||
|
||
## 10. Sacred zones — do not modify without test vectors
|
||
|
||
These data structures and functions encode validated clinical
|
||
references. Every one of them was either pulled from a regulatory body
|
||
or fit against a peer-reviewed validator (peditools, Epic). Changing a
|
||
single number can shift a borderline case across a decision boundary
|
||
and produce wrong clinical guidance. **Do not touch any of the
|
||
following without a corresponding test vector and explicit approval.**
|
||
|
||
### `public/js/calc-math.js`
|
||
|
||
- `LB` — Lund-Browder regional %
|
||
- All formula functions (`parkland`, `maintenance421`, `pramScore`,
|
||
`westleyScore`, `epi*`, `nrpEpiIV`, `rsi*`, `minSBP`, `ettSize`,
|
||
`lundBrowderTBSA`)
|
||
|
||
### `public/js/calculators.js`
|
||
|
||
- `_htLMS_F_L` / `_htLMS_F_M` / `_htLMS_F_S` (lines 89–91) and the
|
||
matching `_htLMS_M_*` arrays — 218 entries each, height LMS
|
||
- `_bpCoeff_F_SYS` / `_bpCoeff_F_DIA` / `_bpCoeff_M_SYS` /
|
||
`_bpCoeff_M_DIA` — 99 × 13 each, BCM/Rosner BP coefficients
|
||
- `bmiLMS` (line 739) — CDC 2000 BMI LMS
|
||
- `wfaLMS` and the other growth-chart LMS tables in the
|
||
`growth` calculator block
|
||
- `photoThresholds35..40` and `photoThresholds35..38risk` — AAP 2022
|
||
phototherapy
|
||
- `exchangeThresholds35..38` and `..38risk` — AAP 2022 exchange
|
||
- `bhutaniZones.p95 / p75 / p40` — Bhutani 1999 nomogram
|
||
- `RESUS_MEDS` doses
|
||
- `EQUIP_DATA` and `VITALS_DATA` lookup tables
|
||
- The functions `calcHeightPctile`, `computeBPPercentile`,
|
||
`classifyBPFromPercentiles`, `calcBMIPercentile`, `valueFromLMS`,
|
||
`interpolateLMS`, `interpolateThreshold`, `classifyBMI`
|
||
|
||
### `public/js/bedside/neonatal.js`
|
||
|
||
- `fentonLMS` (lines 20–37) — preterm growth LMS, peditools-validated
|
||
(40 5/7 wk male 3070 g → z = −1.42)
|
||
|
||
### `public/js/bedside/burns.js`
|
||
|
||
- `LB` (lines 10–30) — same as calc-math.js. If you update one, update
|
||
both.
|
||
|
||
### `public/js/bedside/sutures.js`
|
||
|
||
- `SITES` table — site defaults are clinical recommendations (size,
|
||
removal day, technique, materialFamily, glueOK)
|
||
- `recommend()` decision rules — every `if` branch encodes a clinical
|
||
judgment (cat/human bites do not get primary closure; >12 h non-
|
||
face/scalp triggers delayed closure; etc.)
|
||
|
||
### `public/js/peGuide.js`
|
||
|
||
- `SCALES` — every grading scale (MRC, DTRs, Beighton, Levine, Westley,
|
||
Silverman, etc.)
|
||
- `RESP_SOUNDS` and `CARDIAC_SOUNDS` `clinical` and `features` strings
|
||
(also: don't break the `src` paths — they map to real audio files)
|
||
- `PE_DATA[ageGroup][system]` — every step's `method` and `normal`
|
||
text
|
||
|
||
### `public/js/pediatricScheduleData.js`
|
||
|
||
- All twelve exported constants. The whole file is reference data.
|
||
|
||
### `public/js/milestonesData.js`
|
||
|
||
- `MILESTONES_DATA_STATIC` is the static fallback; the live data lives
|
||
in the DB. Edit through the admin endpoints, not the static file.
|
||
|
||
For all of the above: if you really do need to update a value (e.g.,
|
||
peditools releases a new Fenton fit, or AAP issues a 2027 bilirubin
|
||
update), do it via:
|
||
|
||
1. Land a test vector in `test/` first (`node --test`).
|
||
2. Update the data with the change.
|
||
3. Confirm tests pass with the new vectors.
|
||
4. Cross-check at least 3 boundary cases against the upstream source
|
||
(peditools / Epic / the AAP figure).
|
||
|
||
---
|
||
|
||
## 11. How to add a new bedside section
|
||
|
||
Concrete checklist using `burns.js` and `sutures.js` as templates.
|
||
|
||
1. **Create the module**: `public/js/bedside/<key>.js`.
|
||
|
||
```js
|
||
// ============================================================
|
||
// bedside/<key>.js
|
||
// <Section name> — <one-line description>
|
||
// ============================================================
|
||
|
||
import { S } from './shared.js';
|
||
|
||
function calcXxx() {
|
||
var wt = parseFloat(document.getElementById('xxx-weight').value);
|
||
var el = document.getElementById('xxx-result');
|
||
if (!wt) { el.innerHTML = '<p style="color:var(--red);font-size:13px;">Enter weight (kg).</p>'; return; }
|
||
var html = '<div ...header...>...</div>';
|
||
html += S.drugTable(...);
|
||
html += S.ref('Citation here.');
|
||
el.innerHTML = html;
|
||
}
|
||
|
||
export function init() {
|
||
document.addEventListener('click', function(e) {
|
||
if (e.target.id === 'btn-xxx-calc' || e.target.closest('#btn-xxx-calc')) calcXxx();
|
||
});
|
||
}
|
||
```
|
||
|
||
2. **Register in `bedside/index.js`**:
|
||
|
||
```js
|
||
import * as xxx from './xxx.js';
|
||
// ... add `xxx,` to the array fed to forEach(m => m.init())
|
||
```
|
||
|
||
3. **Add the key to `SECTIONS` in `bedside/sub-nav.js`** (line 8):
|
||
|
||
```js
|
||
var SECTIONS = [..., 'sutures', 'xxx'];
|
||
```
|
||
|
||
4. **Add the pill button + section div in `public/components/bedside.html`**:
|
||
|
||
```html
|
||
<button class="calc-pill" data-em="xxx"><i class="fas fa-..."></i> Section name</button>
|
||
...
|
||
<div id="em-xxx" class="em-section" style="display:none;">
|
||
<h4 ...>...</h4>
|
||
<input id="xxx-weight" ...>
|
||
<button id="btn-xxx-calc" class="btn-sm btn-primary">Calculate</button>
|
||
<div id="xxx-result"></div>
|
||
</div>
|
||
```
|
||
|
||
5. **(Optional) If using JSON-backed drug data**: add a `xxx` section to
|
||
`public/data/drugs.json` and follow the merge pattern from
|
||
`sedation.js` / `agitation.js` / `antiemetics.js` / `seizure.js`
|
||
(`XXX_FALLBACK` inline + `xxxDrugs()` merger).
|
||
|
||
6. **Cite sources** at the bottom of the rendered output via
|
||
`S.ref('...')`. Bedside is reference content — every algorithmic
|
||
recommendation must be traceable.
|
||
|
||
7. **Test the full flow**: sign out → sign in → click Bedside tab →
|
||
click the new pill → enter weight → click Calculate. Then refresh
|
||
the page and confirm `UIState` lands you on the same pill.
|
||
|
||
8. **Update the SECTIONS list in this doc** under section 4's site
|
||
table or section 3's per-section list as appropriate.
|
||
|
||
Things to NOT do:
|
||
|
||
- Don't import anything that touches `window.UIState` directly — the
|
||
`sub-nav.js` module owns persistence for all bedside pills.
|
||
- Don't put per-section weight in the top `#bedside-weight` field.
|
||
Sections own their own weight; the top is estimator-only.
|
||
- Don't rely on `$('#xxx').click(...)` style direct binding — the
|
||
bedside HTML is lazy-injected, so any direct binding before injection
|
||
will silently fail. Always use `document.addEventListener` +
|
||
`.closest()`.
|
||
|
||
---
|
||
|
||
## 12. Why bedside uses ES modules
|
||
|
||
History: every other JS file in `public/js/` is an IIFE
|
||
(`(function() { ... })();`) or a bare top-level script. The pattern is
|
||
consistent — modules talk via `window` globals and `CustomEvent` on
|
||
`document`. No bundler, no framework, no build step.
|
||
|
||
The original `calculators.js` ate the bedside emergencies sections, so
|
||
it grew to several thousand lines and was difficult to navigate. The
|
||
"calculators" navigation pill for Bedside was eventually promoted to
|
||
its own top-level tab (`<section id="bedside-tab" data-component="bedside">`
|
||
at index.html:309). At that point Daniel asked for bedside to be
|
||
extracted into per-section files — partly for readability, partly as
|
||
the **proof-of-concept for the planned migration to ES modules**.
|
||
|
||
Bedside was chosen because:
|
||
|
||
1. Its sections are independent — cardiac doesn't reference seizure.
|
||
2. It has no AI calls and no PHI, so the import surface is small.
|
||
3. Its only shared dependency (`S` formatting helpers) is a pure
|
||
namespace, easy to import.
|
||
4. The HTML container is one component (`bedside.html`), not scattered.
|
||
|
||
The result is the structure documented here: `bedside/index.js` is the
|
||
sole `<script type="module">` in `index.html`. Everything else still
|
||
loads as IIFE so nothing breaks.
|
||
|
||
This is **the migration target shape** referenced in
|
||
`memory/project_migration_checkpoint.md`. The next steps (per
|
||
`docs/improvements.md`) are:
|
||
|
||
1. Split `calculators.js` (2941 lines, 10 calculators) into
|
||
`calculators/<calc>.js` ES modules with a similar `index.js`
|
||
registrar.
|
||
2. Move LMS / coefficient tables to `data/*.json` so the JS is small
|
||
and the data is easy to diff.
|
||
3. Repeat for `peGuide.js` (1714 lines) — `peGuide/sounds.js`,
|
||
`peGuide/scales.js`, `peGuide/data/<ageGroup>/<system>.js`.
|
||
4. Eventually flip the `<script defer>` load order to `<script type="module">`
|
||
for everything; remove the `window._XXX` glue.
|
||
|
||
Until then: bedside is the only ES-module island. New sections under
|
||
`bedside/` should follow the ES-module convention (`import` /
|
||
`export function init()`); new code outside `bedside/` should still be
|
||
IIFE so the loader order keeps working.
|
||
|
||
---
|
||
|
||
## File index (absolute paths)
|
||
|
||
Bedside:
|
||
- `/home/danvics/docker/ped-ai/public/js/bedside/index.js`
|
||
- `/home/danvics/docker/ped-ai/public/js/bedside/shared.js`
|
||
- `/home/danvics/docker/ped-ai/public/js/bedside/sub-nav.js`
|
||
- `/home/danvics/docker/ped-ai/public/js/bedside/image-lightbox.js`
|
||
- `/home/danvics/docker/ped-ai/public/js/bedside/age-weight.js`
|
||
- `/home/danvics/docker/ped-ai/public/js/bedside/neonatal.js`
|
||
- `/home/danvics/docker/ped-ai/public/js/bedside/airway.js`
|
||
- `/home/danvics/docker/ped-ai/public/js/bedside/cardiac.js`
|
||
- `/home/danvics/docker/ped-ai/public/js/bedside/respiratory.js`
|
||
- `/home/danvics/docker/ped-ai/public/js/bedside/ventilation.js`
|
||
- `/home/danvics/docker/ped-ai/public/js/bedside/seizure.js`
|
||
- `/home/danvics/docker/ped-ai/public/js/bedside/sepsis.js`
|
||
- `/home/danvics/docker/ped-ai/public/js/bedside/anaphylaxis.js`
|
||
- `/home/danvics/docker/ped-ai/public/js/bedside/sedation.js`
|
||
- `/home/danvics/docker/ped-ai/public/js/bedside/agitation.js`
|
||
- `/home/danvics/docker/ped-ai/public/js/bedside/antiemetics.js`
|
||
- `/home/danvics/docker/ped-ai/public/js/bedside/antimicrobials.js`
|
||
- `/home/danvics/docker/ped-ai/public/js/bedside/burns.js`
|
||
- `/home/danvics/docker/ped-ai/public/js/bedside/toxicology.js`
|
||
- `/home/danvics/docker/ped-ai/public/js/bedside/trauma.js`
|
||
- `/home/danvics/docker/ped-ai/public/js/bedside/sutures.js`
|
||
- `/home/danvics/docker/ped-ai/public/components/bedside.html`
|
||
|
||
Calculators:
|
||
- `/home/danvics/docker/ped-ai/public/js/calc-math.js`
|
||
- `/home/danvics/docker/ped-ai/public/js/calculators.js`
|
||
- `/home/danvics/docker/ped-ai/public/js/drugs-loader.js`
|
||
- `/home/danvics/docker/ped-ai/public/components/calculators.html`
|
||
- `/home/danvics/docker/ped-ai/public/data/drugs.json`
|
||
|
||
PE Guide:
|
||
- `/home/danvics/docker/ped-ai/public/js/peGuide.js`
|
||
- `/home/danvics/docker/ped-ai/src/routes/peGuide.js`
|
||
- `/home/danvics/docker/ped-ai/public/components/pe-guide.html`
|
||
|
||
Vax Schedule + Catchup:
|
||
- `/home/danvics/docker/ped-ai/public/js/pediatricScheduleData.js`
|
||
- `/home/danvics/docker/ped-ai/public/js/wellVisit.js` (consumer)
|
||
- `/home/danvics/docker/ped-ai/public/components/vaxschedule.html`
|
||
- `/home/danvics/docker/ped-ai/public/components/catchup.html`
|
||
|
||
Milestones:
|
||
- `/home/danvics/docker/ped-ai/public/js/milestones.js`
|
||
- `/home/danvics/docker/ped-ai/public/js/milestonesData.js`
|
||
- `/home/danvics/docker/ped-ai/src/routes/milestones.js`
|
||
- `/home/danvics/docker/ped-ai/src/routes/adminMilestones.js`
|