Citation quality
- A citation naming a source that never came back is never rendered as a
link, so it appears as plain text and nobody learns it happened. It is now
measured on the server, where the answer and the sources both exist, so it
is seen whether or not a browser rendered it.
- Four Prometheus counters feed a Grafana dashboard (Ped-AI Citation
Quality): answers, citations written, answers affected, and individual
unresolved markers. Only answers with at least one unresolved citation are
stored, with the question and the titles retrieval returned, so an operator
can judge whether retrieval came back thin or the model over-cited. Rows
expire after 30 days: this is a quality signal, not a transcript log.
- Both answer paths are covered. /chat/stream is normal; /chat is the
fallback the client uses when streaming fails, so auditing only the first
would have hidden exactly the answers produced under failure.
- The tracker is resolved on demand and allowed to be absent. Seven test
files load this route with a hand-built list of permitted imports, and
adding a hard dependency would mean editing all seven — and the eighth
written later would break. Observation must never be able to fail an
answer, so a missing module simply means no tracking.
- Metric registration reuses an already-registered counter, because this
module can legitimately load twice in one process.
SSO settings on mobile
- Six rows were laid out inline: flex with a 160px label and an input that
would not shrink, so on a phone the row was wider than the screen with
nothing to scroll and no way to reach the rest. They use .admin-row now,
which already stacks below 640px. Verified at 390px and 360px: nothing
off-screen, no sideways overflow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
With more than one admin, every setting in the panel was editable by all of
them — prompts, model policy, retrieval budgets, SMTP, email templates.
ADMIN_LOCKDOWN separates running the service from changing how it behaves.
It is an environment variable on purpose: a setting could be switched off by
the very admin it restrains, so lifting this needs host access and a restart.
The server is the control. One gate refuses configuration writes rather than
a check in each of the fifteen write routes, because that list grows and a
route added later would quietly miss it. Reads always pass — lockdown hides
nothing. Day-to-day operation stays available: invitations, announcements,
registration, feature flags, and the test endpoints, which persist nothing.
A setting invented later is locked until someone deliberately makes it
editable, rather than defaulting to open.
The panel disables what it cannot save and says why, but that is courtesy;
the refusal is what enforces it.
Two things this taught me, both fixed: my first version painted the panel
from an IIFE, which the module conventions forbid, and fetched the whole
config a second time just to read one flag — breaking the test that pins
admin loaders firing exactly once. The state now rides on the invites
response the panel already requests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
registration_enabled was a single switch: open to anyone, or closed to
everyone. This adds the setting an operator actually wants in between — open
to people you invited.
A code is single-use, expires (7 days by default, 90 maximum), and can be
revoked or deleted. It is stored hashed with only its last four characters
kept, because an invite grants account creation and a database dump should
not hand someone a working one. The code is readable exactly once, in the
response that creates it.
The claim is a single conditional UPDATE carrying every condition, so two
registrations racing the same code cannot both succeed. It happens after the
account exists, so a code is never spent on a failed registration — and if
the race is lost, the just-created account is removed rather than left behind
as a free registration. The rejection never says which of the four reasons
applied; distinguishing them would tell someone probing codes which guesses
were closer.
Codes avoid I, L, O and U so they survive being read aloud or copied off a
screen, and matching ignores case and separators.
The sign-up field appears only when the server says a code is required. The
admin card creates, lists, revokes and deletes, and carries the toggle.
Verified against the live database: create, claim, second claim refused,
unknown code refused, revoking a used code refused, delete. 684 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
Admin links
- The [data-admin-jump] click handler had been pasted inside
discoverModels(), so it only registered once someone pressed Search in AI
Model Management, and re-registered on every later search. Normally
nothing intercepted the click, so href="#" did what it says: jump to the
top and leave "#" in the URL.
- This is an app, so the pointers are plain text naming the sections rather
than links, and the handler is gone.
Test flake
- admin-clinical-assistant-wiring failed about 1 run in 4 with "Unable to
deserialize cloned data due to invalid or unsupported version": node:test
reads a test file's results back over the child's stdout, and app.js's own
console.log landed inside a serialized frame.
- The page's console is now forwarded to stderr (jsdom 29: forwardTo).
Verified: child stdout clean, 0 failures in 32 stress runs (was 6 in 24),
and three full-suite runs at 668/668.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
Image models
- The Clinical Assistant "Image models" list waited on an old
#assistant-image-model dropdown that no longer exists, so discovery never
reached it and only four hard-coded fallbacks appeared — with no way to
add any of the gateway's 50 image models.
- Image Generation search rows now have + Add / Added. Added models are
saved as clinical_assistant.image_model_roster (validated server-side:
up to 100 ids) and appear in the Clinical Assistant list at once; ticking
one there offers it to users. Anything already allowed or configured
stays listed. Unsaved ticks survive an add.
- The roster notification is guarded, so it can never fail the settings load.
Phone top bar
- The page is drawn under the status bar (viewport-fit=cover) and its
theme colour was the removed header's blue, so on an iPhone content showed
scrolling at the top of the screen. The row is now a real fixed element
that extends behind the status bar (env(safe-area-inset-top), 0 in a
normal tab), the menu button, sources pill and drawers clear it, and the
theme colour is white.
Verified in Chromium: + Add -> saved roster -> listed unticked; tick kept;
remove works. Top bar is the only thing in the top 48px on all 22 pages;
phone menu positions unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4rptBNvn6RYieQw54GXNS
Phone
- The desktop collapse rules (52px icon rail) were unscoped, and menu-hidden
is remembered across viewports, so after collapsing on a desktop the phone
sheet opened with its links, the Assistant/Workspace switch and the account
name all display:none. Scoped them to min-width:769px.
- Workspace pill: the sheet now closes on tap, and the launcher is no longer
display:none below 640px, so the pill opens the workspace instead of
toggling a view that could never appear.
- The menu button has its own 48px row instead of floating over the page;
layouts subtract --mobile-bar (0px on desktop, so desktop is unchanged).
- Sources open as their own scrollable band under that row, and the toggle
sits in the row, instead of a sheet/FAB covering the composer.
- The sheet uses dvh, so the account card is not pushed below the visible
screen by the browser toolbar.
Admin
- Image Generation card beside TTS and STT: search the gateway's image
models and test one. No Set button: an image model is chosen per workflow.
It searches only on demand, so opening Admin still makes one discovery call.
- Account card sits at the same height in every rail state; the model
availability list points to where new models are added.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4rptBNvn6RYieQw54GXNS
loadOidcConfig() was called from the first IIFE in admin.js but declared
in the second IIFE. Function declarations don't cross IIFE boundaries,
so the call threw ReferenceError and left the SSO form empty with
"Disabled" selected — even when SSO was configured and working. Moved
the call into the tabChanged handler of the IIFE where the function
lives.
- Add neonatal assessment calculator: GA classification (extremely preterm through
post term), weight-for-GA percentile (AGA/SGA/LGA) using Fenton 2013 LMS data,
birth weight category (ELBW/VLBW/LBW/normal/macrosomia)
- Add DOCX support via mammoth, PPTX/ODT/EPUB via jszip in Learning Hub content
generator file upload
- Add gatewayUrl() helper for consistent API URL construction — handles
LITELLM_API_BASE with or without /v1 suffix, works with any OpenAI-compatible
gateway (LiteLLM, Bifrost, etc.)
- Fix TTS model/voice separation: discovery now tags items as MODEL or VOICE,
auto-detects provider from voice name (Vertex, ElevenLabs, OpenAI)
- Fix STT discovery to include ElevenLabs Scribe and Chirp models
- Fix TTS discovery to include ElevenLabs and Vertex voices alongside models
- Fix admin model test to bypass allowlist check (skipAllowlistCheck) so
discovered models can be tested before adding
- Fix Nextcloud token decryption in learningAI.js WebDAV browse and file import
- Fix admin embedding test to show DB model name instead of hardcoded default
- Fix admin STT test to use correct endpoint for Whisper models
- Add AI gateway migration guide to configuration docs
- Add Grafana dashboard JSON for Loki log visualization
- Replace browser prompt() with inline UI for: 2FA disable (password field),
admin password reset (inline input), admin test email (inline input)
- Remove all password breach warning UI (login, register, settings)
Backend HIBP check endpoint remains but is no longer called from frontend
- Remove model cost display from dropdown and header badge
- Hide empty cost-badge element in header
- Fix model dropdown to flat list (no category grouping)
- Fix model search for all providers: Bedrock now falls back to built-in
list (with live ListFoundationModels attempt), Azure returns built-in list
- Add Test button on every model row (built-in, discovered, custom) that
sends a live prompt and shows response + latency in a toast
- Add TTS management section: search voices from provider API (Google TTS
voices.list, LiteLLM /v1/models, ElevenLabs /v1/voices), Set as Default
writes tts.voice/tts.model to DB, runtime respects DB override
- Add STT management section: search models from provider (Gemini, Whisper,
LiteLLM, OpenAI, local), Set as Default writes stt.model to DB, runtime
respects DB override in transcribe.js
- Add Embedding models section: search from provider (LiteLLM, Vertex,
OpenAI), Set as Default writes embeddings.model+dimensions to DB,
embeddings.js respects DB override
- Add record-and-transcribe STT test (browser MediaRecorder)
- Add TTS synthesize-and-play test (returns base64 audio)
- Add embedding generate test (shows dims + vector sample)
- Expand PUT /config/:key(*) whitelist to include tts., stt., embeddings.
- Add @aws-sdk/client-bedrock as optional dependency for live Bedrock discovery
- LITELLM_MODELS = [] — no hardcoded models, global selector now only
shows what admin has actually added via Search API
- getAvailableModelsWithOverrides: for LiteLLM returns only custom list
- Remove toggle safety check — admin can disable any/all models freely
- Admin panel always reloads on tab open (was cached, showing stale data)
- Add 'Clear all models' button for LiteLLM to wipe and start fresh
- Add POST /config/models/clear-all endpoint
- Root cause: PUT /config/:key(*) wildcard was registered before
/config/models/toggle and /config/models/default, intercepting them
and returning "value is required" (body had modelId not value)
- Fix: move all model-specific PUT routes before the wildcard
- LiteLLM: return empty built-in list with discovery hint (hardcoded
models don't match user's proxy — must use Search API)
- After adding a discovered model: auto-select it in the default dropdown
- GET /config/models now returns defaultModel so dropdown pre-selects it
- Add Vertex AI provider (Gemini models via @google-cloud/vertexai SDK)
- Add LiteLLM proxy support (OpenAI-compatible, routes to any provider)
- Admin panel: model search/discover from provider API, enable/disable, custom models, set default
- New endpoints: /config/models/discover, /config/models/add-discovered, /config/models/default
- Updated models.js with VERTEX_MODELS and LITELLM_MODELS lists
- Updated health endpoint with vertex + litellm status
The admin model management (enable/disable, custom models, default override)
had persistent rendering issues. Removed the UI panel — models are managed
via the global model selector in the header, which works reliably. Backend
API endpoints for model config are retained for future use.
- Full model management UI in admin panel: toggle models on/off, add custom
model IDs (any OpenRouter/Bedrock ID), set admin-configured default model
- /api/models now returns admin-set default model, frontend respects it
- Toggle switch CSS for clean enable/disable UX
- Backend already had the API endpoints, this adds the missing UI
- New dedicated Content Manager tab (WordPress-like layout) with:
- Stats dashboard (published, drafts, categories, quizzes, attempts)
- Sidebar with category tree and filter controls (status, category)
- Table-style content list with search
- Full-page editor with title bar, meta fields, body editor, quiz builder
- Content Manager visible only for admin and moderator roles in sidebar
- Removed CMS from admin panel (admin panel is admin-only again)
- Email templates: fully responsive with max-width, proper mobile breakpoints,
table-based layout for Outlook/Gmail compatibility, no overflow
- Security: escape all user data in emails with escHtml(), escape APP_URL
in verify-email HTML, replace raw err.message with generic errors in auth
- Security: email body from admin settings now HTML-escaped before rendering
- New Learning Hub under Pediatric menu with content feed, category browsing,
search, content viewer, and interactive quizzes (MCQ, true/false)
- Quiz system with per-option wrong-answer explanations and general explanations
- Admin CMS for creating/editing categories, content (articles, pearls, quizzes),
and inline question builder with answer options
- 3-role system: admin (full access), moderator (can manage Learning Hub content),
user (all clinical features + learning hub read access)
- 5 new database tables: learning_categories, learning_content, learning_questions,
learning_options, learning_progress
- User progress tracking with score history per quiz
- Moderators see "Content Manager" tab instead of full Admin panel
- Fix 2FA status stuck at "Loading...": export load2FAStatus globally so
app.js tabChanged handler can call it when navigating via sidebar
- Fix admin models silent failure: add error message + retry button on catch
- Fix save encounter doubles: persist _savedEncId_* to sessionStorage so
page refresh reuses existing record instead of inserting a duplicate;
add _savingInProgress guard to prevent race-condition double-saves
- Desktop sidebar collapsible: add collapse/expand buttons (not collapsed
by default), state saved to localStorage; hidden on mobile
- Copy to Visit Note now also copies SSHADESS assessment to wv-shadess-text
- Store selected visit age in window._wellVisitAge + sessionStorage so
other tabs can read the patient's age
- Bigger screening/vaccine boxes: min-height on wv-vax-list, wv-screen-list,
wv-visit-detail; slightly more padding on vaccine items
- Wire New Patient (clearTab) buttons to document click handler
- Replace horizontal tab-nav with collapsible left sidebar (sticky desktop, slide-in mobile with hamburger toggle)
- Fix tab persistence: use localStorage instead of URL hash; restore last tab after login in enterApp()
- Fix admin CMS models loading: move autoDeleteDays read inside .then() callback (was ReferenceError), switch admin loading from click-only to tabChanged event
- Add Save/Load bars to Hospital Course and Chart Review tabs with popover support
- Add wv (well visit) save handler in encounters.js
- Register load handlers for hospital and chart tabs
- Expand popover prefix list to include hosp and chart