feat: B — extract drug data to public/data/drugs.json (schema v1.0)

Moved 43 weight-based drug entries out of calculators.js string literals
into a structured JSON reference. Renderers now iterate JSON → S.drugRow.

Sections extracted (43 drugs):
- anaphylaxis (7), sedation (11), agitation (11), antiemetics (7), seizure (7)

Out of scope: seizure refractory drips (compound strings), NRP, antimicrobial
empiric regimens (already structured), airway/cardiac/tox/trauma/respiratory
/sepsis/burns sections (fine as-is or no drugs).

New:
- public/data/drugs.json — { version, last_reviewed, sections.<key>.drugs[] }
- public/js/drugs-loader.js — fetches JSON on boot, exposes window._DRUGS +
  window._DRUGS_READY Promise. Non-fatal: each calc function has a matching
  *_FALLBACK constant so a 404 on drugs.json doesn't break anything.

Schema:
- dose_mg_per_kg | dose_mg_per_kg_low/high (for ranges) | max_mg | unit |
  route | notes | source | optional special-cases (weight bands, age-dep text)

Added one unit test asserting drugs.json loads + has all 5 sections with
non-empty drugs arrays. 37/37 unit tests + 26/26 Playwright e2e tests pass.
This commit is contained in:
Daniel 2026-04-20 04:23:18 +02:00
parent cd131e0b02
commit 54285865d5
7 changed files with 802 additions and 63 deletions

1
.gitignore vendored
View file

@ -3,6 +3,7 @@ node_modules/
.env.local
.env.production
data/
!public/data/
*.db
*.db-journal
*.db-wal

498
public/data/drugs.json Normal file
View file

