REMOVED:
- Milestone editing UI from Admin Panel (per user request)
- Milestones will be managed via hardcoded static data only
- Kept backend routes and database support for future use
ADDED:
- Refresh button in Learning Hub CMS content list
- Manual refresh for AI-generated content updates
- Better discoverability of content refresh functionality
FIXES:
- AI learning content now has visible refresh button
- Users can manually refresh content list after AI generation
- Cleaner admin panel without milestone management clutter
NOTE:
- Developmental milestones still work via static fallback
- Edit milestones by modifying public/js/milestonesData.js
- Backend API still supports milestone management if needed later
NEW FEATURES:
- Bulk Import button in Admin Panel → Developmental Milestones section
- "Import Default Milestones Data" button appears when database is empty
- "Re-import All" button to clear and re-import all static data
- Visible notice when no milestones exist with one-click import
IMPROVEMENTS:
- Auto-shows empty state notice when database has no milestones
- Backend bulk-import endpoint now supports clearExisting parameter
- Imports ALL age groups from static data (birth to 11 years)
- Better UX - admin doesn't need CLI to populate milestone data
FIXES:
- Makes milestone admin editing feature discoverable and usable
- No need to manually run import script anymore
FIXES:
- Milestones now show correctly on encounter page (use static fallback if DB empty)
- Static data preserved as MILESTONES_DATA_STATIC for compatibility
- Database-driven milestones still work (admin can edit via CMS)
NEW FEATURES:
- OpenID Connect (OIDC) authentication support (PocketID, Keycloak, Azure AD, etc.)
- Comprehensive setup guide: OPENID_SETUP.md
- Auto-linking existing users by email on SSO login
- Multiple PDF upload support in Learning Hub (up to 10 files)
- 100 MB per file limit (was 20 MB)
- Full PDF content used for AI generation
- Embeddings use first ~8K chars for semantic search
IMPROVEMENTS:
- Updated UI to show multiple file selection with list
- Drag-and-drop supports multiple files
- Better file upload validation and error handling
- Added clarifying comments about embedding truncation
FINAL WORKING SOLUTION:
Previous attempts failed because:
- transformers.js v2.17.2 is ES module-only
- Module workers require complex CSP and external imports
- importScripts() doesn't work with ES modules
Solution:
- Use transformers.js v2.6.2 (has worker-compatible UMD build)
- Bundle library + models, serve entirely from our server
- Classic worker with importScripts() - no CSP issues
What's self-hosted:
- ✅ transformers.min.js (760KB) - at /models/transformers.min.js
- ✅ Whisper models (42MB) - at /models/Xenova/whisper-tiny.en/
Worker loads:
1. importScripts('/models/transformers.min.js') - OUR SERVER
2. Loads models from /models/ - OUR SERVER
3. ZERO external network calls
4. Works in any network (firewalled, air-gapped, etc.)
This is the production-ready, truly offline solution.
Issue: transformers.js is an ES module package and cannot be loaded
with importScripts() in classic workers.
Solution:
- Changed to module worker (type: 'module')
- Import transformers.js from CDN as ES module
- Models (42MB) still served from local server at /models/
Trade-off:
- Library (900KB): Loads from cdn.jsdelivr.net once, cached
- Models (42MB): Self-hosted, served from /models/ (no CDN)
This is necessary because:
1. @xenova/transformers is ES module-only (package.json: "type": "module")
2. ES modules cannot use importScripts()
3. Module workers require HTTPS for imports
4. CDN is HTTPS and cacheable
If CDN is blocked:
- Use Web Speech API (with privacy warnings)
- OR use Server Transcription (Vertex AI/AWS)
Models remain self-hosted as they're 40MB+ and contain the AI.
Provides two transcription options:
1. Browser Whisper (Offline, Batch) - RECOMMENDED
- 100% offline, zero network calls
- HIPAA-compliant, audio never leaves device
- Highest accuracy (Whisper)
- Processes after recording (batch mode)
- Models self-hosted, bundled in v2
2. Web Speech API (Real-time, Streaming) - EXPERIMENTAL
- Real-time transcription (see words as you speak)
- Uses browser's built-in speech recognition
- ⚠️ Sends audio to cloud (Chrome/Edge → Google)
- ⚠️ NOT HIPAA-compliant
- Requires user consent with clear warnings
Features:
- Settings UI for both options
- Clear privacy warnings for Web Speech
- Mutual exclusion (only one active at a time)
- Browser detection shows which provider is used
- Confirmation dialog before enabling Web Speech
Use Cases:
- Clinical/HIPAA: Use Browser Whisper only
- Personal/Non-clinical: Can use Web Speech for real-time feedback
- Maximum privacy: Browser Whisper (offline)
- Maximum speed: Web Speech (if privacy not required)
Implementation:
- speechRecognition.js: Web Speech API wrapper
- transcriptionSettings.js: Settings UI handler
- Privacy info displayed per browser
User can choose based on their privacy vs. speed preference.
Features:
- Admin can add, edit, and delete developmental milestones via dashboard
- Milestones stored in PostgreSQL (developmental_milestones table)
- Client-side loads milestones from API instead of static file
- Import script to migrate existing static data to database
- Organized by age group and domain
- Supports sorting and filtering
Admin UI:
- New section in Admin panel for milestone management
- Filter by age group
- Add/Edit modal with validation
- Delete with confirmation
- Auto-complete for age groups and domains
API Endpoints:
- GET /api/milestones-data - Public endpoint for authenticated users
- GET /api/admin/milestones - List all milestones (admin only)
- GET /api/admin/milestones/meta - Get age groups and domains
- POST /api/admin/milestones - Create milestone
- PUT /api/admin/milestones/:id - Update milestone
- DELETE /api/admin/milestones/:id - Delete milestone
- POST /api/admin/milestones/bulk-import - Bulk import
Usage:
1. Run import script: node scripts/import-milestones.js
2. Access Admin dashboard → Developmental Milestones section
3. Add/Edit/Delete milestones as needed
BREAKING FIX: Browser Whisper now fully self-contained
Previous issue:
- Loaded transformers.js from cdn.jsdelivr.net
- Downloaded models from cdn-lfs.huggingface.co
- Failed in corporate/clinical networks with firewall
- Stuck at "Initializing..." with no progress
Solution:
- Bundle transformers.js library (~876KB)
- Bundle Whisper tiny.en model (~42MB)
- Serve everything from local server
- Works in ANY network environment
Changes:
- whisperWorker.js: Load transformers from /models/ instead of CDN
- Dockerfile: Download models during Docker build
- Add download script for local dev
- Add comprehensive setup documentation
Docker image size: +~42MB (one-time cost, runtime benefit)
Tested: Works on unrestricted and firewalled networks
REALITY CHECK: Browser Whisper CDN loading cannot work in all environments
- Corporate firewalls block cdn.jsdelivr.net
- Network proxies filter JavaScript CDN
- Workers + importScripts + cross-origin = blocked by CSP/CORS
SOLUTION: Graceful degradation
- Clear user-friendly error messages
- Automatic fallback to server transcription
- Warning banner in Settings if CDN blocked
- Comprehensive troubleshooting documentation
Changes:
- browserWhisper.js: Show toast on worker error, fallback gracefully
- app.js: Display CSP warning banner on preload failure
- settings.html: Add warning about network/firewall requirements
- BROWSER_WHISPER_TROUBLESHOOTING.md: Complete guide for users
Key Message:
Browser Whisper is OPTIONAL. Server transcription (Google/AWS/OpenAI)
is the primary method and works everywhere. Browser Whisper is a
privacy-focused bonus feature that requires CDN access.
User Experience:
- If CDN works: Great! Browser Whisper available
- If CDN blocked: No problem! Server transcription works perfectly
- Clear messaging: User knows what to expect
- Allow empty voice value to preview server default
- Display 'server default' in preview text
- Clears user preference (sets to null) when testing default
ROOT CAUSES FOUND AND FIXED:
1. TTS Preview not working: voicePreferences.js listening for wrong event
- Was: 'tab-loaded' (never dispatched)
- Now: 'tabChanged' (correct event name used by app.js)
- Added immediate init if page already loaded
- Added 500ms delay for DOM readiness
2. Browser Whisper CDN blocked: CSP too restrictive
- Added 'unsafe-eval' to scriptSrc (required by transformers.js)
- Added cdn.jsdelivr.net to connectSrc (worker importScripts)
- Added childSrc directive for worker script loading
- Better error messages in worker
3. Worker loading errors: Now logged with specific reasons
- importScripts wrapped in try-catch
- Posts error message to main thread
- Verifies transformers object exists after load
Testing:
- TTS Preview should now work when clicking Settings tab
- Browser Whisper should load from CDN (or show specific error)
- Console logs will show exact init sequence
The button was always returning null because it searched for onclick=speakText
but all output cards use data-action="speak" data-target="id". Now checks
data-action first so the button correctly toggles to Stop during playback.
Emails: white card, clean typography, dark button, no gradients.
Same minimal aesthetic as Linear/Resend/Notion emails.
Verify page responses also updated to match.
LiteLLM /audio/transcriptions gives 'Unmapped provider' for Vertex AI Chirp.
The correct approach: use /v1/chat/completions with a Gemini model and send
audio as base64 input_audio content block — Gemini natively understands audio.
Set LITELLM_STT_MODEL to your Gemini model name (e.g. gemini-2.5-flash).
- Raise express.json limit from 1MB to 10MB — handles large chart reviews
with many notes (50 full clinic notes ≈ 600KB, well within new limit)
- Client-side: warn user if payload >8MB, friendly toast if >30 notes
- Bump to v13.0.0
- Bug 1: When user selected "Outpatient" review type but had any subspecialty
visit cards filled in, the backend ignored the top-level type and switched to
the subspecialty prompt. Fixed: top-level type dropdown is now definitive.
Per-visit note types only control data formatting/labeling, not prompt selection.
- Bug 2: Labs entered in a visit card were silently dropped for outpatient and
subspecialty visits (only ED visit labs were included). Fixed: per-visit labs
now appear immediately after their visit content, labeled with the visit date.
- Improved lab labeling: visit labs are labeled "Labs from this visit (date)"
and the separate labs section is labeled "ADDITIONAL LABS (not tied to a
specific visit)" so the AI clearly distinguishes them.
- TTS response now includes X-TTS-Provider header (google-tts, litellm/model, elevenlabs)
- Frontend reads header and shows actual provider in toast instead of hardcoded "Adam/ElevenLabs"
- CORS exposes X-TTS-Provider header so frontend can access it
- Updated .env.example: clarify that LITELLM_TTS_MODEL and LITELLM_STT_MODEL
can be either the model_name alias OR the full provider/model path depending
on your LiteLLM config (important for BAA compliance routing)
vertex_ai/chirp does not work via LiteLLM's audio transcription proxy.
Changed default LITELLM_STT_MODEL from vertex_ai/chirp to whisper-1.
Updated .env.example documentation to match.
STT: Vertex AI Chirp not supported via LiteLLM proxy (confirmed by docs).
Now uses Gemini directly (transcribeGoogle.js) — auto-detected when
GOOGLE_VERTEX_PROJECT is set, fallback to AWS then OpenAI.
TTS: LiteLLM Vertex TTS DOES work but requires the model_list ALIAS
(tts-1) not the underlying path (vertex_ai/text-to-speech).
Also pass voice param — LiteLLM supports Google Cloud voice names.
Auto-detected when LITELLM_API_BASE is set.
LiteLLM's atranscription has a routing bug with Vertex AI Chirp proxy.
AWS Transcribe is already configured and working. Auto-detect now prefers
AWS over LiteLLM. Use TRANSCRIBE_PROVIDER=litellm to force LiteLLM.
- TTS: switch to axios, drop voice param (configured in LiteLLM per model)
- STT: log full LiteLLM error body so 500s are diagnosable in logs
- TTS: same error detail logging
- Fix 'ElevenLabs unavailable' toast to generic 'TTS unavailable'
- Add red Stop button to encounter recording UI
- TTS: switch from OpenAI SDK to axios (same fix as STT), drop voice
param since it's configured inside LiteLLM per model
- Fix 'ElevenLabs unavailable' toast shown even when provider is LiteLLM
- Add dedicated red Stop button to encounter recording UI
Vertex AI Chirp via LiteLLM rejects/hangs when 'prompt' and
'response_format' are included — these are OpenAI Whisper-only params.
Send only file + model for LiteLLM/Chirp.
Having LITELLM_API_BASE for AI text was auto-routing audio transcription
through LiteLLM even when the proxy has no Whisper model configured,
causing silent hangs. Now LiteLLM STT only activates when LITELLM_STT_MODEL
is explicitly set. Falls back correctly to AWS Transcribe when configured.
OpenAI SDK's audio.transcriptions.create() hangs with LiteLLM
(no timeout, SDK-level incompatibility with multipart handling).
Use axios + form-data directly with 120s timeout — same approach
as ElevenLabs TTS. Handles both {text:"..."} and plain string responses.
Old pedscribe-v11 cache was serving stale admin.js to browsers
even after server updates. New cache name forces old SW to
deactivate and all clients to get fresh JS on next load.
Also switch JS/CSS from stale-while-revalidate to network-first
so code fixes are picked up immediately.
- 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