Four changes batched: 1. ED Encounters tab (new) — multi-stage emergency note with don't-miss tooltips and 2023 E/M MDM finalize. New route /api/ed-encounters (generate per-stage + finalize MDM), new ed-encounters.js owning all client logic, new ed-encounter.html component, new template_ed memory category. Persists draft to localStorage every keystroke and to saved_encounters on stage advance. encounters.js touched only to register the new tab in sessionStorage restore + tabMap (save and idempotency code untouched). 2. Notes model selector — /notes/from-voice now accepts a client-supplied model (validated by the existing callAI allow-list); falls back to the admin default. Added <select class="tab-model-select"> to notes.html so the existing app.js populator handles options + default. 3. Remove AI-learning-from-corrections — deleted correctionTracker.js, POST /memories/correction, the corrections branch in /memories/context, the settings UI section, the FAQ entry, and all dead trackAIOutput/saveCorrection guards in callers. Legacy correction_* DB rows are filtered (NOT LIKE) rather than dropped, so no destructive migration. 4. Fix notes AI framing — /notes/from-voice prompt no longer assumes "physician dictation". Plain notes (shopping lists, reminders, ideas) now match the dictation tone instead of being forced into clinical structure. All 46 tests pass.
178 lines
7.3 KiB
JavaScript
Executable file
178 lines
7.3 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
// ============================================================
|
|
// STATIC REFERENCE LINTER
|
|
// ============================================================
|
|
// Scans public/js/ for DOM id references and verifies each target
|
|
// exists in some public/*.html or public/components/*.html. Catches
|
|
// the class of bug where a component is moved/renamed/deleted and a
|
|
// JS file keeps calling getElementById('orphan') — the handler then
|
|
// silently no-ops at runtime. The lightbox that broke for Bedside
|
|
// pathway images was exactly this: the markup lived in
|
|
// calculators.html, but after the reorg the bedside tab never loaded
|
|
// it, so getElementById('img-lightbox') returned null.
|
|
//
|
|
// Also checks:
|
|
// - <button data-img-src="/path"> resolves to a file in public/
|
|
// - <audio|img src="/path"> resolves to a file in public/
|
|
//
|
|
// Prints a list of unresolved references and exits non-zero. An
|
|
// optional allowlist below handles IDs that are legitimately created
|
|
// at runtime (e.g. dynamically injected components).
|
|
// ============================================================
|
|
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const ROOT = path.resolve(__dirname, '..');
|
|
const PUB = path.join(ROOT, 'public');
|
|
|
|
// ── Files to scan for id references ──────────────────────────
|
|
function walk(dir, ext, acc = []) {
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const p = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) walk(p, ext, acc);
|
|
else if (ext.test(entry.name)) acc.push(p);
|
|
}
|
|
return acc;
|
|
}
|
|
|
|
const jsFiles = walk(path.join(PUB, 'js'), /\.(js|mjs)$/);
|
|
const htmlFiles = walk(PUB, /\.html$/);
|
|
|
|
// ── Collect every id defined anywhere ────────────────────────
|
|
// Both static HTML attributes and dynamic JS (innerHTML strings,
|
|
// createElement().id = 'X'). If an id is "defined" by any file in
|
|
// the repo, a reference to it is considered resolved — whether it
|
|
// appears in the DOM at any given moment depends on control flow,
|
|
// which this linter doesn't model. We only catch the dead-reference
|
|
// class of bug: a JS module reaches for an id that literally no
|
|
// source in the tree ever produces.
|
|
const definedIds = new Set();
|
|
function collect(src) {
|
|
for (const m of src.matchAll(/\bid\s*=\s*["']([a-zA-Z_][\w-]*)["']/g)) definedIds.add(m[1]);
|
|
for (const m of src.matchAll(/\.id\s*=\s*["']([a-zA-Z_][\w-]*)["']/g)) definedIds.add(m[1]);
|
|
}
|
|
for (const f of htmlFiles) collect(fs.readFileSync(f, 'utf8'));
|
|
for (const f of jsFiles) collect(fs.readFileSync(f, 'utf8'));
|
|
|
|
// Dynamic-id allowlist: created by JS via innerHTML or createElement.
|
|
// Keep this list short and specific; expand only when sure the id is
|
|
// genuinely runtime-constructed. Use prefixes ending in '-' to match
|
|
// an entire family.
|
|
const DYNAMIC_ID_PREFIXES = [
|
|
'em-', // bedside section containers built by sub-nav.js
|
|
'shadess-', // wellvisit SSHADESS domain injection
|
|
'wv-panel-', // wellvisit subpanel ids (these exist in HTML but also built in JS)
|
|
'milestone-', // milestones checklist
|
|
'quiz-', // learning hub quiz questions
|
|
'lh-quiz-', // learning hub quiz answers
|
|
'pe-', // PE guide steps are rendered dynamically
|
|
'ros-', // ROS dynamic rows
|
|
'nrp-', // NRP pathway dynamic widgets
|
|
'seizure-', // seizure pathway dynamic widgets
|
|
'sepsis-', // sepsis pathway dynamic widgets
|
|
'airway-', // airway pathway dynamic widgets
|
|
'cardiac-', // cardiac-arrest pathway dynamic widgets
|
|
'anaph-', // anaphylaxis
|
|
'burn-', // burns
|
|
'vent-', // ventilation
|
|
'tox-', // toxicology
|
|
'trauma-', // trauma
|
|
'neo-', // neonatal
|
|
'resp-', // respiratory
|
|
'sed-', // sedation
|
|
'agit-', // agitation
|
|
'antiemetic-', // antiemetics
|
|
'antibio-', // antimicrobials
|
|
'bili-', // bilirubin dynamic columns
|
|
'bmi-', // BMI chart elements
|
|
'bp-', // BP percentile chart elements
|
|
'growth-', // growth chart elements
|
|
'dose-', // dosing widgets
|
|
'bsa-', // BSA calc
|
|
'equip-', // equipment sizing
|
|
'resus-', // resus meds
|
|
'gcs-', // GCS components
|
|
'vitals-', // vitals reference
|
|
'catchup-', // catch-up schedule
|
|
'totp-', // 2FA elements
|
|
'sessions-', // active sessions list
|
|
'nc-', // nextcloud settings
|
|
'mem-', // memories
|
|
'doc-', // documents
|
|
'audio-', // audio backups
|
|
'saved-', // saved encounters
|
|
'ext-', // extensions CRUD
|
|
'cms-', // content manager
|
|
'fpa-', // forgot-password accept screen
|
|
'rsp-', // reset password
|
|
'browser-whisper-', // whisper settings
|
|
'admin-', // admin panel
|
|
'adminms-', // admin milestones
|
|
'2fa-', // 2FA UI
|
|
'calc-', // calculator panels (some may be missing from HTML)
|
|
];
|
|
|
|
function isDynamicId(id) {
|
|
return DYNAMIC_ID_PREFIXES.some(p => id.startsWith(p));
|
|
}
|
|
|
|
// ── Scan JS for getElementById / querySelector('#id') / '#id' ─
|
|
const unresolved = [];
|
|
const idRef = /getElementById\s*\(\s*['"]([^'"]+)['"]\s*\)|querySelector(?:All)?\s*\(\s*['"]#([a-zA-Z_][\w-]*)\s*['"]|querySelector(?:All)?\s*\(\s*['"][^'"]*?#([a-zA-Z_][\w-]+)/g;
|
|
for (const f of jsFiles) {
|
|
const src = fs.readFileSync(f, 'utf8');
|
|
const seen = new Set();
|
|
for (const m of src.matchAll(idRef)) {
|
|
const id = m[1] || m[2] || m[3];
|
|
if (!id || seen.has(id)) continue;
|
|
seen.add(id);
|
|
if (definedIds.has(id) || isDynamicId(id)) continue;
|
|
unresolved.push({ file: path.relative(ROOT, f), id });
|
|
}
|
|
}
|
|
|
|
// ── Scan HTML for asset references that must resolve on disk ──
|
|
const brokenAssets = [];
|
|
function checkAsset(file, p) {
|
|
if (!p || !p.startsWith('/')) return; // only root-absolute
|
|
if (p.startsWith('//') || p.startsWith('/api/') || p.startsWith('/components/')) return;
|
|
if (p.startsWith('/#')) return; // hash-only
|
|
const onDisk = path.join(PUB, p.split('?')[0].split('#')[0]);
|
|
if (!fs.existsSync(onDisk)) brokenAssets.push({ file: path.relative(ROOT, file), path: p });
|
|
}
|
|
|
|
const ASSET_PATTERNS = [
|
|
/data-img-src\s*=\s*["']([^"']+)["']/g,
|
|
/<(?:img|audio|video|source|link)[^>]*\bsrc\s*=\s*["']([^"']+)["']/g,
|
|
/<(?:link)[^>]*\bhref\s*=\s*["']([^"']+)["']/g,
|
|
];
|
|
for (const f of htmlFiles) {
|
|
const html = fs.readFileSync(f, 'utf8');
|
|
for (const pat of ASSET_PATTERNS) {
|
|
for (const m of html.matchAll(pat)) checkAsset(f, m[1]);
|
|
}
|
|
}
|
|
|
|
// ── Report ────────────────────────────────────────────────────
|
|
let failed = false;
|
|
|
|
if (unresolved.length) {
|
|
console.error('\n❌ Unresolved DOM id references:');
|
|
for (const { file, id } of unresolved) console.error(` ${file} -> #${id}`);
|
|
failed = true;
|
|
} else {
|
|
console.log('✓ All JS id references resolve to an HTML id.');
|
|
}
|
|
|
|
if (brokenAssets.length) {
|
|
console.error('\n❌ Broken asset paths:');
|
|
for (const { file, path: p } of brokenAssets) console.error(` ${file} -> ${p}`);
|
|
failed = true;
|
|
} else {
|
|
console.log('✓ All asset paths resolve to a file on disk.');
|
|
}
|
|
|
|
process.exit(failed ? 1 : 0);
|