The MCP server builds a Nextcloud client per session and closes it only
when the session ends. The clinical assistant replaced its cached session
every time the ten-minute TTL lapsed but never ended the old one, so each
abandoned client held its Nextcloud connections open. The production MCP
container was holding 708 sockets in CLOSE-WAIT against a 1024 descriptor
ceiling, roughly 300 from the point where every clinical search fails.
Expired and server-rejected sessions are now deleted. Session reuse is
unchanged: a live session is still shared across calls, since closing one
still in use would force a re-initialize on every search.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reverts 126d7928 and 2d292d12 in full. The markdown normaliser and the trailing
emphasis and dollar handling return to exactly the state tagged
pre-citation-fixes-20260828, which has been rendering acceptably in use.
The one change kept is mermaid. DOMPurify 3.1.6 strips an attribute whose value
contains "-->", and every mermaid flowchart contains one, so the sanitiser
removed data-mermaid and querySelectorAll('[data-mermaid]') never matched: the
diagram sat on "Rendering graph..." forever. Reproduced against the exact pinned
build from cdnjs; sequence diagrams using "->>" were unaffected, which is why
only flowcharts failed.
The diagram source is now percent-encoded into the attribute and decoded when
read, so no arrow ever appears in an attribute value. Nothing about the
sanitiser configuration changes and no markup is newly allowed. The reader
tolerates an unencoded value, so anything already in flight still renders.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The earlier fix corrected two of the repair rules and left the rest with the same
flaw, and the punctuation boundary it introduced let the table case back in. A
review found the remainder.
Every repair rule inserted newlines with no idea whether it was inside a markdown
table row. A dose row such as "| Dexamethasone | 0.6 mg/kg PO (max 16 mg) - single
dose | 1 |" was split mid-row, which drops that row's citation and every row
below it out of the table. A "#" column destroyed the table outright. The rules
now skip table rows, dividers and headers entirely.
Ranges were still split in three other places: in a heading, so "### Dexamethasone
0.15 - 0.6 mg/kg" rendered as a heading reading "Dexamethasone 0.15"; after a
citation; and beside a hash, where "Room # 4" became a heading. A hyphen now
starts a list item only when a number does not follow, and a run-together heading
is recognised by the capital letter after the hashes rather than by position.
Adjacent citation merging could also cross a paragraph break, turning "[1]\n\n[2]
Vancomycin only if MRSA" into "[1, 2]Vancomycin only if MRSA" -- joining two
paragraphs and moving a citation onto a claim it never supported. Merging is now
limited to citations on the same line.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three rendering defects in the clinical assistant, all confirmed by running the
code rather than reading it.
A spaced hyphen anywhere in a sentence was rewritten as a list item, so every
numeric range written that way was split in two:
"Give dexamethasone 0.15 - 0.6 mg/kg orally."
-> "Give dexamethasone 0.15\n- 0.6 mg/kg orally."
which renders as a truncated sentence followed by a bullet, and a dose range
therefore reads as a different dose. The same applied to SpO2 targets, pH ranges
and age ranges. A hyphen now starts a list item only at the beginning of a line
or after sentence punctuation, which still catches the case the rule was written
for.
An answer ending in a bold phrase lost its closing marker, because trailing
emphasis was stripped unconditionally and the strip ran twice. Only an unpaired
marker is removed now.
Inline maths swallowed dollar amounts: "Costs $5 to $10 per dose" rendered the
text between the signs as an equation and dropped both signs. A $...$ span is
now treated as maths only when it contains something mathematical, so subscripts
and fractions still render.
Citation handling is unchanged and covered by the same tests: clusters still
merge and sort, unknown source numbers stay literal, links still resolve by index,
and bare numbers in a table's Source column still become bracketed tokens.
The state before these fixes is tagged pre-citation-fixes-20260828.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Voice lists were a single flat set from LITELLM_TTS_VOICES, so picking a
model could leave an incompatible voice selected and the request would
fail at the gateway. Voices are now resolved per model family (Kokoro,
Kitten, Supertonic, Groq Orpheus EN/AR), with a compatibility check that
falls back through user → admin → env → first valid voice. Groq Orpheus
requests also pin response_format to wav.
Also refreshes the cardiac/respiratory auscultation samples, extends the
well-visit component, and fixes the Android launch theme background
(@null → colorPrimary) so the splash does not flash through.
NOTE: this is in-progress work that was already sitting uncommitted in
the working tree; it is committed here as-is so the tree was clean for
the release bump.
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.
Calculators:
- Bedside tab consolidating emergency protocols (Neonatal+Apgar+NRP, Airway/RSI,
Cardiac Arrest/PALS, Respiratory, O2 & Ventilation, Status Epilepticus,
Sepsis & Fever with PECARN/Aronson/Rochester/Step-by-Step, Anaphylaxis,
Procedural Sedation, Agitation, Antiemetics, Antimicrobials, Burns with
Lund-Browder body-parts TBSA + Parkland, Toxicology, Trauma).
- Global age→weight estimator at top of Calculators tab (APLS + Best Guess).
- Pressure-time waveform SVG teaching graphic for Ventilation.
- Algorithm image lightbox (fullscreen, Esc/tap-to-close).
- Every weight-based dose shows mg/kg inline for clinician verification.
- Drug tables wrapped in overflow-x:auto for mobile.
Infrastructure:
- Pure dose math extracted to public/js/calc-math.js (dual-export Node+browser).
- 36 unit tests in test/calc-math.test.js via node:test (zero new deps).
- "npm test" added to package.json.