pediatric-ai-scribe-v3/public/js/bedside/ventilation.js
Daniel 031bfb995a feat: C — extract Bedside reference into ES modules
Split the Bedside clinical reference section out of calculators.js
(~1220 lines) into 20 focused ES modules under public/js/bedside/.
Each module owns one clinical topic (cardiac, seizure, sepsis, burns,
etc.) and exports an init() wired up by bedside/index.js. Shared
helpers live in shared.js and also set window._EM for back-compat
with the 4 remaining call sites in calculators.js.

Load order: classic defer calculators.js first, then module script
bedside/index.js. Handlers bind at DOM-ready; runtime _EM lookups
resolve after both are evaluated.

Makes bedside content editable per-section, shrinks calculators.js
from 4111 to 2891 lines, and keeps the existing Playwright smoke
suite (e2e/tests/bedside-smoke.spec.js) as the behavioral contract.
2026-04-20 23:19:50 +02:00

147 lines
15 KiB
JavaScript

// ============================================================
// bedside/ventilation.js
// O2 & VENTILATION (teaching reference).
// ============================================================
import { S } from './shared.js';
function showVent() {
var wt = parseFloat(document.getElementById('vent-weight').value);
var age = parseFloat(document.getElementById('vent-age').value);
var el = document.getElementById('vent-result');
var html = '';
// ── Target SpO2 quick reference ──
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:0 0 6px;">Target SpO2</h5>';
html += '<div style="overflow-x:auto;-webkit-overflow-scrolling:touch;margin-bottom:14px;"><table style="width:100%;min-width:460px;border-collapse:collapse;font-size:12px;">';
html += '<thead><tr style="background:var(--g100);"><th style="text-align:left;padding:5px 8px;">Patient</th><th style="text-align:left;padding:5px 8px;">Target</th><th style="text-align:left;padding:5px 8px;">Notes</th></tr></thead><tbody>';
html += '<tr><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">Most children</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);"><strong>94-98%</strong></td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">Normal</td></tr>';
html += '<tr><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">Bronchiolitis (AAP 2014/2023)</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);"><strong>≥90%</strong></td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">Don\'t chase higher saturations</td></tr>';
html += '<tr><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">Chronic lung disease / CF</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);"><strong>90-94%</strong></td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">Avoid hyperoxia in CO2 retainers</td></tr>';
html += '<tr><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">Preterm neonate</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);"><strong>90-95%</strong></td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">Minimize ROP risk</td></tr>';
html += '<tr><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">Term neonate (min of life)</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);"><strong>Per NRP ladder</strong></td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">1 min 60-65%, 10 min 85-95%</td></tr>';
html += '</tbody></table></div>';
// ── Escalation ladder ──
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:14px 0 6px;">Escalation ladder (step up when target not reached / work of breathing)</h5>';
html += S.stepBox('#10b981',
'1. Nasal cannula (low-flow)',
'<strong>0.5-6 L/min</strong> &nbsp;·&nbsp; FiO2 ~24-40% (rough: room air 21% + 4% per L/min) &nbsp;·&nbsp; comfortable, no humidification needed. Good for mild hypoxia or baseline support.');
html += S.stepBox('#3b82f6',
'2. Simple face mask',
'<strong>6-10 L/min</strong> &nbsp;·&nbsp; FiO2 35-60%. Must keep flow &gt;6 L/min to flush CO2. Tolerated less well than NC.');
html += S.stepBox('#8b5cf6',
'3. Non-rebreather mask',
'<strong>10-15 L/min</strong> &nbsp;·&nbsp; FiO2 60-90%. Reservoir bag must stay inflated. For severe hypoxia with intact breathing.');
html += S.stepBox('#f59e0b',
'4. High-flow nasal cannula (HFNC)',
'<strong>' + (wt ? '1-2 L/kg/min = ' + S.d(wt, 1) + '-' + S.d(wt, 2) + ' L/min' : '1-2 L/kg/min') + '</strong> (max ~50-60 L/min adults) &nbsp;·&nbsp; heated + humidified &nbsp;·&nbsp; FiO2 30-100% titratable &nbsp;·&nbsp; generates ~2-5 cmH2O PEEP &nbsp;·&nbsp; works for bronchiolitis, early resp failure. Reassess at 1-2 h — no improvement → step up.');
html += S.stepBox('#ef4444',
'5. Non-invasive ventilation (CPAP / BiPAP)',
'<strong>CPAP</strong> 5-10 cmH2O (pure PEEP) &nbsp;·&nbsp; <strong>BiPAP</strong> IPAP 10-14 / EPAP 5 (adds pressure support) &nbsp;·&nbsp; FiO2 as needed. Needs cooperative patient, intact airway reflexes, no copious secretions. Contraindicated: coma, apnea, unstable hemodynamics, facial trauma.');
html += S.stepBox('#dc2626',
'6. Intubate + mechanical ventilation',
'When NIV fails, airway compromised, apnea, or GCS ≤8. See RSI under the Airway tab for drugs.');
// ── BVM how-to ──
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:14px 0 6px;">Bag-Valve-Mask (BVM)</h5>';
html += '<div style="padding:10px 12px;background:var(--blue-light);border-radius:6px;font-size:12px;line-height:1.6;">';
html += '<strong>When:</strong> apnea, bradycardia (HR &lt;60 in neonate; inadequate breathing at any age), during resuscitation.<br>';
html += '<strong>Rate:</strong> Newborn 40-60 /min &nbsp;·&nbsp; Infant-child 20-30 /min &nbsp;·&nbsp; Adolescent 10-12 /min (1 breath q5-6 sec).<br>';
html += '<strong>Tidal volume:</strong> 6-8 mL/kg — only enough to see <strong>gentle chest rise</strong>. Avoid over-ventilation (gastric distension, ↓venous return, lung injury).<br>';
html += '<strong>Technique:</strong> head tilt / jaw thrust, proper mask seal (E-C grip or 2-thumb), squeeze 1 sec, release fully before next breath. Attach O2 at 10-15 L/min + reservoir.<br>';
html += '<strong>If not ventilating:</strong> MR SOPA — <u>M</u>ask reseal, <u>R</u>eposition airway, <u>S</u>uction mouth+nose, <u>O</u>pen mouth, <u>P</u>ressure ↑, <u>A</u>lternative airway (LMA or ETT).';
html += '</div>';
// ── Mechanical vent settings ──
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:14px 0 6px;">Mechanical ventilator — starting settings</h5>';
var rate = age == null ? '' : (age < 0.1 ? '30-40' : age < 1 ? '25-35' : age < 5 ? '20-25' : age < 12 ? '16-20' : '12-16');
html += '<div style="padding:10px 12px;background:var(--g50);border-radius:6px;font-size:12px;line-height:1.65;">';
html += '<strong>Mode:</strong> Volume-control (set TV, get pressure) OR Pressure-control (set pressure, get TV). PRVC / SIMV-PS are hybrids. Pick what your unit uses.<br>';
html += '<strong>Tidal volume:</strong> <strong>' + (wt ? S.d(wt, 6) + '-' + S.d(wt, 8) + ' mL' : '6-8 mL/kg') + '</strong> <span style="color:var(--g500);font-size:11px;">(6-8 mL/kg)</span>. Use lower (4-6 mL/kg) for ARDS.<br>';
html += '<strong>Rate:</strong> ' + (rate ? rate + ' /min (for age ' + age + ' yr)' : 'Newborn 30-40 · Infant 25-35 · Child 16-20 · Adolescent 12-16') + '.<br>';
html += '<strong>PEEP:</strong> start <strong>5 cmH2O</strong> (routine). Increase to 8-12+ for refractory hypoxia (ARDS).<br>';
html += '<strong>FiO2:</strong> start 100%, wean rapidly to lowest that maintains target SpO2 (usually &lt;60%).<br>';
html += '<strong>I:E ratio:</strong> 1:2 normally. 1:3-4 for obstructive disease (asthma, bronchiolitis) to allow exhalation.<br>';
html += '<strong>Inspiratory time:</strong> 0.5 s infant &nbsp;·&nbsp; 0.7-1 s child &nbsp;·&nbsp; 1-1.2 s adolescent.<br>';
html += '<strong>Pressure limits:</strong> keep plateau &lt;30 cmH2O (ideally &lt;28) to avoid barotrauma.';
html += '</div>';
// ── Pressure-time waveform ──
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:14px 0 6px;">Pressure-time waveform (what the monitor shows)</h5>';
html += '<div style="padding:10px;background:white;border:1px solid var(--g200);border-radius:8px;margin-bottom:14px;">';
html += '<svg viewBox="0 0 540 260" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:640px;display:block;margin:0 auto;">' +
// grid lines
'<line x1="60" y1="60" x2="500" y2="60" stroke="#f3f4f6"/>' +
'<line x1="60" y1="100" x2="500" y2="100" stroke="#f3f4f6"/>' +
'<line x1="60" y1="140" x2="500" y2="140" stroke="#f3f4f6"/>' +
'<line x1="60" y1="180" x2="500" y2="180" stroke="#f3f4f6"/>' +
// axes
'<line x1="60" y1="220" x2="500" y2="220" stroke="#374151" stroke-width="1.5"/>' +
'<line x1="60" y1="20" x2="60" y2="220" stroke="#374151" stroke-width="1.5"/>' +
// y-axis tick labels (cmH2O)
'<text x="55" y="64" font-size="10" text-anchor="end" fill="#6b7280">30</text>' +
'<text x="55" y="104" font-size="10" text-anchor="end" fill="#6b7280">20</text>' +
'<text x="55" y="144" font-size="10" text-anchor="end" fill="#6b7280">10</text>' +
'<text x="55" y="184" font-size="10" text-anchor="end" fill="#6b7280">5</text>' +
'<text x="55" y="224" font-size="10" text-anchor="end" fill="#6b7280">0</text>' +
// axis titles
'<text x="275" y="250" font-size="11" text-anchor="middle" fill="#374151">Time (seconds)</text>' +
'<text x="18" y="120" font-size="11" text-anchor="middle" fill="#374151" transform="rotate(-90 18 120)">Pressure (cmH₂O)</text>' +
// x-axis time markers
'<text x="60" y="238" font-size="10" text-anchor="middle" fill="#6b7280">0</text>' +
'<text x="200" y="238" font-size="10" text-anchor="middle" fill="#6b7280">2</text>' +
'<text x="340" y="238" font-size="10" text-anchor="middle" fill="#6b7280">4</text>' +
'<text x="480" y="238" font-size="10" text-anchor="middle" fill="#6b7280">6</text>' +
// PEEP reference dashed line (y=180 = 5 cmH2O)
'<line x1="60" y1="180" x2="500" y2="180" stroke="#10b981" stroke-width="1" stroke-dasharray="4,3" opacity="0.5"/>' +
// Pressure curve — 3 breaths, each 2s (Ti=0.8s, Te=1.2s); 70px/sec; PEEP=5→y=180, PIP=25→y=80, Plateau=22→y=88
'<path d="M 60 180 L 80 180 L 90 80 L 145 88 L 155 180 L 200 180 L 210 80 L 265 88 L 275 180 L 340 180 L 350 80 L 405 88 L 415 180 L 480 180" fill="none" stroke="#3b82f6" stroke-width="2.5" stroke-linejoin="round"/>' +
// PIP label
'<line x1="120" y1="50" x2="92" y2="78" stroke="#ef4444"/>' +
'<text x="125" y="48" font-size="11" font-weight="bold" fill="#ef4444">PIP</text>' +
'<text x="125" y="60" font-size="9" fill="#6b7280">peak inspiratory</text>' +
// Plateau label
'<line x1="230" y1="50" x2="145" y2="88" stroke="#a855f7"/>' +
'<text x="235" y="48" font-size="11" font-weight="bold" fill="#a855f7">Plateau</text>' +
'<text x="235" y="60" font-size="9" fill="#6b7280">reflects alveolar pressure</text>' +
// PEEP label
'<line x1="380" y1="208" x2="345" y2="182" stroke="#10b981"/>' +
'<text x="385" y="208" font-size="11" font-weight="bold" fill="#10b981">PEEP</text>' +
// Ti bracket (x=90 to 155)
'<line x1="90" y1="32" x2="155" y2="32" stroke="#f59e0b" stroke-width="1"/>' +
'<line x1="90" y1="28" x2="90" y2="36" stroke="#f59e0b"/>' +
'<line x1="155" y1="28" x2="155" y2="36" stroke="#f59e0b"/>' +
'<text x="122" y="25" font-size="10" font-weight="bold" text-anchor="middle" fill="#f59e0b">Ti</text>' +
// Te bracket (x=155 to 200)
'<line x1="155" y1="202" x2="200" y2="202" stroke="#f59e0b" stroke-width="1"/>' +
'<line x1="155" y1="198" x2="155" y2="206" stroke="#f59e0b"/>' +
'<line x1="200" y1="198" x2="200" y2="206" stroke="#f59e0b"/>' +
'<text x="177" y="215" font-size="10" font-weight="bold" text-anchor="middle" fill="#f59e0b">Te</text>' +
'</svg>';
html += '<div style="font-size:11px;color:var(--g500);margin-top:8px;line-height:1.5;"><strong style="color:#ef4444;">PIP</strong> = peak pressure during inspiration (depends on TV, airway resistance, lung compliance). <strong style="color:#a855f7;">Plateau</strong> = static pressure at end-inspiration with brief breath-hold (reflects alveolar pressure — keep &lt;30). <strong style="color:#10b981;">PEEP</strong> = baseline end-expiratory pressure keeping alveoli open. <strong style="color:#f59e0b;">Ti / Te</strong> = inspiratory / expiratory time. Rate &times; (Ti+Te) = 60 s.</div>';
html += '</div>';
// ── Troubleshooting: "if this, change that" ──
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:14px 0 6px;">Adjusting for gas exchange</h5>';
html += '<div style="overflow-x:auto;-webkit-overflow-scrolling:touch;"><table style="width:100%;min-width:480px;border-collapse:collapse;font-size:12px;">';
html += '<thead><tr style="background:var(--g100);"><th style="text-align:left;padding:5px 8px;">Problem</th><th style="text-align:left;padding:5px 8px;">First lever</th><th style="text-align:left;padding:5px 8px;">Second lever</th></tr></thead><tbody>';
html += '<tr><td style="padding:5px 8px;border-bottom:1px solid var(--g100);"><strong>Low SpO2</strong> (oxygenation)</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">↑ FiO2</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">↑ PEEP (recruits collapsed alveoli)</td></tr>';
html += '<tr><td style="padding:5px 8px;border-bottom:1px solid var(--g100);"><strong>↑ PCO2</strong> (ventilation)</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">↑ Rate</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">↑ Tidal volume</td></tr>';
html += '<tr><td style="padding:5px 8px;border-bottom:1px solid var(--g100);"><strong>↓ PCO2</strong> (over-ventilating)</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">↓ Rate</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">↓ Tidal volume</td></tr>';
html += '<tr><td style="padding:5px 8px;border-bottom:1px solid var(--g100);"><strong>High peak pressure</strong></td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">Check tube (kink, mucus plug), compliance, bronchospasm</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">Suction, bronchodilator, lower TV</td></tr>';
html += '<tr><td style="padding:5px 8px;border-bottom:1px solid var(--g100);"><strong>Auto-PEEP</strong> (asthma, bronchiolitis)</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">↓ Rate, ↑ expiratory time</td><td style="padding:5px 8px;border-bottom:1px solid var(--g100);">Disconnect + bag briefly if critical</td></tr>';
html += '</tbody></table></div>';
// ── Mental model summary ──
html += '<div style="margin-top:14px;padding:10px 12px;background:#d1fae5;border-radius:6px;font-size:12px;color:#065f46;line-height:1.55;"><strong>Mental model:</strong> Oxygenation is mostly <strong>FiO2 + PEEP</strong>. Ventilation (CO2) is mostly <strong>rate + tidal volume</strong>. Match mode to disease: obstructive (asthma, bronchiolitis) → long expiratory time, permissive hypercapnia. Restrictive (ARDS) → low TV, high PEEP, permissive hypercapnia + hypoxia.</div>';
html += S.ref('Based on AAP / PALS / AARC guidance. Tailor to individual patient and institutional protocols.');
el.innerHTML = html;
}
export function init() {
document.addEventListener('click', function(e) {
if (e.target.id === 'btn-vent-show' || e.target.closest('#btn-vent-show')) showVent();
});
}