Commit graph

355 commits

Author SHA1 Message Date
Daniel
7ed8a2365b chore: ignore .codex workspace marker (accidentally committed in 66d4f1f) 2026-04-25 01:35:27 +02:00
github-actions[bot]
9106d85f98 Release v6.53.1 2026-04-24 23:35:20 +00:00
Daniel
df1a6613dd fix(notes): voice → AI note now produces HTML the editor can render
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).
2026-04-25 01:35:10 +02:00
github-actions[bot]
fd5108e7b3 Release v6.53.0 2026-04-24 23:02:58 +00:00
Daniel
9961688bfa feat(notes): trash + restore + DOMPurify sanitizer + 9 contract tests
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.
2026-04-25 01:02:49 +02:00
github-actions[bot]
8c9c03c656 Release v6.52.1 2026-04-24 11:22:36 +00:00
Daniel
2a4269d496 fix(notes): 5 bugs found in post-commit review
1. flushAutosave dropped brand-new unsaved notes.
   The guard was `if (_dirty && _activeId != null)` but _activeId is
   null until the first POST succeeds. So tap-New-Note → type → tap-
   Back went through flushAutosave → skipped → closeToList → lost
   the typed content. saveNote() already handles isNew via POST;
   the guard only needs to check _dirty.

2. beforeunload sendBeacon used wrong HTTP method.
   navigator.sendBeacon is POST-only by spec, but my code used it to
   target PUT /api/notes/:id. Beacons silently hit a 404 route and
   the note didn't save. For brand-new notes the URL resolved to
   /api/notes/undefined. Replaced with fetch({ keepalive: true })
   which browsers queue + ship even across unload, supports PUT,
   and handles POST for new notes correctly.

3. Mobile: empty-state card stacked below the sidebar in list view.
   The mobile media-query hid .notes-reader + .notes-editor when
   data-view="list" but didn't include .notes-empty-state, so the
   big "new note" card appeared below the list on phones. Added
   the missing selector.

4. Delete fallback used window.confirm.
   The `else if (window.confirm(...))` branch in deleteActive was
   a rule violation even as a fallback — feedback_no_native_dialogs
   says no native dialogs anywhere in frontend. Dropped it;
   showConfirm is loaded by app.js before any tab activates so the
   fallback path is unreachable in practice. Also upgraded the
   confirm call to use the danger + confirmText options so the
   modal renders red "Delete" button instead of neutral "Confirm".

5. Recorder kept running after switching tabs or notes.
   Dictating, then tapping another clinical-tools tab (or opening
   another note) left the MediaRecorder active and the mic stream
   open — wasted battery, privacy surprise. Added stopRecording
   (silent=true) to the non-notes branch of tabChanged, to
   startCreate, and to openReader so any in-flight recording
   cancels cleanly when the user moves on.

Minor:
  • Dead "'Saving…' : 'Saving…'" ternary in saveNote simplified.

SW cache bumped to pedscribe-v12-notes4.
2026-04-24 13:22:26 +02:00
github-actions[bot]
a25c36c875 Release v6.52.0 2026-04-24 10:49:31 +00:00
Daniel
2d89f295dd feat(notes): strip explanatory UI copy
Removed the "your note saves as you type" / "encrypted at rest" /
tip-list filler that telegraphs AI-generated code. UI now reads
the way a real app reads — names where names go, no paragraphs
explaining what a button does.

  • Module header: "Notes" — removed the paragraph tagline.
  • Empty state: icon + "New note" button only — removed the
    "Hello 👋" heading, the description, and the three-tip list.
  • Placeholders: "Title" / "Search" — removed the "…" ellipses
    and the "Note title" / "Search notes" verbosity.
  • Status + meta: emptied the "New note — will save automatically
    once you type" meta string and the "This note is empty. Tap
    Edit to add content" reader placeholder.
  • Empty-list copy trimmed to just "No matches" when the search
    filter is active.

SW cache bumped to pedscribe-v12-notes3.
2026-04-24 12:49:22 +02:00
github-actions[bot]
fd658739c3 Release v6.51.0 2026-04-24 10:45:40 +00:00
Daniel
9bb4879be1 feat(notes): read-mode default + Edit toggle + autosave + mobile layout
Four changes landing together so the UX flows properly.

1. Fix voice-generation model error.
   /api/notes/from-voice was passing `req.body.model` (undefined)
   through to callAI, which resolved to LITELLM_DEFAULT_MODEL (empty
   for LiteLLM deployments with no explicit default) → `model=""` →
   LiteLLM 400 "Invalid model name". Server now reads the admin-
   configured `models.default` setting, falls back to the
   LITELLM_DEFAULT_MODEL env var, and only passes a model if one
   resolved. Empty string never reaches the provider.

2. Read mode as the default post-save.
   Opening an existing note or saving a new one now lands on a
   clean, read-only rendering of the body (sanitized HTML with an
   allowlist — p/h2/h3/strong/em/u/s/a/ul/ol/li/blockquote/code/pre).
   Click the "Edit" button to switch to the Tiptap editor. Matches
   the mental model of "notes are documents I review, not drafts I'm
   always editing."

3. Autosave during edit.
   Title-input + Tiptap onUpdate trigger a 1.2-second-debounced save.
   In-flight saves coalesce: if the user types more while a save is
   in progress, a follow-up fires right after it lands so the last
   keystroke never gets stranded. beforeunload uses navigator.
   sendBeacon to best-effort flush on tab close. The manual Save
   button is kept as a "save + switch to reader" shortcut. Ctrl/
   Cmd+S still works in the editor.

4. Mobile layout.
   The two-pane layout collapses to a single-pane view below 900px,
   driven by a data-view attribute on .notes-layout. Back buttons
   appear in the reader + editor heads on mobile to return to the
   list. Desktop layout unchanged (sidebar + right pane always
   visible).