@ -0,0 +1,498 @@
{
"version": "1.0",
"last_reviewed": "2026-04-20",
"sections": {
"anaphylaxis": {
"drugs": [
{
"name": "Epinephrine IM",
"dose_mg_per_kg": 0.01,
"max_mg": 0.5,
"unit": "mg",
"route": "IM",
"notes": "1:1000 (1 mg/mL), lateral thigh. May repeat q5-15 min.",
"concentration": "1:1000",
"source": "ASCIA 2021"
},
{
"name": "Normal saline bolus",
"dose_mg_per_kg": 20,
"max_mg": null,
"unit": "mL",
"route": "IV/IO",
"notes": "Repeat up to 60 mL/kg for hypotension",
"source": "ASCIA 2021"
},
{
"name": "Diphenhydramine",
"dose_mg_per_kg": 1.25,
"max_mg": 50,
"unit": "mg",
"route": "IV/IM/PO",
"notes": "H1 blocker. NOT first-line — adjunct only",
"source": "AAP/ACAAI"
},
{
"name": "Ranitidine",
"dose_mg_per_kg": 1,
"max_mg": 50,
"unit": "mg",
"route": "IV over 5 min",
"notes": "H2 blocker. Optional adjunct",
"source": "AAP/ACAAI"
},
{
"name": "Dexamethasone",
"dose_mg_per_kg": 0.6,
"max_mg": 16,
"unit": "mg",
"route": "IV/IM/PO",
"notes": "Prevents biphasic reaction (4-6 hrs later)",
"source": "AAP/ACAAI"
},
{
"name": "Methylprednisolone",
"dose_mg_per_kg": 2,
"max_mg": 125,
"unit": "mg",
"route": "IV",
"notes": "Alternative steroid",
"source": "AAP/ACAAI"
},
{
"name": "Glucagon",
"dose_mg_per_kg": 0.02,
"max_mg": 1,
"unit": "mg",
"route": "IV/IM",
"notes": "For patients on beta-blockers not responding to epi",
"source": "ASCIA 2021"
}
]
},
"sedation": {
"drugs": [
{
"name": "Ketamine IV",
"display": "Ketamine",
"dose_mg_per_kg_low": 1.5,
"dose_mg_per_kg_high": 2,
"max_mg_low": 100,
"max_mg_high": 150,
"unit": "mg",
"route": "IV",
"onset": "1 min",
"duration": "15-30 min",
"notes": "Dissociative. Preserves airway reflexes.",
"source": "AAP sedation guideline"
},
{
"name": "Ketamine IM",
"display": "",
"dose_mg_per_kg_low": 4,
"dose_mg_per_kg_high": 5,
"max_mg_low": 300,
"max_mg_high": 400,
"unit": "mg",
"route": "IM",
"onset": "3-5 min",
"duration": "30-60 min",
"notes": "Give atropine 0.01 mg/kg to reduce secretions.",
"source": "AAP sedation guideline"
},
{
"name": "Midazolam IV",
"display": "Midazolam",
"dose_mg_per_kg_low": 0.05,
"dose_mg_per_kg_high": 0.1,
"max_mg_low": 2,
"max_mg_high": 5,
"unit": "mg",
"route": "IV",
"onset": "2-3 min",
"duration": "30-60 min",
"notes": "Anxiolysis. Titrate q3-5 min.",
"source": "AAP sedation guideline"
},
{
"name": "Midazolam IN/PO",
"display": "",
"dose_mg_per_kg": 0.5,
"max_mg": 20,
"unit": "mg",
"route": "IN/PO",
"onset": "10-15 min",
"duration": "30-60 min",
"notes": "IN (max 10 mg / naris) or PO (max 20 mg).",
"source": "AAP sedation guideline"
},
{
"name": "Propofol",
"display": "Propofol",
"dose_mg_per_kg": 1,
"max_mg": 40,
"unit": "mg",
"route": "IV",
"onset": "30 sec",
"duration": "5-10 min",
"notes": "Short procedures. Causes apnea — manage airway.",
"source": "AAP sedation guideline"
},
{
"name": "Fentanyl IV",
"display": "Fentanyl",
"dose_mg_per_kg": 1,
"max_mg": 100,
"unit": "mcg",
"route": "IV",
"onset": "2-3 min",
"duration": "30-60 min",
"notes": "Analgesic. Often combined with midazolam.",
"source": "AAP sedation guideline"
},
{
"name": "Fentanyl IN",
"display": "",
"dose_mg_per_kg": 2,
"max_mg": 100,
"unit": "mcg",
"route": "IN",
"onset": "5-10 min",
"duration": "30-60 min",
"notes": "Intranasal.",
"source": "AAP sedation guideline"
},
{
"name": "Nitrous oxide",
"display": "Nitrous oxide",
"dose_mg_per_kg": null,
"max_mg": null,
"unit": "mix",
"dose_display": "50:50 or 70:30 mix",
"route": "Inhaled",
"onset": "2-5 min",
"duration": "5 min off",
"notes": "Self-administered via demand valve. Anxiolysis + mild analgesia.",
"source": "AAP sedation guideline"
},
{
"name": "Dexmedetomidine IN",
"display": "Dexmedetomidine",
"dose_mg_per_kg_low": 2,
"dose_mg_per_kg_high": 3,
"max_mg_low": 100,
"max_mg_high": null,
"unit": "mcg",
"route": "IN",
"onset": "15-30 min",
"duration": "60-90 min",
"notes": "Intranasal. No respiratory depression. Good for imaging.",
"source": "AAP sedation guideline"
},
{
"name": "Naloxone",
"display": "Naloxone",
"dose_mg_per_kg": 0.1,
"max_mg": 2,
"unit": "mg",
"route": "IV/IM/IN",
"reverses": "Opioids (fentanyl, morphine)",
"notes": "Max 2 mg. Repeat q2-3 min. Duration shorter than opioids — monitor for re-sedation.",
"source": "AAP sedation guideline"
},
{
"name": "Flumazenil",
"display": "Flumazenil",
"dose_mg_per_kg": 0.01,
"max_mg": 0.2,
"unit": "mg",
"route": "IV",
"reverses": "Benzodiazepines (midazolam)",
"notes": "Max 0.2 mg single dose. Repeat q1 min to max 1 mg total. Risk of seizures — use cautiously.",
"source": "AAP sedation guideline"
}
]
},
"agitation": {
"drugs": [
{
"name": "Lorazepam PO",
"display": "Lorazepam",
"step": 2,
"dose_mg_per_kg": 0.05,
"max_mg": 2,
"unit": "mg",
"route": "PO",
"notes": "0.05 mg/kg. May repeat in 30 min.",
"source": "AAP/ACEP agitation"
},
{
"name": "Midazolam PO",
"display": "Midazolam",
"step": 2,
"dose_mg_per_kg": 0.5,
"max_mg": 10,
"unit": "mg",
"route": "PO",
"notes": "0.5 mg/kg (max 10 mg)",
"source": "AAP/ACEP agitation"
},
{
"name": "Midazolam IN",
"display": "Midazolam",
"step": 2,
"dose_mg_per_kg": 0.3,
"max_mg": 10,
"unit": "mg",
"route": "IN (5 mg/mL)",
"notes": "0.3 mg/kg split between nares (max 10 mg)",
"source": "AAP/ACEP agitation"
},
{
"name": "Olanzapine (ODT)",
"display": "Olanzapine (ODT)",
"step": 2,
"dose_mg_per_kg": null,
"max_mg": null,
"unit": "mg",
"route": "PO / ODT",
"dose_display_if_under_30kg": "2.5-5 mg",
"dose_display_if_30kg_or_more": "5-10 mg",
"notes": "Age ≥6 yr. Avoid IM + benzo combo (risk of resp depression).",
"source": "AAP/ACEP agitation"
},
{
"name": "Diphenhydramine PO",
"display": "Diphenhydramine",
"step": 2,
"dose_mg_per_kg": 1,
"max_mg": 50,
"unit": "mg",
"route": "PO",
"notes": "1 mg/kg. Adjunct only; sedating.",
"source": "AAP/ACEP agitation"
},
{
"name": "Midazolam IV/IM",
"display": "Midazolam",
"step": 3,
"dose_mg_per_kg": 0.1,
"max_mg": 5,
"unit": "mg",
"route": "IV / IM",
"notes": "0.05-0.1 mg/kg IV, 0.1-0.15 mg/kg IM (max 10 mg)",
"source": "AAP/ACEP agitation"
},
{
"name": "Lorazepam IV/IM",
"display": "Lorazepam",
"step": 3,
"dose_mg_per_kg": 0.1,
"max_mg": 4,
"unit": "mg",
"route": "IV / IM",
"notes": "0.05-0.1 mg/kg (max 4 mg). May cause resp depression.",
"source": "AAP/ACEP agitation"
},
{
"name": "Haloperidol",
"display": "Haloperidol",
"step": 3,
"dose_mg_per_kg": 0.05,
"max_mg": null,
"unit": "mg",
"route": "IM / IV",
"dose_display_if_under_40kg": "0.025-0.075 mg/kg",
"dose_display_if_40kg_or_more": "2.5-5 mg",
"notes": "Avoid &lt;3 yr. Risk: QT, EPS, NMS. ECG if repeated.",
"source": "AAP/ACEP agitation"
},
{
"name": "Olanzapine IM",
"display": "Olanzapine",
"step": 3,
"dose_mg_per_kg": null,
"max_mg": null,
"unit": "mg",
"route": "IM",
"dose_display_if_under_40kg": "2.5-5 mg",
"dose_display_if_40kg_or_more": "5-10 mg",
"notes": "Avoid benzo co-administration (resp depression, hypotension)",
"source": "AAP/ACEP agitation"
},
{
"name": "Ketamine IM",
"display": "Ketamine",
"step": 3,
"dose_mg_per_kg": 4,
"max_mg": 500,
"unit": "mg",
"route": "IM",
"notes": "4-5 mg/kg IM (rescue for severe excited delirium). Monitor airway.",
"source": "AAP/ACEP agitation"
},
{
"name": "Droperidol",
"display": "Droperidol",
"step": 3,
"dose_mg_per_kg": 0.05,
"max_mg": null,
"unit": "mg",
"route": "IM / IV",
"dose_display_prefix": "0.03-0.07 mg/kg",
"notes": "Effective but QT concern — get ECG.",
"source": "AAP/ACEP agitation"
}
]
},
"antiemetics": {
"drugs": [
{
"name": "Ondansetron",
"dose_mg_per_kg": 0.15,
"max_mg": 8,
"unit": "mg",
"route": "PO / ODT / IV",
"weight_band_dose": {
"under_15kg": "2 mg",
"15_to_30kg": "4 mg",
"30kg_or_more": "8 mg"
},
"notes": "First-line. Max single 8 mg. Repeat q8h. QT prolongation — avoid with other QT drugs. &lt;6 mo: limited data.",
"source": "AAP gastroenteritis; Lexicomp"
},
{
"name": "Metoclopramide",
"dose_mg_per_kg": 0.15,
"max_mg": 10,
"unit": "mg",
"route": "IV / IM / PO",
"notes": "0.1-0.15 mg/kg (max 10 mg). Give with diphenhydramine to prevent EPS / dystonia.",
"source": "Lexicomp"
},
{
"name": "Dimenhydrinate",
"dose_mg_per_kg": 1.25,
"max_mg": 50,
"unit": "mg",
"route": "PO / IV / IM / PR",
"notes": "1.25 mg/kg (max 50 mg) q6h. ≥2 yr.",
"source": "Lexicomp"
},
{
"name": "Diphenhydramine",
"dose_mg_per_kg": 1,
"max_mg": 50,
"unit": "mg",
"route": "PO / IV / IM",
"notes": "1 mg/kg (max 50 mg) q6h. Adjunct, sedating.",
"source": "Lexicomp"
},
{
"name": "Promethazine",
"dose_mg_per_kg": 0.25,
"max_mg": 25,
"unit": "mg",
"route": "PO / IV / IM",
"notes": "0.25-1 mg/kg. <strong>CONTRAINDICATED &lt;2 yr</strong> (resp depression). Tissue injury if IV extrav.",
"source": "Lexicomp"
},
{
"name": "Dexamethasone",
"dose_mg_per_kg": 0.15,
"max_mg": 10,
"unit": "mg",
"route": "IV / PO",
"notes": "Adjunct, esp. chemo-induced. 0.15 mg/kg (max 10 mg).",
"source": "Lexicomp"
},
{
"name": "Scopolamine patch",
"dose_mg_per_kg": null,
"max_mg": null,
"unit": "mg",
"dose_display": "1.5 mg patch",
"route": "Transdermal",
"notes": "≥12 yr. Motion sickness. Apply 4h before exposure.",
"source": "Lexicomp"
}
]
},
"seizure": {
"drugs": [
{
"name": "Lorazepam",
"phase": "1st benzo",
"dose_mg_per_kg": 0.1,
"max_mg": 4,
"unit": "mg",
"route": "IV / IO",
"label_suffix": " <span style=\"color:#065f46;font-weight:600;\">(IV preferred)</span>",
"notes": "Slow push over 1-2 min.",
"source": "AES 2016; ESETT 2019"
},
{
"name": "Midazolam",
"phase": "1st benzo",
"dose_mg_per_kg": 0.2,
"max_mg": 10,
"unit": "mg",
"route": "IM / IN / buccal",
"notes": "Use 5 mg/mL concentrate for IN; split between nares.",
"source": "AES 2016; ESETT 2019"
},
{
"name": "Diazepam",
"phase": "1st benzo",
"dose_mg_per_kg": 0.5,
"max_mg": 20,
"unit": "mg",
"route": "PR",
"notes": "Only if no IV/IM/IN access.",
"source": "AES 2016"
},
{
"name": "Levetiracetam",
"phase": "2nd-line",
"dose_mg_per_kg": 60,
"max_mg": 4500,
"unit": "mg",
"route": "IV over 5-15 min",
"label_suffix": " <span style=\"color:#065f46;font-weight:600;\">(preferred)</span>",
"notes": "Best tolerability. No ECG monitoring needed.",
"source": "ESETT 2019"
},
{
"name": "Fosphenytoin",
"phase": "2nd-line",
"dose_mg_per_kg": 20,
"max_mg": 1500,
"unit": "mg PE",
"route": "IV over 10 min",
"notes": "Max rate 3 mg PE/kg/min. Monitor ECG, BP. Avoid if cardiac dysrhythmia.",
"source": "ESETT 2019"
},
{
"name": "Valproic acid",
"phase": "2nd-line",
"dose_mg_per_kg": 40,
"max_mg": 3000,
"unit": "mg",
"route": "IV over 10 min",
"notes": "<strong>Avoid &lt;2 yr, hepatic / mitochondrial disease, pregnancy.</strong>",
"source": "ESETT 2019"
},
{
"name": "Phenobarbital",
"phase": "2nd-line",
"dose_mg_per_kg": 20,
"max_mg": 1000,
"unit": "mg",
"route": "IV over 20 min",
"notes": "Last-choice 2nd line. High risk of resp depression + hypotension.",
"source": "AES 2016"
}
]
}
}
}

