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.
#2 — Don't-miss tooltip (encounters HPI + sick visit, max 5)
- New POST /api/dont-miss returning {points: [{point, why}]} capped at 5
(cap defended both in the prompt and server-side .slice(0,5))
- New dontMissTooltip prompt in prompts.js
- New suggestDontMiss() helper in app.js mirroring suggestBillingCodes;
inserts an orange-bordered card next to the note output, silent on empty
- Wired into liveEncounter.js (encounter HPI) and sickVisit.js. Not added
to wellvisit/soap/hospital/chart per spec.
#1 — Bedside suture selector
- New ES module public/js/bedside/sutures.js (~300L) following the
burns.js pattern: site × age × tension × cosmetic × contamination ×
hours-since-injury → material, size, technique, removal day range,
glue/Steri-strip alternative, warnings, tetanus reminder.
- 15 anatomic sites covered (face, eyelid, lip vermilion, intraoral,
ear, scalp, neck, trunk, upper/lower ext, hand, foot, joint surface,
genitalia, fingertip).
- Bites: cat/human → don't-close-primarily warning; dog bite to hand →
loose-approximation note. Heavy contamination → delayed primary
closure. >12h non-face/scalp → judgment call note.
- Removal days shown as ranges (3–5, 7–10, 10–14) per source norms,
not single midpoints.
- Subungual hematoma trephination guidance corrected: any painful
hematoma with intact nail and no displaced fracture (especially if
25–50% or more), per current UpToDate guidance.
- Inline citation: Roberts & Hedges 7e (2019), Fleisher & Ludwig 8e,
AAP Section on EM, UpToDate (Pope JV).
- Pill registered in sub-nav SECTIONS + bedside/index.js. Persists
active state via existing UIState helper.
All 46 tests pass.
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.
Symptom Daniel reported: "note recording, not working nor going into
textbox". Root cause was on the server side — /api/notes/from-voice
asked the AI for HTML in its prompt, but real-world models return
markdown ~25% of the time. Tiptap's setContent only renders HTML;
markdown comes through as literal text or a partial render, looking
like the textbox didn't fill.
Server (src/routes/notes.js):
• New toHtmlBody() helper. If the AI returned real HTML (any
block tag), pass through. Otherwise run through `marked` so
markdown becomes <p>/<h*>/<strong>/etc.
• Strips ```json / ```html code fences before JSON parsing.
• Stricter JSON-recovery: only accepts {title|body} parsed shape;
falls back to wrapping the AI's full reply via toHtmlBody().
• Final guard: if body would be empty after sanitisation, wrap
the raw transcript so the user can at least edit it manually.
Client (public/js/notes.js):
• applyGeneratedNote prefers _editor.commands.setContent over a
full remount — avoids the toolbar-reattach flicker + the brief
window where the body looked empty.
• Logs to console when the editor target is missing or Tiptap
setContent throws, so a future regression is greppable.
Plus the two infra fixes Daniel approved earlier in the same
session — keeping them in this commit since they're already
deployed and tested:
• src/db/database.js: cleanup interval handle exposed; server
shutdown now clearInterval()s it before pool.end(). Removes
the SIGTERM → 9-second-hang → Docker SIGKILL race.
• src/routes/audioBackups.js: switch multer.memoryStorage() to
diskStorage with cleanup. 10 concurrent 25 MB uploads no
longer pin 250 MB of RAM. Identical user-visible perf since
upload is wire-bound.
New dep: marked@latest (used server-side only in toHtmlBody).
Soft-delete for notes — Daniel asked for "deleted notes go to trash"
so a slip of the finger doesn't lose work.
Schema: migrations/1777090000000_notes-trash.js adds a deleted_at
timestamptz column to personal_notes (NULL = active) plus an index
on (user_id, deleted_at).
Server (src/routes/notes.js):
GET /api/notes now filters deleted_at IS NULL
GET /api/notes/trash new — list trashed items, newest-
deleted first
DELETE /api/notes/:id now soft-deletes (sets deleted_at)
DELETE /api/notes/:id?hard=1 hard-delete, only allowed on items
already in trash (UI bug can't
erase an active note)
POST /api/notes/:id/restore pull a note out of trash
POST /api/notes/trash/empty hard-delete every trashed note for
the user
Frontend (public/components/notes.html + public/js/notes.js +
public/css/styles.css):
• Sidebar gets two tabs — "Notes" / "Trash (n)" with live count
• Trash tab shows deleted-at timestamps, Restore + delete-forever
per row, Empty-trash button at the bottom
• Active list and trash count refresh in parallel after every
save / delete / restore
• Delete button in the editor now says "Move to trash" and uses
the showConfirm helper (no native dialogs)
Sanitizer swap (public/js/notes.js):
Replaced the homegrown allowlist walker with DOMPurify (already
loaded from cdnjs in index.html, used by learningHub.js too).
Custom HTML sanitizers historically have bypasses; DOMPurify is
the right primitive.
Tests (test/notes-sanitize.test.js — node:test + jsdom + dompurify
as new dev deps):
9 contract tests covering script-tag stripping, inline event
handlers, img onerror, iframe/object, style attributes, every
preserved tag in the allowlist, javascript: URI rejection,
null/undefined input, and nested-script-inside-paragraph. Total
test count: 37 → 46 passing.
SW cache bumped to pedscribe-v12-notes5.