Independent education · Sources and limitations documented
Portfolio / Smart NOA simulation

Software simulation

Source snapshot · 5 November 2026

simulator/ui/advisory.js

// "Advisory: haemodynamic rescue" card (engine/rescue.js). EDUCATIONAL SIMULATION — not a medical device.
// The card only shows the advisory; "Give suggested bolus" is the clinician's explicit approval
// (advisor.accept) and "Dismiss" is logged (advisor.dismiss). Nothing here runs on its own.
import { h, clear, setText, fmtClock } from './dom.js';
import { srcTags } from './sources.js';
import { ADVISORY_DISCLAIMER } from '../engine/rescue.js';
import { DEPTH_LOW_DISCLAIMER } from '../engine/depth-advisory.js';

/**
 * Builds one card. `prefix` makes the ids unique (two copies: Run screen and guided panel).
 * `live`: whether this copy announces changes (aria-live polite); only one copy should.
 * Returns { el, update(view) }.
 */
export function advisoryCard({ prefix, live, onAccept, onDismiss }) {
  const title = h('h3', { id: `${prefix}-h` }, 'Advisory: haemodynamic rescue');
  const disclaimer = h('p', { class: 'advisory__disclaimer', id: `${prefix}-disclaimer` }, ADVISORY_DISCLAIMER);
  const headline = h('p', { class: 'advisory__headline', id: `${prefix}-headline`, role: live ? 'status' : null, 'aria-live': live ? 'polite' : 'off' });
  const countdown = h('p', { class: 'advisory__countdown', id: `${prefix}-countdown`, 'aria-live': 'off' });
  const suggestion = h('div', { class: 'advisory__suggestion', id: `${prefix}-suggestion` });
  // Cross-reference when the depth-low card is shown at the same time (set by update(v, crossNote)).
  const cross = h('p', { class: 'advisory__cross', id: `${prefix}-cross`, hidden: true });
  const rationale = h('ul', { class: 'list list--tight advisory__rationale', id: `${prefix}-rationale` });
  const checklist = h('ul', { class: 'list list--tight advisory__checklist', id: `${prefix}-checklist` });
  const accept = h('button', { type: 'button', class: 'btn btn--hmi btn--primary', id: `${prefix}-accept` }, 'Give suggested bolus');
  const dismiss = h('button', { type: 'button', class: 'btn btn--hmi', id: `${prefix}-dismiss` }, 'Dismiss');
  accept.addEventListener('click', () => { if (accept.dataset.entry) onAccept(accept.dataset.entry); });
  dismiss.addEventListener('click', () => { if (dismiss.dataset.entry) onDismiss(dismiss.dataset.entry); });
  const el = h('section', { class: 'hmi-panel advisory', id: prefix, role: 'region', 'aria-labelledby': `${prefix}-h`, 'data-auto-safe': 'advisory', hidden: true },
    title, disclaimer, headline, countdown, suggestion, cross,
    h('div', { class: 'btn-row' }, accept, dismiss),
    h('details', { class: 'hmi-details', open: true }, h('summary', {}, 'Why, and what else to check (no doses)'), rationale, checklist));

  function update(v, crossNote = '') {
    el.hidden = !v || !v.visible;
    if (el.hidden) return;
    el.dataset.kind = v.kind;
    setText(cross, crossNote || ''); cross.hidden = !crossNote;
    const head = v.dismissed ? `${v.headline || 'Advisory'} Dismissed at ${fmtClock(v.dismissedAtSec)} — it returns if MAP recovers and falls again.`
      : v.kind === 'hypotension_recovering' ? 'MAP recovering (at or above the trigger) — no dose suggested now; reassess.'
        : v.suppressed && v.suppressed.reason !== 'interval' ? `${v.headline} ${v.suppressed.message}`
          : v.suppressed ? `${v.headline} Reassess before another dose.` : v.headline;
    setText(headline, head);
    setText(countdown, v.suppressed && v.suppressed.reason === 'interval' && !v.dismissed ? v.suppressed.message : '');
    countdown.hidden = !countdown.textContent;
    const s = v.suggestion && !v.dismissed ? v.suggestion : null;
    const sig = s ? `${v.entry && v.entry.id}|${s.drugId}|${s.amount}` : '';
    if (suggestion.dataset.sig !== sig) {
      suggestion.dataset.sig = sig;
      clear(suggestion).append(...(s ? [h('p', {}, h('strong', {}, `Suggested: ${s.name} ${s.amount} ${s.unit} IV bolus `), srcTags(s.sources),
        ` (label rescue range ${s.range.min}–${s.range.max} ${s.unit}${s.capped ? `; ${s.capped}` : ''}).`)] : []));
    }
    accept.hidden = !v.canAccept; dismiss.hidden = !v.canDismiss;
    accept.disabled = !v.canAccept; dismiss.disabled = !v.canDismiss;
    if (v.entry) { accept.dataset.entry = v.entry.id; dismiss.dataset.entry = v.entry.id; } else { delete accept.dataset.entry; delete dismiss.dataset.entry; }
    const rsig = (v.rationale || []).join('|') + (v.checklist || []).join('|');
    if (rationale.dataset.sig !== rsig) {
      rationale.dataset.sig = rsig;
      clear(rationale).append(...(v.rationale || []).map((x) => h('li', {}, x)));
      clear(checklist).append(...(v.checklist || []).map((x) => h('li', {}, '☐ ', x)));
    }
  }
  return { el, update };
}