Also:
  • CSS specificity fix — .hidden{display:none} was losing to
    .notes-rec-indicator{display:inline-flex} on source order, so
    the "Recording" indicator was showing when idle. Added explicit
    .notes-rec-indicator.hidden + .notes-voice-bar .btn-sm.hidden
    overrides at higher specificity.
  • Reader-body sanitizer — allowlist of safe tags + attributes;
    <a> links get target=_blank + rel=noopener.
  • SW cache bumped to pedscribe-v12-notes2 so clients pick up the
    new module / component / CSS.
2026-04-24 12:45:30 +02:00
github-actions[bot]
415f67d432 Release v6.50.0 2026-04-24 04:18:56 +00:00
Daniel
3c0de624fc feat(notes): personal notes with rich-text editor + voice dictation
New "Notes" tab under Clinical Tools — a per-user scratchpad that's
explicitly NOT fed into AI prompts (distinct from user_memories).
Two-pane layout: searchable list on the left, rich-text editor with
title + save/edit/delete on the right.

Backend:
  migrations/…_add-personal-notes.js  — personal_notes table
    (id, user_id → users ON DELETE CASCADE, title, body, created_at,
    updated_at) with indexes on user_id + (user_id, updated_at).
  src/routes/notes.js — CRUD + one AI endpoint:
    GET    /api/notes           list, newest-updated first
    GET    /api/notes/:id       fetch one
    POST   /api/notes           create (title + body required)
    PUT    /api/notes/:id       update
    DELETE /api/notes/:id       remove
    POST   /api/notes/from-voice  transcript → { title, body }
                                   via callAI (admin-controlled
                                   provider — never selectable by
                                   the clinician).
    Body + title encrypted at rest via the same cryptoUtil used for
    user_memories; 500-note per-user cap; 200-char title / 50 KB
    body limits.

Frontend:
  public/components/notes.html — empty-state card ("Hello 👋"),
    sidebar list with search, editor head with voice-bar (Dictate /
    Pause / Resume / Stop + live timer + pulse indicator), Tiptap
    body, metadata footer. Uses existing .tp-* toolbar classes.
  public/js/notes.js — lazy-init on first tab activation; Tiptap
    editor built from window.Tiptap (same bundle the Content
    Manager uses); delegated list clicks; Ctrl/Cmd+S to save;
    uses app.js's AudioRecorder + transcribeAudio so the STT
    pipeline is shared. On stop → transcribe → /api/notes/from-
    voice → drop the AI-structured title + body into the editor;
    clinician reviews then saves.
  public/css/styles.css — 70 lines of .notes-* styles (card
    layout, warm empty-state with gradient icon + tips, pulse
    animation for the recording indicator, focus states, hover
    nudges).
  public/sw.js — bump cache from pedscribe-v12 → pedscribe-v12-
    notes1 so clients pick up the new module/component.

Admin-controlled STT provider: the recorder posts its audio blob
to the existing /api/transcribe (Google / LiteLLM / ElevenLabs /
Browser Whisper — whatever admin wired up in Settings). Users
cannot pick the provider from this UI.
2026-04-24 06:18:41 +02:00
Daniel
d71714b65d test(lint): static reference linter — catches dead-code + orphan refs
You were right that Playwright has been catching the easy bugs while
the high-signal bugs (lightbox stranded after the Bedside reorg, PVC
clinically wrong, prod not rebuilt) all had to be caught by you as
the user. Adding a static linter so the class of bug that produced
the lightbox regression fails CI next time instead of the app.

scripts/lint-references.js walks public/js and validates every
getElementById('X') and querySelector('#X') resolves to an id that
is defined SOMEWHERE in the repo — either a static HTML attribute,
a .id = 'X' assignment, or an id="X" substring inside a JS template
string. It also walks HTML for asset references (data-img-src,
<img src>, <audio src>, <link href>) and verifies each root-absolute
path maps to a real file on disk.

Running it on the current tree surfaced two real problems:

1. shadess.js:917 reached for #wv-note-transcript when collecting
   refine source context. The actual id is #wv-transcript (no -note-
   infix — the transcript element is shared across well-visit sub-
   panels). The bug meant a generated well-visit note's Refine call
   silently missed the original transcript as source context; the AI
   still "worked" but with less signal and no error. Fixed.

