Follows the same pattern as Change Password and 2FA sections —
hidden by default in the HTML, revealed only when canLocalAuth=true.
Why: revoke technically deletes the PedScribe session row and clears
the cookie on that device, but the SSO user can re-auth instantly
because their IdP session is still live. Surfacing a "revoke" button
that the IdP will immediately undo is misleading. SSO users now see
only the SSO-relevant sections of Settings.
Sections were briefly visible for SSO-only users before load2FAStatus
resolved and hid them. Flipped the default: both sections now carry
style="display:none" in the HTML and are revealed only when the /me
fetch confirms the user has a real password hash.
SSO-only users never see the sections, even for a flash.
Middleware:
- Log to console.warn + audit_log when a session is killed for
inactivity. Shows up in Grafana/Loki so you can see how often
users actually get kicked. Audit action: 'session_idle_timeout'
- last_activity throttle bumped 5 min → 10 min — halves DB writes
per active user. Idle precision slop widens to 24h00-24h10;
still invisible in practice.
Per-user local-auth visibility:
- /api/auth/me now returns user.canLocalAuth: true when the stored
password is a real bcrypt / argon2 hash, false for the random
blob OIDC auto-creates for SSO-only users.
- Settings page hides "Change Password" and "Two-Factor
Authentication" sections when canLocalAuth is false — those UIs
are meaningless for users whose sign-in lives at the IdP.
- Password hash is not leaked in the /me payload.
Mobile (restating existing behaviour for clarity): no idle check,
365-day JWT in Keychain/Keystore, never auto-logs-out. Only logout
triggers are: manual logout, password change, admin revoke, JWT hit
365d, or app uninstall.
Session model:
Web — 24h sliding idle timeout enforced server-side via
user_sessions.last_activity. 30-day JWT + cookie are a
safety net; middleware is the real clock. Cookie is
re-set on active use so browsers match the sliding window.
Mobile — 365-day JWT, no idle timeout (stays persistent via Keychain
/ Keystore). Detected via User-Agent ("PedScribe" /
"Capacitor") or X-Client: mobile header.
2FA backup codes:
- 10 single-use codes generated when 2FA is first enabled
- Stored as bcrypt hashes in new users.totp_backup_codes column
- Consumed atomically on successful login fallback (when TOTP fails)
- Regenerate endpoint (POST /api/auth/2fa/backup-codes) requires
current password; invalidates prior codes
- Count endpoint (GET /api/auth/2fa/backup-codes/count) powers a
"N codes remaining" indicator on the 2FA settings card
- Modal shows codes exactly once with Copy + Download .txt actions
- Codes cleared when 2FA is disabled
New files:
src/utils/platform.js — isMobileClient() helper
Schema migration (idempotent):
ALTER TABLE users ADD COLUMN IF NOT EXISTS totp_backup_codes TEXT
- 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)
Security:
- Add session management: users can view/revoke active sessions in Settings
- Add password change in Settings (requires current password, HIBP check)
- Force logout all sessions on password reset
- Fix logout to destroy server-side session (was only clearing cookie)
- Add trust proxy for correct client IP in rate limiting and audit logs
- Add CORS support for multiple domains (CORS_ORIGINS env var)
- Add HIBP breach check endpoint and inline warnings on password fields
Audit logging:
- Add audit logging to all 24 PHI-handling endpoints across 13 route files
- Covers: generation, transcription, TTS, refine, encounters, documents, Nextcloud
- All fire-and-forget (no response delay)
AI improvements:
- Refine now includes original source material (transcript, notes, labs)
so AI can reference the full input when modifying output
- Add correction tracking (trackAIOutput) to sick visit and well visit tabs
- Fix sickvisit missing from encounter save noteIdMap
UI fixes:
- Non-blocking busy bar for transcription and AI generation (replaces full-screen overlay)
- Fix encounter recording: hide record button during recording (was showing two stop buttons)
- Fix ROS/PE "All WNL" stacking duplicate event handlers; add Clear buttons
- Enlarge AI instructions textarea in Learning Hub CMS
Domain:
- Primary domain now app.pedshub.com, with scribe.pedshub.com and peds.danvics.com as CORS origins
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.
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
Backend (src/routes/learningAI.js):
- POST /api/admin/learning/ai-generate
* Accepts: topic text, uploaded file (PDF/TXT/MD/HTML), or Nextcloud WebDAV path
* Extracts text from PDFs via pdf-parse; plain text read directly
* Fetches WebDAV files using stored Nextcloud credentials
* Prompts AI to return structured JSON: title, subject, body (HTML), questions[]
* Strips code fences, falls back to regex JSON extraction if needed
- POST /api/admin/learning/ai-refine — refine existing body with instructions
- GET /api/admin/learning/webdav-browse — PROPFIND-based Nextcloud file browser
- POST /api/admin/learning/webdav-path — save user's default browse path
Frontend:
- AI Generate button in CMS editor header (gradient purple→blue)
- Panel with 3 source tabs: By Topic / Upload File / Nextcloud
- Drag-and-drop file dropzone with DataTransfer API support
- WebDAV file browser: navigate folders, select files, breadcrumb path
- Options: question count, content type (article/quiz/pearl), model selector, instructions
- Refine Current Body: prompt-driven AI rewrite of existing content
- Generated content auto-fills title, subject, type, Tiptap body, quiz questions
Settings:
- Nextcloud section: "Learning Hub Default Browse Path" field (shown when connected)
- Saves to webdav_learning_path column via /api/admin/learning/webdav-path
DB: ALTER TABLE users ADD COLUMN IF NOT EXISTS webdav_learning_path TEXT