View file

@ -16,6 +16,7 @@
<section id="calculators-tab" class="tab-content active" data-component="calculators"></section>
<script defer src="/js/calc-math.js"></script>
<script defer src="/js/drugs-loader.js"></script>
<script defer src="/js/calculators.js"></script>
<script defer src="/js/e2e-bootstrap.js"></script>
</body>

View file

@ -445,6 +445,7 @@
<script defer src="/js/memories.js"></script>
<script defer src="/js/documents.js"></script>
<script defer src="/js/calc-math.js"></script>
<script defer src="/js/drugs-loader.js"></script>
<script defer src="/js/calculators.js"></script>
<script defer src="/js/learningHub.js"></script>
<script defer src="/js/admin.js"></script>

View file

@ -2565,6 +2565,44 @@
'</div>';
}
// Fallback inline data — mirrors the non-refractory seizure entries in
// drugs.json. Refractory infusion drips are kept inline below because
// their dose strings (bolus → gtt range) don't map cleanly to the
// single-row JSON schema.
var SEIZURE_FALLBACK = [
{ name: 'Lorazepam', phase: '1st benzo', dose_mg_per_kg: 0.1, max_mg: 4, unit: 'mg', route: 'IV / IO', label_suffix: ' <span style="color:#065f46;font-weight:600;">(IV preferred)</span>', notes: 'Slow push over 1-2 min.' },
{ name: 'Midazolam', phase: '1st benzo', dose_mg_per_kg: 0.2, max_mg: 10, unit: 'mg', route: 'IM / IN / buccal', notes: 'Use 5 mg/mL concentrate for IN; split between nares.' },
{ name: 'Diazepam', phase: '1st benzo', dose_mg_per_kg: 0.5, max_mg: 20, unit: 'mg', route: 'PR', notes: 'Only if no IV/IM/IN access.' },
{ name: 'Levetiracetam', phase: '2nd-line', dose_mg_per_kg: 60, max_mg: 4500, unit: 'mg', route: 'IV over 5-15 min', label_suffix: ' <span style="color:#065f46;font-weight:600;">(preferred)</span>', notes: 'Best tolerability. No ECG monitoring needed.' },
{ name: 'Fosphenytoin', phase: '2nd-line', dose_mg_per_kg: 20, max_mg: 1500, unit: 'mg PE', route: 'IV over 10 min', notes: 'Max rate 3 mg PE/kg/min. Monitor ECG, BP. Avoid if cardiac dysrhythmia.' },
{ name: 'Valproic acid', phase: '2nd-line', dose_mg_per_kg: 40, max_mg: 3000, unit: 'mg', route: 'IV over 10 min', notes: '<strong>Avoid &lt;2 yr, hepatic / mitochondrial disease, pregnancy.</strong>' },
{ name: 'Phenobarbital', phase: '2nd-line', dose_mg_per_kg: 20, max_mg: 1000, unit: 'mg', route: 'IV over 20 min', notes: 'Last-choice 2nd line. High risk of resp depression + hypotension.' }
];
function seizureDrugs() {
var s = window._DRUGS && window._DRUGS.sections && window._DRUGS.sections.seizure;
if (s && s.drugs && s.drugs.length) {
return s.drugs.map(function(d) {
var fb = SEIZURE_FALLBACK.filter(function(x) { return x.name === d.name; })[0] || {};
return Object.assign({}, fb, d);
});
}
return SEIZURE_FALLBACK;
}
// Build the joined S.drugRow output for one phase ("1st benzo" / "2nd-line").
function seizureRowsByPhase(wt, phase) {
var S = window._EM;
return seizureDrugs().filter(function(dg) { return dg.phase === phase; }).map(function(dg) {
var label = dg.name + (dg.label_suffix || '');
var unitArg = dg.unit && dg.unit !== 'mg' ? ' ' + dg.unit : undefined;
var dose = unitArg
? S.dStr(wt, dg.dose_mg_per_kg, dg.max_mg, unitArg)
: S.dStr(wt, dg.dose_mg_per_kg, dg.max_mg);
return S.drugRow(label, dose, dg.route, dg.notes);
}).join('');
}
function calcSeizure() {
var wt = parseFloat(document.getElementById('seizure-weight').value);
var resultDiv = document.getElementById('seizure-result');
@ -2585,11 +2623,7 @@
// 5 min — 1ST BENZO
html += stepRow('5 min', '#8b5cf6', '1st benzodiazepine (give once, pick by access)',
S.drugTable(
S.drugRow('Lorazepam <span style="color:#065f46;font-weight:600;">(IV preferred)</span>', S.dStr(wt, 0.1, 4), 'IV / IO', 'Slow push over 1-2 min.') +
S.drugRow('Midazolam', S.dStr(wt, 0.2, 10), 'IM / IN / buccal', 'Use 5 mg/mL concentrate for IN; split between nares.') +
S.drugRow('Diazepam', S.dStr(wt, 0.5, 20), 'PR', 'Only if no IV/IM/IN access.')
)
S.drugTable(seizureRowsByPhase(wt, '1st benzo'))
);
// 10 min — 2ND BENZO
@ -2599,12 +2633,7 @@
// 20 min — 2ND-LINE
html += stepRow('20 min', '#f59e0b', '2nd-line anti-epileptic — pick one (ESETT: equivalent efficacy)',
S.drugTable(
S.drugRow('Levetiracetam <span style="color:#065f46;font-weight:600;">(preferred)</span>', S.dStr(wt, 60, 4500), 'IV over 5-15 min', 'Best tolerability. No ECG monitoring needed.') +
S.drugRow('Fosphenytoin', S.dStr(wt, 20, 1500, ' mg PE'), 'IV over 10 min', 'Max rate 3 mg PE/kg/min. Monitor ECG, BP. Avoid if cardiac dysrhythmia.') +
S.drugRow('Valproic acid', S.dStr(wt, 40, 3000), 'IV over 10 min', '<strong>Avoid &lt;2 yr, hepatic / mitochondrial disease, pregnancy.</strong>') +
S.drugRow('Phenobarbital', S.dStr(wt, 20, 1000), 'IV over 20 min', 'Last-choice 2nd line. High risk of resp depression + hypotension.')
)
S.drugTable(seizureRowsByPhase(wt, '2nd-line'))
);
// 30 min — 2nd of 2nd-line (optional)
@ -2646,6 +2675,28 @@
function d(wt, mgPerKg, max) { var v = Math.round(wt * mgPerKg * 100) / 100; return max ? Math.min(v, max) : v; }
// Lookup a drug entry (by name) from window._DRUGS.sections.anaphylaxis,
// falling back to a hardcoded record so rendering stays identical if the
// JSON fetch hasn't resolved (or failed). Returns an object exposing the
// per-kg + max fields needed by S.dStr.
var ANAPH_FALLBACK = {
'Epinephrine IM': { dose_mg_per_kg: 0.01, max_mg: 0.5, unit: 'mg' },
'Normal saline bolus': { dose_mg_per_kg: 20, max_mg: null, unit: 'mL' },
'Diphenhydramine': { dose_mg_per_kg: 1.25, max_mg: 50, unit: 'mg' },
'Ranitidine': { dose_mg_per_kg: 1, max_mg: 50, unit: 'mg' },
'Dexamethasone': { dose_mg_per_kg: 0.6, max_mg: 16, unit: 'mg' },
'Methylprednisolone': { dose_mg_per_kg: 2, max_mg: 125, unit: 'mg' },
'Glucagon': { dose_mg_per_kg: 0.02, max_mg: 1, unit: 'mg' }
};
function anaphDrug(name) {
var s = window._DRUGS && window._DRUGS.sections && window._DRUGS.sections.anaphylaxis;
if (s && s.drugs) {
var hit = s.drugs.filter(function(x) { return x.name === name; })[0];
if (hit) return hit;
}
return ANAPH_FALLBACK[name] || {};
}
function calcAnaphylaxis() {
var wt = parseFloat(document.getElementById('anaph-weight').value);
var age = document.getElementById('anaph-age').value;
@ -2653,14 +2704,22 @@
if (!wt) { resultDiv.innerHTML = '<p style="color:var(--red);font-size:13px;">Enter weight (kg).</p>'; return; }
var S = window._EM;
var epiDose = d(wt, 0.01, 0.5);
var epi = anaphDrug('Epinephrine IM');
var fluids = anaphDrug('Normal saline bolus');
var dph = anaphDrug('Diphenhydramine');
var rnt = anaphDrug('Ranitidine');
var dex = anaphDrug('Dexamethasone');
var mpn = anaphDrug('Methylprednisolone');
var glc = anaphDrug('Glucagon');
var epiDose = d(wt, epi.dose_mg_per_kg, epi.max_mg);
var epiVol = Math.round(epiDose * 10) / 10; // 1:1000 = 1 mg/mL
var autoInjector = wt < 10 ? 'Draw up manually' : wt <= 25 ? 'EpiPen Jr / Auvi-Q 0.15 mg' : 'EpiPen / Auvi-Q 0.3 mg';
var html = '<div style="padding:14px;border-radius:10px;background:#fee2e2;border:2px solid #ef4444;margin-bottom:16px;">';
html += '<div style="font-size:18px;font-weight:700;color:#dc2626;margin-bottom:6px;"><i class="fas fa-triangle-exclamation"></i> STEP 1: Epinephrine IM — GIVE IMMEDIATELY</div>';
html += '<div style="font-size:15px;font-weight:600;color:var(--g800);margin-bottom:4px;">Epinephrine 1:1000 (1 mg/mL): <span style="color:#dc2626;">' + epiDose + ' mg (' + epiVol + ' mL)</span> IM to lateral thigh</div>';
html += '<div style="font-size:13px;color:var(--g600);">0.01 mg/kg (max 0.5 mg) | Auto-injector: ' + autoInjector + '</div>';
html += '<div style="font-size:13px;color:var(--g600);">' + epi.dose_mg_per_kg + ' mg/kg (max ' + epi.max_mg + ' mg) | Auto-injector: ' + autoInjector + '</div>';
html += '<div style="font-size:12px;color:#991b1b;margin-top:6px;">May repeat every 5-15 minutes if symptoms persist. No contraindications in anaphylaxis.</div>';
html += '</div>';
@ -2668,14 +2727,14 @@
html += '<tr><td style="font-weight:600;color:var(--blue);">STEP 2</td><td style="font-weight:600;">Position</td><td>Supine + legs elevated</td><td>—</td><td style="font-size:12px;color:var(--g500);">If dyspneic: sitting position. If vomiting: recovery position</td></tr>';
html += '<tr><td style="font-weight:600;color:var(--blue);">STEP 3</td><td style="font-weight:600;">O2</td><td>High flow 10-15 L/min</td><td>Face mask</td><td style="font-size:12px;color:var(--g500);">100% O2. Prepare for airway management</td></tr>';
html += '<tr><td style="font-weight:600;color:var(--blue);">STEP 4</td><td style="font-weight:600;">IV fluids</td><td>NS ' + S.dStr(wt, 20, null, ' mL') + ' bolus</td><td>IV/IO</td><td style="font-size:12px;color:var(--g500);">Repeat up to 60 mL/kg for hypotension</td></tr>';
html += '<tr><td style="font-weight:600;color:var(--blue);">STEP 5</td><td style="font-weight:600;">Diphenhydramine</td><td>' + S.dStr(wt, 1.25, 50) + '</td><td>IV/IM/PO</td><td style="font-size:12px;color:var(--g500);">H1 blocker. NOT first-line — adjunct only</td></tr>';
html += '<tr><td></td><td style="font-weight:600;">Ranitidine</td><td>' + S.dStr(wt, 1, 50) + '</td><td>IV over 5 min</td><td style="font-size:12px;color:var(--g500);">H2 blocker. Optional adjunct</td></tr>';
html += '<tr><td style="font-weight:600;color:var(--blue);">STEP 6</td><td style="font-weight:600;">Dexamethasone</td><td>' + S.dStr(wt, 0.6, 16) + '</td><td>IV/IM/PO</td><td style="font-size:12px;color:var(--g500);">Prevents biphasic reaction (4-6 hrs later)</td></tr>';
html += '<tr><td></td><td style="font-weight:600;">Methylprednisolone</td><td>' + S.dStr(wt, 2, 125) + '</td><td>IV</td><td style="font-size:12px;color:var(--g500);">Alternative steroid</td></tr>';
html += '<tr><td style="font-weight:600;color:var(--blue);">STEP 4</td><td style="font-weight:600;">IV fluids</td><td>NS ' + S.dStr(wt, fluids.dose_mg_per_kg, fluids.max_mg, ' mL') + ' bolus</td><td>IV/IO</td><td style="font-size:12px;color:var(--g500);">Repeat up to 60 mL/kg for hypotension</td></tr>';
html += '<tr><td style="font-weight:600;color:var(--blue);">STEP 5</td><td style="font-weight:600;">Diphenhydramine</td><td>' + S.dStr(wt, dph.dose_mg_per_kg, dph.max_mg) + '</td><td>IV/IM/PO</td><td style="font-size:12px;color:var(--g500);">H1 blocker. NOT first-line — adjunct only</td></tr>';
html += '<tr><td></td><td style="font-weight:600;">Ranitidine</td><td>' + S.dStr(wt, rnt.dose_mg_per_kg, rnt.max_mg) + '</td><td>IV over 5 min</td><td style="font-size:12px;color:var(--g500);">H2 blocker. Optional adjunct</td></tr>';
html += '<tr><td style="font-weight:600;color:var(--blue);">STEP 6</td><td style="font-weight:600;">Dexamethasone</td><td>' + S.dStr(wt, dex.dose_mg_per_kg, dex.max_mg) + '</td><td>IV/IM/PO</td><td style="font-size:12px;color:var(--g500);">Prevents biphasic reaction (4-6 hrs later)</td></tr>';
html += '<tr><td></td><td style="font-weight:600;">Methylprednisolone</td><td>' + S.dStr(wt, mpn.dose_mg_per_kg, mpn.max_mg) + '</td><td>IV</td><td style="font-size:12px;color:var(--g500);">Alternative steroid</td></tr>';
html += '<tr style="background:#fef3c7;"><td style="font-weight:600;color:#dc2626;">IF REFRACTORY</td><td style="font-weight:600;">Epinephrine gtt</td><td>0.1-1 mcg/kg/min</td><td>IV infusion</td><td style="font-size:12px;color:var(--g500);">For persistent hypotension despite fluids + IM epi</td></tr>';
html += '<tr style="background:#fef3c7;"><td></td><td style="font-weight:600;">Glucagon</td><td>' + S.dStr(wt, 0.02, 1) + '</td><td>IV/IM</td><td style="font-size:12px;color:var(--g500);">For patients on beta-blockers not responding to epi</td></tr>';
html += '<tr style="background:#fef3c7;"><td></td><td style="font-weight:600;">Glucagon</td><td>' + S.dStr(wt, glc.dose_mg_per_kg, glc.max_mg) + '</td><td>IV/IM</td><td style="font-size:12px;color:var(--g500);">For patients on beta-blockers not responding to epi</td></tr>';
html += '</tbody></table></div>';
html += '<div style="padding:10px 12px;background:var(--amber-light);border-radius:6px;font-size:12px;color:#92400e;"><strong>Observe minimum 4-6 hours</strong> after last dose of epinephrine (biphasic reactions occur in 5-20% of cases). Discharge with EpiPen prescription and anaphylaxis action plan. Refer to allergist.</div>';
@ -2694,39 +2753,95 @@
function d(wt, mgPerKg, max) { var v = Math.round(wt * mgPerKg * 100) / 100; return max ? Math.min(v, max) : v; }
// Fallback inline data — mirrors drugs.json sedation section. The first
// table's rows group by drug (Ketamine has both IV + IM lines), hence the
// display vs name split. `reverses` marks the final 2 rows as reversal
// agents shown in a separate table.
var SED_FALLBACK = [
{ name: 'Ketamine IV', display: 'Ketamine', dose_mg_per_kg_low: 1.5, dose_mg_per_kg_high: 2, max_mg_low: 100, max_mg_high: 150, unit: 'mg', route: 'IV', onset: '1 min', duration: '15-30 min', notes: 'Dissociative. Preserves airway reflexes.' },
{ name: 'Ketamine IM', display: '', dose_mg_per_kg_low: 4, dose_mg_per_kg_high: 5, max_mg_low: 300, max_mg_high: 400, unit: 'mg', route: 'IM', onset: '3-5 min', duration: '30-60 min', notes: 'Give atropine 0.01 mg/kg to reduce secretions.' },
{ name: 'Midazolam IV', display: 'Midazolam', dose_mg_per_kg_low: 0.05, dose_mg_per_kg_high: 0.1, max_mg_low: 2, max_mg_high: 5, unit: 'mg', route: 'IV', onset: '2-3 min', duration: '30-60 min', notes: 'Anxiolysis. Titrate q3-5 min.' },
{ name: 'Midazolam IN/PO', display: '', dose_mg_per_kg: 0.5, max_mg: 20, unit: 'mg', route: 'IN/PO', onset: '10-15 min', duration: '30-60 min', notes: 'IN (max 10 mg / naris) or PO (max 20 mg).' },
{ name: 'Propofol', display: 'Propofol', dose_mg_per_kg: 1, max_mg: 40, unit: 'mg', route: 'IV', onset: '30 sec', duration: '5-10 min', notes: 'Short procedures. Causes apnea — manage airway.' },
{ name: 'Fentanyl IV', display: 'Fentanyl', dose_mg_per_kg: 1, max_mg: 100, unit: 'mcg', route: 'IV', onset: '2-3 min', duration: '30-60 min', notes: 'Analgesic. Often combined with midazolam.' },
{ name: 'Fentanyl IN', display: '', dose_mg_per_kg: 2, max_mg: 100, unit: 'mcg', route: 'IN', onset: '5-10 min', duration: '30-60 min', notes: 'Intranasal.' },
{ name: 'Nitrous oxide', display: 'Nitrous oxide', dose_display: '50:50 or 70:30 mix', unit: 'mix', route: 'Inhaled',onset: '2-5 min', duration: '5 min off', notes: 'Self-administered via demand valve. Anxiolysis + mild analgesia.' },
{ name: 'Dexmedetomidine IN', display: 'Dexmedetomidine', dose_mg_per_kg_low: 2, dose_mg_per_kg_high: 3, max_mg_low: 100, max_mg_high: null, unit: 'mcg', route: 'IN', onset: '15-30 min', duration: '60-90 min', notes: 'Intranasal. No respiratory depression. Good for imaging.' },
{ name: 'Naloxone', display: 'Naloxone', dose_mg_per_kg: 0.1, max_mg: 2, unit: 'mg', route: 'IV/IM/IN', reverses: 'Opioids (fentanyl, morphine)', notes: 'Max 2 mg. Repeat q2-3 min. Duration shorter than opioids — monitor for re-sedation.' },
{ name: 'Flumazenil', display: 'Flumazenil', dose_mg_per_kg: 0.01, max_mg: 0.2, unit: 'mg', route: 'IV', reverses: 'Benzodiazepines (midazolam)', notes: 'Max 0.2 mg single dose. Repeat q1 min to max 1 mg total. Risk of seizures — use cautiously.' }
];
function sedDrugs() {
var s = window._DRUGS && window._DRUGS.sections && window._DRUGS.sections.sedation;
if (s && s.drugs && s.drugs.length) {
return s.drugs.map(function(d) {
var fb = SED_FALLBACK.filter(function(x) { return x.name === d.name; })[0] || {};
return Object.assign({}, fb, d);
});
}
return SED_FALLBACK;
}
// perKg footer that matches the original inline format exactly.
function sedPerKg(lo, hi, unit) {
unit = unit || 'mg';
return ' <span style="color:var(--g500);font-size:11px;">(' + lo + (hi != null ? '-' + hi : '') + ' ' + unit + '/kg)</span>';
}
// Build the Dose cell text for a sedation drug (supports low/high range
// and single-dose entries; honors custom dose_display for fixed doses).
function sedDoseCell(wt, dg) {
if (dg.dose_display) return dg.dose_display;
var unitLabel = dg.unit === 'mcg' ? ' mcg' : (dg.unit === 'mix' ? '' : ' mg');
var perKgUnit = dg.unit === 'mcg' ? 'mcg' : 'mg';
if (dg.dose_mg_per_kg_low != null) {
var lo = d(wt, dg.dose_mg_per_kg_low, dg.max_mg_low);
if (dg.dose_mg_per_kg_high != null) {
var hi = d(wt, dg.dose_mg_per_kg_high, dg.max_mg_high);
return lo + '-' + hi + unitLabel + sedPerKg(dg.dose_mg_per_kg_low, dg.dose_mg_per_kg_high, perKgUnit);
}
return lo + unitLabel + sedPerKg(dg.dose_mg_per_kg_low, null, perKgUnit);
}
if (dg.dose_mg_per_kg != null) {
var v = d(wt, dg.dose_mg_per_kg, dg.max_mg);
return v + unitLabel + sedPerKg(dg.dose_mg_per_kg, null, perKgUnit);
}
return '';
}
function sedRow(wt, dg, showReverses) {
var displayName = dg.display == null ? dg.name : dg.display;
var nameCell = displayName
? '<td style="font-weight:600;">' + displayName + '</td>'
: '<td></td>';
var dose = sedDoseCell(wt, dg);
if (showReverses) {
return '<tr>' + nameCell + '<td>' + dose + '</td><td>' + dg.route + '</td><td>' + (dg.reverses || '') + '</td><td style="font-size:12px;color:var(--g500);">' + dg.notes + '</td></tr>';
}
return '<tr>' + nameCell + '<td>' + dose + '</td><td>' + dg.route + '</td><td>' + (dg.onset || '') + '</td><td>' + (dg.duration || '') + '</td><td style="font-size:12px;color:var(--g500);">' + dg.notes + '</td></tr>';
}
function calcSedation() {
var wt = parseFloat(document.getElementById('sed-weight').value);
var resultDiv = document.getElementById('sed-result');
if (!wt) { resultDiv.innerHTML = '<p style="color:var(--red);font-size:13px;">Enter weight (kg).</p>'; return; }
var all = sedDrugs();
var agents = all.filter(function(dg) { return !dg.reverses; });
var reversals = all.filter(function(dg) { return !!dg.reverses; });
var html = '<div style="padding:10px 14px;border-radius:8px;background:var(--purple-light);border:1.5px solid var(--purple);margin-bottom:12px;"><span style="font-size:14px;font-weight:700;color:var(--purple);">Procedural Sedation — ' + wt + ' kg patient</span></div>';
// Sedation agents
html += '<h4 style="font-size:14px;font-weight:700;color:var(--g800);margin:0 0 8px;">Sedation Agents</h4>';
html += '<div style="overflow-x:auto;-webkit-overflow-scrolling:touch;margin-bottom:16px;"><table style="width:100%;min-width:640px;border-collapse:collapse;font-size:13px;"><thead><tr style="background:var(--g100);"><th style="text-align:left;padding:6px 8px;">Drug</th><th style="padding:6px 8px;">Dose</th><th style="padding:6px 8px;">Route</th><th style="padding:6px 8px;">Onset</th><th style="padding:6px 8px;">Duration</th><th style="padding:6px 8px;">Notes</th></tr></thead><tbody>';
// Small helper: display dose ranges with per-kg footer consistently.
var perKg = function(lo, hi, unit) {
unit = unit || 'mg';
return ' <span style="color:var(--g500);font-size:11px;">(' + lo + (hi != null ? '-' + hi : '') + ' ' + unit + '/kg)</span>';
};
html += '<tr><td style="font-weight:600;">Ketamine</td><td>' + d(wt, 1.5, 100) + '-' + d(wt, 2, 150) + ' mg' + perKg(1.5, 2) + '</td><td>IV</td><td>1 min</td><td>15-30 min</td><td style="font-size:12px;color:var(--g500);">Dissociative. Preserves airway reflexes.</td></tr>';
html += '<tr><td></td><td>' + d(wt, 4, 300) + '-' + d(wt, 5, 400) + ' mg' + perKg(4, 5) + '</td><td>IM</td><td>3-5 min</td><td>30-60 min</td><td style="font-size:12px;color:var(--g500);">Give atropine 0.01 mg/kg to reduce secretions.</td></tr>';
html += '<tr><td style="font-weight:600;">Midazolam</td><td>' + d(wt, 0.05, 2) + '-' + d(wt, 0.1, 5) + ' mg' + perKg(0.05, 0.1) + '</td><td>IV</td><td>2-3 min</td><td>30-60 min</td><td style="font-size:12px;color:var(--g500);">Anxiolysis. Titrate q3-5 min.</td></tr>';
html += '<tr><td></td><td>' + d(wt, 0.5, 20) + ' mg' + perKg(0.5, null) + '</td><td>IN/PO</td><td>10-15 min</td><td>30-60 min</td><td style="font-size:12px;color:var(--g500);">IN (max 10 mg / naris) or PO (max 20 mg).</td></tr>';
html += '<tr><td style="font-weight:600;">Propofol</td><td>' + d(wt, 1, 40) + ' mg' + perKg(1, null) + '</td><td>IV</td><td>30 sec</td><td>5-10 min</td><td style="font-size:12px;color:var(--g500);">Short procedures. Causes apnea — manage airway.</td></tr>';
html += '<tr><td style="font-weight:600;">Fentanyl</td><td>' + d(wt, 1, 100) + ' mcg' + perKg(1, null, 'mcg') + '</td><td>IV</td><td>2-3 min</td><td>30-60 min</td><td style="font-size:12px;color:var(--g500);">Analgesic. Often combined with midazolam.</td></tr>';
html += '<tr><td></td><td>' + d(wt, 2, 100) + ' mcg' + perKg(2, null, 'mcg') + '</td><td>IN</td><td>5-10 min</td><td>30-60 min</td><td style="font-size:12px;color:var(--g500);">Intranasal.</td></tr>';
html += '<tr><td style="font-weight:600;">Nitrous oxide</td><td>50:50 or 70:30 mix</td><td>Inhaled</td><td>2-5 min</td><td>5 min off</td><td style="font-size:12px;color:var(--g500);">Self-administered via demand valve. Anxiolysis + mild analgesia.</td></tr>';
html += '<tr><td style="font-weight:600;">Dexmedetomidine</td><td>' + d(wt, 2, 100) + ' mcg' + perKg(2, 3, 'mcg') + '</td><td>IN</td><td>15-30 min</td><td>60-90 min</td><td style="font-size:12px;color:var(--g500);">Intranasal. No respiratory depression. Good for imaging.</td></tr>';
html += agents.map(function(dg) { return sedRow(wt, dg, false); }).join('');
html += '</tbody></table></div>';
// Reversal agents
html += '<h4 style="font-size:14px;font-weight:700;color:var(--red);margin:0 0 8px;"><i class="fas fa-rotate-left"></i> Reversal Agents</h4>';
html += '<div style="overflow-x:auto;-webkit-overflow-scrolling:touch;"><table style="width:100%;min-width:560px;border-collapse:collapse;font-size:13px;"><thead><tr style="background:#fee2e2;"><th style="text-align:left;padding:6px 8px;">Drug</th><th style="padding:6px 8px;">Dose</th><th style="padding:6px 8px;">Route</th><th style="padding:6px 8px;">Reverses</th><th style="padding:6px 8px;">Notes</th></tr></thead><tbody>';
html += '<tr><td style="font-weight:600;">Naloxone</td><td>' + d(wt, 0.1, 2) + ' mg' + perKg(0.1, null) + '</td><td>IV/IM/IN</td><td>Opioids (fentanyl, morphine)</td><td style="font-size:12px;color:var(--g500);">Max 2 mg. Repeat q2-3 min. Duration shorter than opioids — monitor for re-sedation.</td></tr>';
html += '<tr><td style="font-weight:600;">Flumazenil</td><td>' + d(wt, 0.01, 0.2) + ' mg' + perKg(0.01, null) + '</td><td>IV</td><td>Benzodiazepines (midazolam)</td><td style="font-size:12px;color:var(--g500);">Max 0.2 mg single dose. Repeat q1 min to max 1 mg total. Risk of seizures — use cautiously.</td></tr>';
html += reversals.map(function(dg) { return sedRow(wt, dg, true); }).join('');
html += '</tbody></table></div>';
html += '<div style="margin-top:12px;padding:10px 12px;background:var(--amber-light);border-radius:6px;font-size:12px;color:#92400e;"><strong>Pre-sedation checklist:</strong> NPO status (2h clear liquids, 6h solids), consent, monitoring equipment (pulse ox, capnography, BP), resuscitation equipment at bedside, suction ready, IV access. Minimum monitoring: continuous SpO2, HR, capnography. Provider capable of managing airway must be present.</div>';
@ -3306,30 +3421,78 @@ window._EM = {
if (e.target.id === 'btn-agit-calc' || e.target.closest('#btn-agit-calc')) calcAgit();
});
// Fallback inline data — mirrors drugs.json agitation section, split into
// step-2 (PO/IN) and step-3 (IV/IM) subgroups by the route field.
// Display-name column intentionally drops the route suffix from the JSON
// name so the table still shows "Lorazepam" / "Midazolam" / "Ketamine".
var AGIT_FALLBACK = [
// Step 2 — PO / IN
{ name: 'Lorazepam PO', display: 'Lorazepam', step: 2, dose_mg_per_kg: 0.05, max_mg: 2, unit: 'mg', route: 'PO', notes: '0.05 mg/kg. May repeat in 30 min.' },
{ name: 'Midazolam PO', display: 'Midazolam', step: 2, dose_mg_per_kg: 0.5, max_mg: 10, unit: 'mg', route: 'PO', notes: '0.5 mg/kg (max 10 mg)' },
{ name: 'Midazolam IN', display: 'Midazolam', step: 2, dose_mg_per_kg: 0.3, max_mg: 10, unit: 'mg', route: 'IN (5 mg/mL)', notes: '0.3 mg/kg split between nares (max 10 mg)' },
{ name: 'Olanzapine (ODT)', display: 'Olanzapine (ODT)', step: 2, dose_mg_per_kg: null, max_mg: null, unit: 'mg', route: 'PO / ODT', notes: 'Age ≥6 yr. Avoid IM + benzo combo (risk of resp depression).', dose_display_if_under_30kg: '2.5-5 mg', dose_display_if_30kg_or_more: '5-10 mg' },
{ name: 'Diphenhydramine PO', display: 'Diphenhydramine', step: 2, dose_mg_per_kg: 1, max_mg: 50, unit: 'mg', route: 'PO', notes: '1 mg/kg. Adjunct only; sedating.' },
// Step 3 — IV / IM
{ name: 'Midazolam IV/IM', display: 'Midazolam', step: 3, dose_mg_per_kg: 0.1, max_mg: 5, unit: 'mg', route: 'IV / IM', notes: '0.05-0.1 mg/kg IV, 0.1-0.15 mg/kg IM (max 10 mg)' },
{ name: 'Lorazepam IV/IM', display: 'Lorazepam', step: 3, dose_mg_per_kg: 0.1, max_mg: 4, unit: 'mg', route: 'IV / IM', notes: '0.05-0.1 mg/kg (max 4 mg). May cause resp depression.' },
{ name: 'Haloperidol', display: 'Haloperidol', step: 3, dose_mg_per_kg: 0.05, max_mg: null, unit: 'mg', route: 'IM / IV', notes: 'Avoid &lt;3 yr. Risk: QT, EPS, NMS. ECG if repeated.', dose_display_if_under_40kg: '0.025-0.075 mg/kg', dose_display_if_40kg_or_more: '2.5-5 mg' },
{ name: 'Olanzapine IM', display: 'Olanzapine', step: 3, dose_mg_per_kg: null, max_mg: null, unit: 'mg', route: 'IM', notes: 'Avoid benzo co-administration (resp depression, hypotension)', dose_display_if_under_40kg: '2.5-5 mg', dose_display_if_40kg_or_more: '5-10 mg' },
{ name: 'Ketamine IM', display: 'Ketamine', step: 3, dose_mg_per_kg: 4, max_mg: 500, unit: 'mg', route: 'IM', notes: '4-5 mg/kg IM (rescue for severe excited delirium). Monitor airway.' },
{ name: 'Droperidol', display: 'Droperidol', step: 3, dose_mg_per_kg: 0.05, max_mg: null, unit: 'mg', route: 'IM / IV', notes: 'Effective but QT concern — get ECG.', dose_display_prefix: '0.03-0.07 mg/kg' }
];
function agitDrugs() {
var s = window._DRUGS && window._DRUGS.sections && window._DRUGS.sections.agitation;
if (s && s.drugs && s.drugs.length) {
// Merge per-drug hints from fallback (display name, step) when the
// JSON doesn't carry them — keeps rendering identical.
return s.drugs.map(function(d) {
var fb = AGIT_FALLBACK.filter(function(x) { return x.name === d.name; })[0] || {};
return Object.assign({}, fb, d);
});
}
return AGIT_FALLBACK;
}
// Render one drug row, handling the agitation-specific display quirks:
// weight-conditional dose strings (Olanzapine, Haloperidol) and hand-
// computed "prefix = value mg" strings (Droperidol, Haloperidol &lt;40 kg).
function agitRowFor(wt, d) {
var S = window._EM;
var dose;
if (d.dose_display_if_under_30kg && d.dose_display_if_30kg_or_more) {
dose = wt < 30 ? d.dose_display_if_under_30kg : d.dose_display_if_30kg_or_more;
} else if (d.dose_display_if_under_40kg && d.dose_display_if_40kg_or_more) {
dose = wt < 40 ? d.dose_display_if_under_40kg + ' = ' + S.d(wt, d.dose_mg_per_kg) + ' mg' : d.dose_display_if_40kg_or_more;
} else if (d.dose_display_prefix) {
dose = d.dose_display_prefix + ' = ' + S.d(wt, d.dose_mg_per_kg) + ' mg';
} else if (d.dose_mg_per_kg != null) {
dose = S.dStr(wt, d.dose_mg_per_kg, d.max_mg, ' ' + d.unit);
} else {
dose = d.dose_display || '';
}
return S.drugRow(d.display || d.name, dose, d.route, d.notes);
}
// Haloperidol has a weight-specific quirk: under 40 kg the display line
// needs the computed mg, but over 40 kg it's just "2.5-5 mg". Handled
// uniformly by dose_display_if_under_40kg above.
function calcAgit() {
var wt = parseFloat(document.getElementById('agit-weight').value);
var el = document.getElementById('agit-result');
if (!wt) { el.innerHTML = '<p style="color:var(--red);font-size:13px;">Enter weight (kg).</p>'; return; }
var S = window._EM;
var drugs = agitDrugs();
var step2 = drugs.filter(function(d) { return d.step === 2; });
var step3 = drugs.filter(function(d) { return d.step === 3; });
var html = '<div style="padding:10px 14px;border-radius:8px;background:var(--purple-light);border:1.5px solid var(--purple);margin-bottom:10px;"><strong style="color:var(--purple);">Agitation management — ' + wt + ' kg</strong></div>';
html += S.stepBox('#10b981', 'Step 1 — Non-pharmacologic', 'Dim lights, quiet room, familiar caregiver present, reduce stimulation. Rule out hypoglycemia, hypoxia, pain, ↑ICP, toxic ingestion, infection.');
html += S.stepBox('#3b82f6', 'Step 2 — Oral / intranasal (cooperative)', 'First-line for moderate agitation if the patient will accept PO/IN.');
html += S.drugTable(
S.drugRow('Lorazepam', S.dStr(wt, 0.05, 2, ' mg'), 'PO', '0.05 mg/kg. May repeat in 30 min.') +
S.drugRow('Midazolam', S.dStr(wt, 0.5, 10, ' mg'), 'PO', '0.5 mg/kg (max 10 mg)') +
S.drugRow('Midazolam', S.dStr(wt, 0.3, 10, ' mg'), 'IN (5 mg/mL)', '0.3 mg/kg split between nares (max 10 mg)') +
S.drugRow('Olanzapine (ODT)', wt < 30 ? '2.5-5 mg' : '5-10 mg', 'PO / ODT', 'Age ≥6 yr. Avoid IM + benzo combo (risk of resp depression).') +
S.drugRow('Diphenhydramine', S.dStr(wt, 1, 50, ' mg'), 'PO', '1 mg/kg. Adjunct only; sedating.')
);
html += S.drugTable(step2.map(function(d) { return agitRowFor(wt, d); }).join(''));
html += S.stepBox('#f59e0b', 'Step 3 — IM / IV (severe, uncooperative, or safety risk)', 'Require continuous monitoring. Consider restraints only as last resort.');
html += S.drugTable(
S.drugRow('Midazolam', S.dStr(wt, 0.1, 5, ' mg'), 'IV / IM', '0.05-0.1 mg/kg IV, 0.1-0.15 mg/kg IM (max 10 mg)') +
S.drugRow('Lorazepam', S.dStr(wt, 0.1, 4, ' mg'), 'IV / IM', '0.05-0.1 mg/kg (max 4 mg). May cause resp depression.') +
S.drugRow('Haloperidol', wt < 40 ? '0.025-0.075 mg/kg = ' + S.d(wt, 0.05) + ' mg' : '2.5-5 mg', 'IM / IV', 'Avoid &lt;3 yr. Risk: QT, EPS, NMS. ECG if repeated.') +
S.drugRow('Olanzapine', wt < 40 ? '2.5-5 mg' : '5-10 mg', 'IM', 'Avoid benzo co-administration (resp depression, hypotension)') +
S.drugRow('Ketamine', S.dStr(wt, 4, 500, ' mg'), 'IM', '4-5 mg/kg IM (rescue for severe excited delirium). Monitor airway.') +
S.drugRow('Droperidol', '0.03-0.07 mg/kg = ' + S.d(wt, 0.05) + ' mg', 'IM / IV', 'Effective but QT concern — get ECG.')
);
html += S.drugTable(step3.map(function(d) { return agitRowFor(wt, d); }).join(''));
html += '<div style="padding:10px;background:#fef3c7;border-radius:6px;font-size:12px;color:#92400e;margin-top:10px;"><strong>Monitoring:</strong> Continuous SpO2 + HR after parenteral sedation. Have airway equipment, flumazenil, naloxone, and IV access ready.</div>';
html += S.ref('AAP Clinical Report on Pediatric Agitation. ACEP Guidelines for Acute Agitation.');
el.innerHTML = html;
@ -3344,23 +3507,50 @@ window._EM = {
if (e.target.id === 'btn-emet-calc' || e.target.closest('#btn-emet-calc')) calcEmet();
});
// Fallback inline data — used if /data/drugs.json hasn't loaded yet.
// Must match drugs.json shape for the antiemetics section.
var EMET_FALLBACK = [
{ name: 'Ondansetron', dose_mg_per_kg: 0.15, max_mg: 8, unit: 'mg', route: 'PO / ODT / IV', notes: 'First-line. Max single 8 mg. Repeat q8h. QT prolongation — avoid with other QT drugs. &lt;6 mo: limited data.', weight_band_dose: { under_15kg: '2 mg', '15_to_30kg': '4 mg', '30kg_or_more': '8 mg' } },
{ name: 'Metoclopramide', dose_mg_per_kg: 0.15, max_mg: 10, unit: 'mg', route: 'IV / IM / PO', notes: '0.1-0.15 mg/kg (max 10 mg). Give with diphenhydramine to prevent EPS / dystonia.' },
{ name: 'Dimenhydrinate', dose_mg_per_kg: 1.25, max_mg: 50, unit: 'mg', route: 'PO / IV / IM / PR', notes: '1.25 mg/kg (max 50 mg) q6h. ≥2 yr.' },
{ name: 'Diphenhydramine', dose_mg_per_kg: 1, max_mg: 50, unit: 'mg', route: 'PO / IV / IM', notes: '1 mg/kg (max 50 mg) q6h. Adjunct, sedating.' },
{ name: 'Promethazine', dose_mg_per_kg: 0.25, max_mg: 25, unit: 'mg', route: 'PO / IV / IM', notes: '0.25-1 mg/kg. <strong>CONTRAINDICATED &lt;2 yr</strong> (resp depression). Tissue injury if IV extrav.' },
{ name: 'Dexamethasone', dose_mg_per_kg: 0.15, max_mg: 10, unit: 'mg', route: 'IV / PO', notes: 'Adjunct, esp. chemo-induced. 0.15 mg/kg (max 10 mg).' },
{ name: 'Scopolamine patch', dose_mg_per_kg: null, max_mg: null, unit: 'mg', dose_display: '1.5 mg patch', route: 'Transdermal', notes: '≥12 yr. Motion sickness. Apply 4h before exposure.' }
];
// Prefer JSON-backed data if loaded; fall back to the inline list above.
function emetDrugs() {
var s = window._DRUGS && window._DRUGS.sections && window._DRUGS.sections.antiemetics;
return (s && s.drugs && s.drugs.length) ? s.drugs : EMET_FALLBACK;
}
// Iterate the drug list to produce S.drugRow(...) output, handling the
// special cases (Ondansetron weight-band prefix, Scopolamine fixed dose).
function emetRows(wt, drugs) {
var S = window._EM;
return drugs.map(function(d) {
var dose;
if (d.name === 'Ondansetron' && d.weight_band_dose) {
var bd = d.weight_band_dose;
var band = wt < 15 ? bd.under_15kg : wt < 30 ? bd['15_to_30kg'] : bd['30kg_or_more'];
dose = band + ' (weight band) OR ' + S.dStr(wt, d.dose_mg_per_kg, d.max_mg, ' ' + d.unit) + ' (' + d.dose_mg_per_kg + ' ' + d.unit + '/kg)';
} else if (d.dose_display) {
dose = d.dose_display;
} else {
dose = S.dStr(wt, d.dose_mg_per_kg, d.max_mg, ' ' + d.unit);
}
return S.drugRow(d.name, dose, d.route, d.notes);
}).join('');
}
function calcEmet() {
var wt = parseFloat(document.getElementById('emet-weight').value);
var el = document.getElementById('emet-result');
if (!wt) { el.innerHTML = '<p style="color:var(--red);font-size:13px;">Enter weight (kg).</p>'; return; }
var S = window._EM;
// Ondansetron weight bands
var ondWt = wt < 15 ? '2 mg' : wt < 30 ? '4 mg' : '8 mg';
var html = '<div style="padding:10px 14px;border-radius:8px;background:var(--blue-light);border:1.5px solid var(--blue);margin-bottom:10px;"><strong style="color:var(--blue-dark);">Antiemetic doses — ' + wt + ' kg</strong></div>';
html += S.drugTable(
S.drugRow('Ondansetron', ondWt + ' (weight band) OR ' + S.dStr(wt, 0.15, 8, ' mg') + ' (0.15 mg/kg)', 'PO / ODT / IV', 'First-line. Max single 8 mg. Repeat q8h. QT prolongation — avoid with other QT drugs. &lt;6 mo: limited data.') +
S.drugRow('Metoclopramide', S.dStr(wt, 0.15, 10, ' mg'), 'IV / IM / PO', '0.1-0.15 mg/kg (max 10 mg). Give with diphenhydramine to prevent EPS / dystonia.') +
S.drugRow('Dimenhydrinate', S.dStr(wt, 1.25, 50, ' mg'), 'PO / IV / IM / PR', '1.25 mg/kg (max 50 mg) q6h. ≥2 yr.') +
S.drugRow('Diphenhydramine', S.dStr(wt, 1, 50, ' mg'), 'PO / IV / IM', '1 mg/kg (max 50 mg) q6h. Adjunct, sedating.') +
S.drugRow('Promethazine', S.dStr(wt, 0.25, 25, ' mg'), 'PO / IV / IM', '0.25-1 mg/kg. <strong>CONTRAINDICATED &lt;2 yr</strong> (resp depression). Tissue injury if IV extrav.') +
S.drugRow('Dexamethasone', S.dStr(wt, 0.15, 10, ' mg'), 'IV / PO', 'Adjunct, esp. chemo-induced. 0.15 mg/kg (max 10 mg).') +
S.drugRow('Scopolamine patch', '1.5 mg patch', 'Transdermal', '≥12 yr. Motion sickness. Apply 4h before exposure.')
);
html += S.drugTable(emetRows(wt, emetDrugs()));
html += '<div style="padding:10px;background:#fef3c7;border-radius:6px;font-size:12px;color:#92400e;margin-top:10px;"><strong>Pearls:</strong> Ondansetron is first-line in ED for acute gastroenteritis vomiting — single PO/ODT dose increases oral rehydration success. Avoid anticholinergics + opioid combinations. Check QTc if stacking ondansetron + other QT drugs.</div>';
html += S.ref('AAP gastroenteritis guidance; WHO essential medicines; Lexicomp.');
el.innerHTML = html;

31
public/js/drugs-loader.js Normal file
View file

@ -0,0 +1,31 @@
// ============================================================
// drugs-loader.js
// Fetches /data/drugs.json at page load and exposes:
// window._DRUGS — the parsed JSON (after fetch resolves)
// window._DRUGS_READY — a Promise that resolves to the JSON
//
// Calculator functions in calculators.js look up drugs via a small
// helper (pickDrug) and fall back to hardcoded inline data if the
// JSON isn't loaded yet. This means the app keeps working even if
// the fetch fails — the JSON is an authoritative data source but
// not a hard dependency at runtime.
// ============================================================
(function() {
// Default to empty shape so lookups don't throw before fetch resolves.
window._DRUGS = window._DRUGS || { version: null, sections: {} };
window._DRUGS_READY = fetch('/data/drugs.json', { credentials: 'same-origin' })
.then(function(r) {
if (!r.ok) throw new Error('drugs.json HTTP ' + r.status);
return r.json();
})
.then(function(j) {
window._DRUGS = j;
return j;
})
.catch(function(err) {
// Non-fatal — calc functions have inline fallbacks.
console.warn('[drugs-loader] could not load /data/drugs.json:', err);
return window._DRUGS;
});
})();

View file

@ -165,3 +165,20 @@ test('Lund-Browder: 50% of both thighs young child = 8%', () => {
test('Lund-Browder: invalid bracket returns null', () => {
assert.equal(M.lundBrowderTBSA('bogus', { head: 50 }), null);
});
// ── drugs.json schema ───────────────────────────────────────────
// Asserts the JSON is loadable and that each of the 5 extracted
// bedside sections exists with a non-empty drugs array.
test('drugs.json: schema loads with all 5 extracted sections + non-empty drugs', () => {
const fs = require('node:fs');
const path = require('node:path');
const raw = fs.readFileSync(path.join(__dirname, '..', 'public', 'data', 'drugs.json'), 'utf8');
const j = JSON.parse(raw);
assert.equal(j.version, '1.0');
assert.ok(j.sections, 'top-level sections object present');
['anaphylaxis', 'sedation', 'agitation', 'antiemetics', 'seizure'].forEach((key) => {
const sec = j.sections[key];
assert.ok(sec, `section ${key} present`);
assert.ok(Array.isArray(sec.drugs) && sec.drugs.length > 0, `${key}.drugs non-empty`);
});
});