/**
 * "Advisory: depth index below the GA band" card (engine/depth-advisory.js). TEXT ONLY: no drug, dose or
 * rate; not an alarm. The only control is "Dismiss" (logged, advisor.dismiss). Nothing here runs on its own.
 * Returns { el, update(view, crossNote) }.
 */
export function depthAdvisoryCard({ prefix, live, onDismiss }) {
  const title = h('h3', { id: `${prefix}-h` }, 'Advisory: depth index below the GA band');
  const disclaimer = h('p', { class: 'advisory__disclaimer', id: `${prefix}-disclaimer` }, `${DEPTH_LOW_DISCLAIMER} Text only — no drug, dose or rate is suggested.`);
  const headline = h('p', { class: 'advisory__headline', id: `${prefix}-headline`, role: live ? 'status' : null, 'aria-live': live ? 'polite' : 'off' });
  const current = h('p', { class: 'advisory__countdown', id: `${prefix}-current`, 'aria-live': 'off' });
  const cross = h('p', { class: 'advisory__cross', id: `${prefix}-cross`, hidden: true });
  const checklist = h('ul', { class: 'list list--tight advisory__checklist', id: `${prefix}-checklist` });
  const foot = h('p', { class: 'hint hint--hmi advisory__foot', id: `${prefix}-foot` });
  const dismiss = h('button', { type: 'button', class: 'btn btn--hmi', id: `${prefix}-dismiss` }, 'Dismiss');
  dismiss.addEventListener('click', () => { if (dismiss.dataset.entry) onDismiss(dismiss.dataset.entry); });
  const el = h('section', { class: 'hmi-panel advisory advisory--depth', id: prefix, role: 'region', 'aria-labelledby': `${prefix}-h`, 'data-auto-safe': 'advisory', 'data-kind': 'depth_low', hidden: true },
    title, disclaimer, headline, current, cross,
    h('details', { class: 'hmi-details', open: true }, h('summary', {}, 'What to check (no doses)'), checklist),
    foot, h('div', { class: 'btn-row' }, dismiss));
  let fsig = '';
  function update(v, crossNote = '') {
    el.hidden = !v || !v.visible;
    if (el.hidden) return;
    setText(headline, v.headline);
    setText(current, v.current || '');
    setText(cross, crossNote || ''); cross.hidden = !crossNote;
    const sig = `${v.footnote}|${(v.checklist || []).join('|')}`;
    if (sig !== fsig) {
      fsig = sig;
      clear(checklist).append(...(v.checklist || []).map((x) => h('li', {}, '☐ ', x)));
      clear(foot).append(`${v.footnote} `, srcTags(v.sources || []), '.');
    }
    dismiss.hidden = !v.canDismiss; dismiss.disabled = !v.canDismiss;
    if (v.entry) dismiss.dataset.entry = v.entry.id; else delete dismiss.dataset.entry;
  }
  return { el, update };
}
Source and scope

Published from Smart-NOA-Controller/simulator/ui/advisory.js at revision 6e5b205e3843. Full source text is retained; HTML presentation does not establish independent clinical review. In-memory simulation with no patient, monitor or pump interface; not clinically validated.