2. public/js/adminMilestones.js (221 lines) referenced 17 ids that
   were removed a year+ ago in commit 3173ce6 ("Remove milestone
   admin UI, add CMS content refresh button"). That commit dropped
   the HTML but forgot the JS, which has been dead-loaded on every
   page view since. All its addEventListener calls are guarded with
   optional chaining, so nothing errored — just silent cruft.
   Deleted the file and dropped the <script defer> tag from
   index.html.

scripts/e2e.sh runs the linter as a preflight before the Playwright
container starts; a broken reference now fails the suite before any
test even boots.

Allowlist is kept small and prefix-based for the handful of id families
that are built dynamically by JS (bedside em-* sections, BP chart
elements, etc.). When a new component is added, its ids get picked up
automatically by the repo-wide collect() pass; the allowlist rarely
needs to grow.

Suite: 294 passed / 0 failed.
2026-04-23 18:58:38 +02:00
Daniel
abc1a64363 test(e2e): +44 tests — sick visit, hospital course, auth screen,
session persistence, per-tab model selector

Brings detailed coverage to sections that were previously only smoke-
tested:

- sickvisit-workflow.spec.js: generate → mocked note, refine bar
  round-trip, load popover, New button clears demographics + transcript.
- hospitalcourse-workflow.spec.js: fill H&P → generate → mocked
  narrative renders via /api/generate-hospital-course, refine updates
  text, load popover open/close.
- auth-screen.spec.js: unauthenticated landing visible, login form
  structure, forgot-password swap + return, register link is
  intentionally display:none on this instance (pinned), register form
  DOM still wired correctly if the link is manually unhidden, reg-
  password has minlength=8 + type=password.
- session-persistence.spec.js: clear cookie simulates logout (UI login
  is gated by Turnstile which can't be completed in the e2e container);
  re-login via loginAs restores the last tab + sub-pill via localStorage.
- model-selector.spec.js: tab-model-select dropdowns render with >0
  options across 7 tabs.

Fixture corrections:
- /api/sick-visit/note and /api/well-visit/note were not being mocked
  at all — the wrong /api/generate-sick-visit pattern was intercepting
  nothing, so tests fell through to the real AI backend. Both now have
  correct patterns and response shapes (sick-visit returns `note`,
  well-visit note returns `note`).
- /api/generate-hospital-course response key updated from `narrative`
  to `hospitalCourse` to match what the frontend actually reads.

wellvisit-workflow: the Visit Note test now asserts a concrete
waitForResponse on /api/well-visit/note + text render, instead of the
previous "hit either endpoint" fallback.

Suite: 294 passed / 0 failed in 5m30s.
2026-04-23 18:58:38 +02:00
Daniel
9c53e19d29 fix(pe-guide): remove PVC entry from cardiac sounds library
PVCs are an ECG / rhythm finding, not a routine auscultation sample —
what you actually hear on the stethoscope is an irregular rhythm with a
compensatory pause, which depends on the underlying rate and is not
teachable from a canned audio clip. The card was also backed by a
synthesized sound, not a real recording. Removing both the card and
the pvc.ogg asset.
2026-04-23 18:58:38 +02:00
Daniel
1b209b5eb7 fix(nav): move image lightbox to index.html so NRP + seizure pathways
open from the Bedside tab

The #img-lightbox overlay markup was sitting at the bottom of
calculators.html. Before the reorg it was fine — the calculators tab
was always the only home for bedside, so by the time a user clicked
the seizure or NRP pathway button the lightbox HTML was guaranteed to
be in the DOM. After promoting Bedside to its own tab, a user can
open Bedside -> Seizures without having visited Calculators first;
the lightbox JS's getElementById('img-lightbox') then returns null
and the click silently no-ops.

Moved the overlay markup to the bottom of index.html so it exists
from page load regardless of which tab has been lazy-loaded. The e2e
harness gets a duplicate copy so the existing lightbox smoke test
keeps working.

Added a regression test for NRP (bedside-smoke.spec.js:141) to catch
any future breakage — the prior seizure-only test didn't exercise the
second pathway button and so the neonatal/NRP path had never been
clicked in CI.

Suite: 252 passed / 0 failed.
2026-04-23 18:58:38 +02:00
Daniel
2e517a67a9 feat(ui-state): persist sub-pill selections across reload + sign-out
Tab-level choice (ped_last_tab) already survived sign-out/in via
localStorage, but sub-pill and sub-tab selections inside a loaded tab
lived only in memory — they reset to defaults after a reload or
browser restart. Now the following are persisted under the ped_ui/
namespace:

  - Calculators nav pill (BP / BMI / GCS / …)
  - Bedside sub-pill (neonatal / airway / …)
  - Well Visit sub-tab (byvisit / milestones / shadess / note)
  - Physical Exam Guide age group + system

Implementation:
- Added public/js/ui-state.js — a ~30-line window.UIState wrapper
  around localStorage with a ped_ui/ prefix and try/catch around both
  read and write (Safari private mode + quota errors silently no-op).
- Each tab's click handler now also calls UIState.set; each tab's
  init path calls UIState.get and replays the saved value through
  the same function a click would call — so there is exactly one
  code path for "show this selection", whether it came from the user
  or from a restore. For Bedside, the restore additionally listens
  for tabChanged so the lazy-loaded HTML is guaranteed to exist by
  the time we re-activate the pill.

Tests:
- e2e/tests/ui-state-persistence.spec.js — 5 specs × 2 viewports =
  10 tests. Each clicks the feature, reloads the page, and asserts
  the same pill / subtab / dropdown value is still active. Catches
  any future regression in the persistence wiring.
- e2e/tests/soap-hospital-workflow.spec.js — fills SOAP transcript,
  generates via mocked AI, clears, opens/closes load popovers; also
  smoke-tests the Hospital Course save-bar.

Suite: 250 passed / 0 failed (+ 20 over the last run).
2026-04-23 18:58:38 +02:00
Daniel
8d97b13bf7 feat(nav): promote Bedside to its own tab; reorganise sidebar sections
Bedside is now a top-level tab instead of a sub-pill inside Calculators —
it's the highest-traffic emergency reference in the app and deserves a
one-click entry. Same DOM structure and JS modules; only the container
moved.

Sidebar reorg:
- Notes: Hospital Course, Chart Review, SOAP Note, Well Visit, Sick Visit
  (Well Visit + Sick Visit relocated from the old "Pediatric" group —
  they're clinical note workflows, not pure reference tools).
- Section rename: "Pediatric" → "Clinical Tools". The section now holds
  Vaccine Schedule, Catch-Up Schedule, Physical Exam Guide, Bedside,
  Calculators, Pagers & Extensions, Learning Hub, Content Manager — mix
  of reference tables + active calculators + utilities, none strictly
  pediatric. "Clinical Tools" reads naturally for the combined set.

Layout changes:
- calculators.html: dropped 480 lines of bedside panel + the Bedside
  nav-pill. The shared age→weight estimator moved with it.
- bedside.html: new component file, contains the full bedside card +
  the age→weight estimator prepended.
- index.html: added bedside-tab section, sidebar restructured.
- e2e-harness.html: renders calculators + bedside side-by-side (not the
  old calc-tab→bedside-pill dance) so the bedside smoke suite still
  works without auth. e2e-bootstrap fetches both with a cache-buster.
- bedside-smoke.spec.js: removed the now-obsolete calc-nav-pill click
  from each test.

Tab persistence is unchanged — ped_last_tab already survives sign-out,
and the lazy-load cache keeps sub-pill state across navigation within a
session. Persistence across browser restart for sub-pills is a separate
follow-up.
2026-04-23 18:58:38 +02:00
Daniel
3884bf673b test(e2e): +120 tests across 8 new specs; baseline fixes
8 new spec files covering sections previously only smoke-tested:
- ai-endpoints-contract.spec.js  — hits 8 real AI endpoints via request
  context and fails if the response leaks TypeError / ReferenceError /
  'Cannot read properties of undefined' / 'is not defined' / 'is not a
  function'. This is the class of bug that shipped the PE-narrative
  regression to prod because every page-level mock prevented the real
  handler from running.
- encounter-workflow.spec.js — generate HPI, refine, clear transcript.
- encounter-save-load.spec.js — save draft, load popover, repopulate.
- wellvisit-workflow.spec.js — byvisit, milestones, SSHADESS (12+
  reveal), visit note.
- vaxschedule-content.spec.js — schedule + catch-up panels populate
  beyond "Loading".
- chart-review-workflow.spec.js — generate + load popover.
- learning-tab.spec.js — search filter, category pills, feed.
- settings-faq-dictation.spec.js — voice/password/2FA/Nextcloud
  sections, FAQ expand/collapse, dictation generate flow.

Baseline fixes:
- Added CORS_ORIGINS + API_RATE_LIMIT_MAX env overrides so the e2e
  container accepts the browser's Origin header and can absorb the
  full suite's API traffic without tripping the 200/min guard.
- Server's /api/ rate limit is now configurable via
  API_RATE_LIMIT_MAX (default stays 200).
- extensions-crud: replaced native page.on('dialog') listeners with
  #confirm-modal-ok clicks (we moved off native confirm()).
- pe-guide-smoke + extensions-crud: mobile viewport opens the hamburger
  before clicking sidebar tabs.
- fixtures.js: /api/refine mock uses 'refined' (real API shape), not
  'content'. /api/chart-review replaced with /api/generate-chart-review.

Suite: 230 passed / 0 failed in 4m36s.
2026-04-23 18:58:38 +02:00
Daniel
015eaf9945 feat(pe-guide): real lung recordings for normal/rhonchi/pleural-rub;
drop grunting + dead synth module

Three lung sounds that were Web-Audio syntheses now play real clinical
recordings sourced from the HLS-CMDS manikin dataset (MIT license):

- normal-vesicular.ogg
- rhonchi.ogg
- pleural-rub.ogg

Dropped expiratory grunting from the library entirely — no
openly-licensed clinical recording located across Wikimedia, Freesound
CC0, SPRSound, Pixabay, Internet Archive, or Littmann/EasyAuscultation
(all proprietary). Card is honest by omission rather than hiding a
synth behind a Play-only UI.

All seven remaining entries now use the same native <audio controls>
player (pause, seek, volume). The synth fallback branch in
renderSoundCard, the stopAllExcept synth reset loop, the script tag,
and the entire public/js/respiratorySounds.js (332 lines of Web Audio)
are removed since nothing references them anymore.
2026-04-23 18:58:38 +02:00
Daniel
02e7281e52 fix(pe-guide): correct route imports (prod 500 regression)
The route destructured PROMPTS/INJECTION_GUARD/wrapUserText from
../utils/prompts, but PROMPTS is the module's default export and the
other two live in ../utils/promptSafe. All three resolved to undefined,
so every /api/generate-pe-narrative request crashed with
"Cannot read properties of undefined (reading 'peGuideNarrative')"
and the client surfaced "Request failed" for PE Guide narrative and
summary generation. Split the require into the two-line form that
every other AI route already uses.
2026-04-23 18:58:38 +02:00
github-actions[bot]
fda5b12143 Release v6.20.0 2026-04-22 20:57:21 +00:00
Daniel
8cfa07dcf5 feat(pe-guide): unified single-play + remove badges; fix(e2e): shared fixture + raised login limit
User-visible changes:
- Removed the REAL / SYNTH badge from each sound card. User feedback:
  "ridiculous". Cards now just show the sound title + description +
  player, no distinction beyond the player type (native <audio controls>
  for recordings, play/stop/progress bar for synth).
- Removed the "real recordings; synth labelled SYNTH" subtitle from
  the sounds library header.
- Single-playback policy across the whole PE Guide: when any sound
  starts (audio or synth), every other playing sound stops. Covers:
  audio → audio (pause the previous), audio → synth, synth → audio,
  synth → synth. Listeners attach on each .pe-audio 'play' event and
  on every synth play-button click.

E2E infrastructure fixes (for the test failures we hit):
- auth-gated-smoke.spec.js now imports test + loginAs from the shared
  fixtures.js so the token cache is unified across every spec. Without
  this, each spec file's module-scoped _tokenCache multiplied logins
  and hit the 10/15min rate limit.
- server.js: /api/auth/login rate limit is now configurable via
  LOGIN_RATE_LIMIT_MAX env var (default 10, prod unchanged).
- docker-compose.e2e.yml: LOGIN_RATE_LIMIT_MAX="500" so Playwright's
  two-project (chromium + mobile-chrome) multi-worker runs can do
  their logins without tripping the cap. Prod container unaffected.
- fixtures.js console.error allowlist expanded to suppress known
  non-bugs: Cross-Origin-Opener-Policy warnings on http:// e2e
  server, transient 401/403/404/503 resource loads, ERR_BLOCKED_BY_CLIENT.
2026-04-22 22:57:11 +02:00
github-actions[bot]
b80a91e40a Release v6.19.0 2026-04-22 19:51:04 +00:00
Daniel
2913b09abd feat(pe-guide): 7 cardiac sounds, APTM image full-width + tap-to-zoom, synth stop/progress
Three user-flagged fixes:

1. Cardiac sounds library expanded from 3 to 7 (from Wikimedia Commons
   heart-sounds + heart-murmurs subcategories). All real recordings:
   - Normal (61 bpm)                 [existing]
   - Infant heartbeat                [new — pediatric reference]
   - VSD                              [existing]
   - Mitral valve prolapse            [existing]
   - Still's murmur in a toddler      [new — classic innocent murmur]
   - Functional murmur (adult female) [new — benign flow murmur]
   - PVCs                             [new — arrhythmia]
   All with native <audio controls> (play/pause/seek/elapsed/total
   work on desktop AND mobile for free).

2. APTM diagram is now full-width on every device, no longer sharing
   row space with the legend on mobile:
   - Image always on its own row, max-width:420px, centered
   - Wrapped in an <a href target="_blank"> — tap/click opens the PNG
     full-size in a new tab where browser pinch-zoom works natively
   - "Tap to open full-size" hint line under the image
   - Legend below uses auto-fit minmax(min(100%,260px),1fr) so it stacks
     on narrow viewports without cramping

3. Synth sounds (rhonchi, pleural rub, grunting, normal vesicular — all
   Web-Audio-API generated, no native controls) now have:
   - Play button → kicks off playback and hides itself
   - Stop button appears, lets user terminate early
   - Progress bar animates over 3.2 s, resets at end
   - Only one synth sound plays at a time (clicking another stops the
     previous)
2026-04-22 21:50:54 +02:00
github-actions[bot]
bf62d15ad6 Release v6.18.0 2026-04-22 19:10:45 +00:00
Daniel
9bbfb4ce83 feat(pe-guide): native audio controls + cardiac sounds library + mobile layout
Three user-flagged issues fixed together:

1. Mixed real/synth audio was labelled only "synthesised samples" — misleading
   since wheeze, stridor, fine+coarse crackles are real Wikimedia recordings.
   Now each sound card has a REAL or SYNTH badge and the library header
   reads: "real recordings where available; synthesised approximations
   labelled SYNTH".

2. No murmur sounds in the CV section. Added a Cardiac sounds library
   between the APTM diagram and the innocent-murmur panel:
     - Normal heart sounds (S1, S2) — 61 bpm reference
     - Ventricular septal defect (VSD) — harsh holosystolic at LLSB
     - Mitral valve prolapse (MVP) — mid-systolic click + late systolic
   All 3 are real recordings from Wikimedia Commons.

3. No pause / stop / duration controls. Replaced the synth-only play
   button with a native <audio controls> element for every real
   recording — gives play, pause, seek, elapsed/total time, volume
   for free on desktop AND mobile (browser-native, accessibility-
   compliant, consistent with platform conventions). Synth sounds
   (rhonchi, pleural rub, grunting, normal vesicular) keep the one-
   shot play button since they\'re Web-Audio-API generated and don\'t
   support seeking.

Mobile layout:
- The APTM diagram + legend was hard-coded 2-col (minmax(280px,1fr) 1fr)
  which could overflow narrow screens. Switched to repeat(auto-fit,
  minmax(280px,1fr)) — stacks to 1-col below ~580 px viewport.
- Refactored sound-library + APTM render into helpers (renderSoundCard,
  renderSoundsLibrary, TWO_COL_GRID constant) to reduce duplication.
2026-04-22 21:10:36 +02:00
github-actions[bot]
7d860c5287 Release v6.17.1 2026-04-22 19:04:54 +00:00
Daniel
b7a2e15107 fix(entrypoint): docker-compose env overrides win over OpenBao-fetched values
The previous entrypoint unconditionally exported every key from
kv/ped-ai/prod. This broke the e2e container, which needs
TURNSTILE_SECRET_KEY="" and SMTP_HOST="" set via docker-compose
environment block so login works without bot challenge and register
auto-verifies. OpenBao's real values were overriding those empties,
re-enabling Turnstile and email on e2e.

Fix: before the OpenBao fetch, snapshot every env var name already
defined (env_file + environment: block). During the export loop,
skip any OpenBao key that's already in the snapshot. Docker-compose
wins, OpenBao fills in the rest.

Impact:
- Prod container: no change (env_file only has OPENBAO_* bootstrap
  vars, which aren't in the KV payload anyway)
- E2e container: TURNSTILE_SECRET_KEY="" and SMTP_HOST="" preserved
  even when the image is rebuilt from the current source tree
- Any future per-container override via docker-compose environment:
  block just works

Log line now reports counts: "applied N secrets; M already set by
docker (kept override)".
2026-04-22 21:04:46 +02:00
Daniel
abb67bd03a test(e2e): PE Guide smoke — 8 tests covering all systems + age-group coverage
Uses the shared fixture so pageerror + console.error fail the test —
would catch any regression of the bug-class that shipped the SSO
ReferenceError. All tests log in as the seeded e2e user and drive the
real UI.

Coverage:
- tab loads with empty-state before age selected
- MSK (default) renders with scales + steps
- switch to Neuro shows MRC scale + teaching pearl
- Respiratory shows 8-sound library with play buttons + RR scale
- Cardiovascular shows APTM image (naturalWidth > 0 — actual network
  fetch confirmed), all 5 landmark letters, innocent-murmur panel,
  7 "S" criteria footer
- parametrised: every age group × {resp, cv} must NOT show "no data",
  must have ≥ 1 step (catches the regression Daniel just flagged)
- step toggle cycles Normal → Abnormal → Skip with visual state change
  and note-field show/hide
- grading scales <details> is collapsible and expands on click
2026-04-22 21:03:13 +02:00
github-actions[bot]
8cabe7da4b Release v6.17.0 2026-04-22 19:00:54 +00:00
Daniel
8be268df6a feat(pe-guide): real audio, innocent-murmur panel, APTM image, all-ages resp+CV
Four changes rolled together:

1. REAL AUDIO from Wikimedia Commons (CC BY-SA 3.0, attribution to follow
   in privacy policy per Daniel). Embedded in /public/audio/ — synthesis
   stays as fallback for sounds not available from Wikimedia.
   respiratorySounds.js now tries the real OGG first; falls back to Web
   Audio synthesis if file missing.

2. REAL APTM IMAGE from Daniel's Nextcloud share, placed at
   /public/images/pe-guide/aptm.png (134 KB PNG). Replaces the inline SVG.
   Kept the side legend with A/P/E/T/M colour-coded points.

3. INNOCENT MURMUR REFERENCE PANEL below the APTM diagram with the 5
   classic innocent murmurs (Still's, pulmonary flow, venous hum, carotid
   bruit, PPS) — age, location, character, confirming maneuver — plus
   the 7 "S" criteria summary.

4. ALL-AGE RESP + CV DATA. Before: only adolescent. Now every age group
   has age-appropriate resp + cv content (newborn through adolescent).
   Newborn: Silverman, pre/postductal sats, duct-dependent lesion screen.
   Infant: bronchiolitis, CHF diaphoresis, VSD, early CHD.
   Toddler: croup/FB/epiglottitis, innocent murmur peak age.
   Preschool + school-age: adult-pattern transition, sports screening.
2026-04-22 21:00:42 +02:00
github-actions[bot]
1aa785068b Release v6.16.0 2026-04-22 18:15:16 +00:00
Daniel
316a5e0338 feat(pe-guide): Cardiovascular system with APTM auscultation SVG
Fourth PE system added. Adolescent cardiovascular exam at the same
teaching-focused depth as respiratory/neuro: five components with
significance + pearls + detailed step methods + watch-for blocks.

APTM auscultation diagram (new, inline SVG, no image file):
- Stylised anterior chest with sternum, clavicles, ICS level lines,
  left mid-clavicular line
- Five colour-coded landmarks:
   A  Aortic    — 2nd ICS right sternal border
   P  Pulmonic  — 2nd ICS left sternal border
   E  Erb's pt  — 3rd ICS left sternal border
   T  Tricuspid — 4th ICS left sternal border
   M  Mitral    — 5th ICS mid-clavicular (apex)
- Side legend: location + what to listen for at each point
- Patient's-left / patient's-right labels to prevent mirror-image
  confusion

CV-specific grading scales (3 new entries in SCALES):
- Murmur grade Levine 1–6
- Pulse amplitude 0–4+
- Capillary refill time thresholds

CV components:
1. Inspection (general appearance, central/peripheral cyanosis,
   clubbing with Schamroth sign, precordial bulge, visible apex, JVP)
2. Palpation (apex position + character, parasternal heave, thrills
   at all 5 points, peripheral pulses upper + lower, radio-femoral
   delay for coarctation)
3. Auscultation — approach (positioning, diaphragm vs bell, systematic
   walk through all 5 points, left lateral decub for MS, leaning
   forward for AR)
4. Auscultation — heart sounds + murmurs (S1, S2 split, S3/S4 gallops,
   murmur characterisation by timing/location/radiation/character,
   Levine grading, dynamic maneuvers, innocent-murmur "7 S" pearl)
5. Peripheral vascular (four-limb BP for coarctation, radio-femoral
   delay, bounding pulse differential)

UI wiring:
- renderSystem() emits APTM diagram card at the top of cv system,
  before scales. Two-column layout: SVG on left, legend on right.
- accentMap/iconMap/labelMap extended with cv = rose accent,
  heart-pulse icon, "Cardiovascular" label
- New sub-tab pill in pe-guide.html
2026-04-22 20:15:08 +02:00
github-actions[bot]
8efc4e9a56 Release v6.15.0 2026-04-22 18:10:04 +00:00
Daniel
83a78fa8cd feat(pe-guide): Respiratory system with synthesized sounds library
Third PE system added. Adolescent respiratory fully fleshed out with
the same teaching-focused depth as neuro: overview, grading scales,
per-component significance + pearls, detailed step methods with HOW
and NORMAL labels, and a watch-for red-flag block.

New respiratorySounds.js uses the Web Audio API to synthesize 8 classic
breath sounds on demand — no network, no audio files, no licensing:
  - Normal vesicular
  - Wheeze (two-partial + vibrato, filtered sawtooth)
  - Stridor (inspiratory, bandpass-filtered sawtooth sweep)
  - Fine crackles (dense brief high-freq noise bursts, late inspiration)
  - Coarse crackles (sparser, longer, lower-freq bursts)
  - Rhonchi (low-pitched warbled sawtooth, expiratory)
  - Pleural friction rub (bandpass noise, biphasic)
  - Expiratory grunting (square-wave short grunts)

Sounds are synthesised approximations intended to teach the pattern
(what makes a wheeze a wheeze vs a stridor). Labelled as such in the UI.
Controls: one play at a time, auto-stop ~3s.

Respiratory-specific grading scales:
  - RR by age (WHO tachypnea cutoffs)
  - Pulse ox (SpO2) with hypoxemia thresholds
  - Silverman–Andersen (neonatal retractions, 0–10)
  - Westley croup severity score

Components in adolescent respiratory:
  1. Inspection (observation-first — RR, pattern, WOB, audible sounds,
     chest shape, colour, clubbing with Schamroth sign)
  2. Palpation (trachea, expansion symmetry, tactile fremitus,
     tenderness, subcutaneous emphysema)
  3. Percussion (technique + systematic zones + cardiac/hepatic
     dullness + diaphragmatic excursion)
  4. Auscultation — normal breath sounds (vesicular, bronchovesicular,
     bronchial) with systematic side-to-side comparison
  5. Auscultation — adventitious sounds with per-sound listen buttons
     linking directly to the sounds library
  6. Special maneuvers — bronchophony, egophony, whispered pectoriloquy

Older age groups (newborn through school-age) will get their own resp
blocks incrementally — v1 focused on adolescent for the quality bar.

UI: new sub-tab pill "Respiratory" with lung icon, sky-blue accent.
renderSystem refactored to use accent/icon maps instead of per-system
if/else — scales to future systems (cardiovascular coming next).
2026-04-22 20:09:54 +02:00
github-actions[bot]
4e1a870fe2 Release v6.14.0 2026-04-22 17:35:12 +00:00
Daniel
33bfc0bfbc feat(pe-guide): teaching-focused redesign — grading scales, pearls, significance
User feedback: the exam steps were too generic (e.g. "Shoulder abduction
— 5/5 bilaterally" never explained what 5/5 means or HOW to test it).
The guide needs to serve as a teaching tool, not just a checkbox list.

Three changes:

1. GRADING SCALES reference card (new). Collapsible panel at the top of
   each system showing the relevant scales:
   - Neuro: MRC strength (0-5), DTR (0-4+), Plantar response
   - MSK: Scoliometer ATR, Beighton hypermobility score
   Each scale shows the grade AND its clinical meaning in a compact
   table. No more orphan "5/5 bilaterally" without definition.

2. Per-component SIGNIFICANCE + PEARL fields (optional). Adolescent
   neuro components enriched with:
   - Significance: one-line clinical relevance (what this component is
     actually for — what pathologies it detects)
   - Teaching pearl: a Hutchison/Bates/Nelson-style tip that helps the
     learner see past the mechanics to the reasoning
   Visually distinct — pearl gets a warm amber accent, significance is
   a crosshair icon under the name.

3. Method strings REWRITTEN for every adolescent strength step. Before:
   "Shoulder abduction — 5/5 bilaterally". After: "Patient abducts both
   arms to 90°. Examiner pushes down on each arm just above the elbow
   while patient resists. Compare sides. — Holds against full resistance
   — MRC 5/5 bilaterally". Same treatment for all 14 strength steps,
   all 8 DTR steps, and tone/pronator-drift.

UI redesign:
- Accent bars on cards (cyan for MSK, purple for neuro) for visual
  anchor
- Numbered step circles instead of "1." prefix
- HOW / NORMAL label badges on each step
- Watch-for block with red left-border for red-flag grouping
- System-level header with icon (bone for MSK, brain for neuro)

Other age groups (newborn through school-age) keep the old data shape
(steps without pearls) — they still render correctly, just without the
pearl/significance blocks. Enriching them is an incremental follow-up.
2026-04-22 19:35:03 +02:00
github-actions[bot]
31507a4f09 Release v6.13.1 2026-04-22 17:30:03 +00:00
Daniel
b37c565cf0 fix(ui): replace native alert/confirm with showConfirm/showToast helpers
Daniel flagged the native browser confirm() dialogs in Extensions as ugly
and incompatible with the app's design. There was also a stray alert()
in calculators.js resus-meds weight validation.

Replaced:
- extensions.js: confirmDelete → showConfirm(..., {confirmText: 'Move to trash'})
- extensions.js: confirmPurge  → showConfirm(..., {danger: true, confirmText: 'Delete permanently'})
- calculators.js:2060 alert() → showToast(..., 'error')

All three helpers (showConfirm, showToast) are already defined as globals
in public/js/app.js. The design already had a modal — I should have used
it from the start.

Audit confirmation: `grep -rnE '\b(alert|confirm|prompt)\s*\(' public/`
now returns only comment references and the showConfirm definition
itself. No native dialogs remain anywhere in the frontend.

Playwright test updated to click the in-app modal's #confirm-modal-ok
and #confirm-modal-cancel buttons instead of intercepting page.on('dialog').
2026-04-22 19:29:53 +02:00
Daniel
8ea79c7f30 test(e2e): shared fixture with pageerror + console-error guards + Extensions CRUD suite
Two additions:

1. e2e/fixtures.js — shared test infrastructure
   - Custom `test` extending @playwright/test with two auto-fixtures on
     every page: page.on('pageerror') and page.on('console') of type
     'error'. Any uncaught JS error fails the test. This is the SSO-
     bug-class safety net: if we'd had this earlier, the silent
     admin.js ReferenceError would have failed CI instead of shipping.
   - `authedPage` fixture — logs in via API, injects session cookie,
     provides pre-authed page ready to drive.
   - `mockAI(page)` helper — intercepts generate-* and transcribe
     endpoints with canned JSON responses. Enables fast deterministic
     CI runs. Opt-out via E2E_USE_REAL_AI=1 to hit real LiteLLM.
   - Console-error allowlist for known noise (favicon 404, lazy-loaded
     Whisper models, etc.).

2. e2e/tests/extensions-crud.spec.js — 11 tests covering the new
   Pagers & Extensions feature end-to-end:
   - empty state, add extension, add pager (correct grouping)
   - edit persists, search by location + by number
   - soft-delete with confirm → moves to trash
   - restore from trash → reappears in active
   - purge from trash → permanent
   - cancel dialog keeps item, cancel form keeps nothing, validation
     on required fields

Known follow-up: the e2e container (pediatric-ai-scribe-e2e) is still
on the pre-entrypoint image from before the OpenBao migration, so it
doesn't have the Extensions routes yet. Rebuilding it needs a small
entrypoint enhancement to honor docker-compose-level env overrides
vs OpenBao-fetched values (e.g. TURNSTILE_SECRET_KEY="" for the e2e
instance). That's separate work — this commit just lays in the tests.
2026-04-22 19:27:05 +02:00
github-actions[bot]
651f799c17 Release v6.13.0 2026-04-22 16:54:37 +00:00
Daniel
d859c8c5a9 feat(extensions): personal Pagers & Extensions directory with soft-delete
New top-level tab positioned after Physical Exam Guide. Per-user
directory of hospital phone extensions and pagers — grouped by location
then type, searchable, soft-deleted.

Data:
- New table user_phone_extensions (id, user_id, location, name, number,
  type CHECK (extension|pager), notes, trashed_at, timestamps).
  Partial indexes on active vs trashed rows for fast filtering.
- Not PHI — hospital internal phone directory. Plaintext.

API (all user-scoped, all params validated):
- GET    /api/extensions?trash=1&q=text  — list active or trash, optional search
- POST   /api/extensions                  — create
- PUT    /api/extensions/:id              — update (requires all three core fields)
- DELETE /api/extensions/:id              — soft-delete (sets trashed_at)
- POST   /api/extensions/:id/restore      — un-trash
- DELETE /api/extensions/:id/purge        — hard-delete (only if trashed)

All :id params parsed + validated (positive integer) before query.
All queries parameterized, every WHERE includes user_id scoping.

UI (public/js/extensions.js + components/extensions.html):
- Search bar with 200ms debounce, server-side LIKE on location/name/number/notes
- Add button expands inline form — location (with datalist of existing
  locations for autocomplete), name/dept, number, type, optional notes
- Each entry renders as a card: big monospace number, dept, type badge,
  edit + delete inline
- Grouped by location → type (Extensions / Pagers subheaders)
- Trash view: toggle shows trashed items with Restore + Purge actions
- Trash count badge on the Trash button updates after every delete/restore
- Delete requires confirm() dialog, then soft-delete (easy to undo)
- Purge from trash requires a second confirm() ("cannot be undone")
- Esc closes the form; form resets between Add and Edit
2026-04-22 18:54:26 +02:00
Daniel
917d4f8115 chore(pe-guide): restore source-citation comment block
Daniel clarified the earlier feedback: not "never cite in code", but
"ask before citing". He confirmed this block should stay.
2026-04-22 18:08:19 +02:00
Daniel
ee4940e0a6 chore(pe-guide): remove source-citation comment block
Daniel prefers no citations embedded in the code. Keeps the data-model
comment which is load-bearing for future edits.
2026-04-22 17:49:36 +02:00
github-actions[bot]
c35b05fc5a Release v6.12.0 2026-04-22 15:15:37 +00:00
Daniel
c13ba04955 feat(pe-guide): step-by-step OSCE checklist, Bates/Nelson/Hutchison sourced
Replaces the generic one-line-per-component format with a step-level
checklist. Each exam component now contains 3–13 discrete steps, each
with its own Normal/Abnormal/Skip toggle and optional abnormal note.
Physician ticks the exam off step-by-step; report generation
summarises at the component level but knows exactly which steps were
performed.

Example — previously the adolescent "Cranial nerves (II–XII)" was a
single row: "How to perform: Full formal adult-pattern exam. Expected:
All cranial nerves intact." That's unhelpful. Now it's 14 discrete
steps: CN I, CN II acuity, CN II fields, CN II fundoscopy, CN II/III
pupils, CN III/IV/VI EOM, CN V sensation V1/V2/V3, CN V motor, CN V
corneal, CN VII forehead/eye-close/smile/puff, CN VIII, CN IX/X, CN
XI, CN XII — each with specific method and expected finding. Same
depth for MSK: scoliosis = 5 discrete steps (standing inspection,
Adam forward-bend, rib-hump check, scoliometer, plumb-line), joint
stability = 8 named tests (Lachman, anterior drawer, varus/valgus,
McMurray, apprehension, Neer/Hawkins, anterior drawer ankle, talar
tilt), Beighton = 5 per-joint measurements, etc.

Sources cited in code header: Bates' Guide 13th ed, Nelson Textbook
22nd ed, Hutchison's Clinical Methods 25th ed, Fenichel Clinical
Pediatric Neurology 8th ed.

Backend route accepts the flat step array (grouped by component on
the server), passes structured text to the AI with methods and
expected findings per step. Prompts updated to summarise at the
component level rather than step-by-step, so output is clinically
readable.

Scope: MSK + Neuro × 6 age groups (newborn, infant, toddler, preschool,
school-age, adolescent). More systems follow the same pattern —
append to PE_DATA.
2026-04-22 17:15:27 +02:00
github-actions[bot]
22dd5cc8f4 Release v6.11.0 2026-04-22 14:52:24 +00:00
Daniel
ac5292b015 feat(pe-guide): Physical Exam Guide tab — OSCE reference + narrative report
New top-level tab (positioned after Catch-Up Schedule) combining two
functions:
1. Study reference — for each (age group, system) shows OSCE-style
   components with technique, expected normal finding, and abnormal-
   feature watch-list.
2. Documentation generator — physician marks each component
   Normal / Abnormal (with free-text detail) / Skip; AI produces a
   two-section report (Technique + Findings), narrative or structured
   list format.

Scope v1: MSK + Neuro × 6 age groups (newborn, infant, toddler,
preschool, school-age, adolescent). More systems can be added to the
embedded PE_DATA in peGuide.js without route changes.

Files:
- src/routes/peGuide.js      — POST /api/generate-pe-narrative (mirrors
                                milestone-narrative pattern: AppRole-level
                                injection guard, clinical audit category,
                                PHI redaction upstream already in place)
- src/utils/prompts.js       — peGuideNarrative + peGuideList prompts,
                                structured two-section output
- public/components/pe-guide.html — demographics bar + sub-pills + cards
- public/js/peGuide.js       — embedded PE_DATA (all clinical content),
                                render + state + AI call
- public/index.html          — tab button, section, script include
- server.js                  — mount route at /api

No schema change. No PHI stored — findings live in memory only, exported
via existing copy/read-aloud/Nextcloud actions.
2026-04-22 16:52:13 +02:00