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
Shortens both JWT expiresIn and httpOnly cookie maxAge to 24h in
auth.js (local + register + reset flows) and oidc.js (SSO callback).
Rationale: shorter absolute session window for a PHI-adjacent app.
No sliding idle refresh — user re-logs in once a day.
capacitor.config.json:
- webContentsDebuggingEnabled: true → false
(was leaving Chrome DevTools able to attach to released builds)
- allowMixedContent: true → false
(API is HTTPS-only; no need to permit cleartext loads)
- server.allowNavigation: ["*"] → restricted to pedshub.com /
peds.danvics.com origins
(prevents WebView following an attacker-controlled redirect)
AndroidManifest.xml:
- android:allowBackup="false" + data_extraction_rules.xml
(Android system backup would otherwise copy EncryptedSharedPreferences
containing the auth token into Google Cloud backups)
- Removed USE_BIOMETRIC permission (feature removed earlier)
AudioRecordingService.java:
- startForeground(id, notif, TYPE_MICROPHONE) on Android 14+
(without the explicit type Android 14 kills the service with
MissingForegroundServiceTypeException)
- WakeLock cap: 1h → 8h (still bounded, onDestroy releases early)
MainActivity.java:
- Removed dead biometric code path and androidx.biometric imports
mobile/package.json:
- Dropped @aparajita/capacitor-biometric-auth — orphan dependency
Runtime split driven by window.Capacitor.isNativePlatform():
Web browser
- No token in localStorage / sessionStorage — XSS can't read it
- Server-set httpOnly cookie carries the session
- fetch() default credentials='same-origin' sends the cookie
- getAuthHeaders() returns Content-Type only, no Authorization
- Middleware already falls back to cookie when Bearer is absent
Capacitor native (iOS / Android)
- Unchanged — Bearer token lives in Keychain / Keystore via the
capacitor-secure-storage-plugin SecureStorage wrapper
- Bearer header still sent on every request
enterApp() / clearSession() / getAuthHeaders() all now branch on
isNativeApp(). Legacy localStorage entries from the dual-mode era
are wiped on clearSession() for users migrating in.
Rollback: git reset --hard pre-httponly-only-2026-04-14
Adds `npm run maint:check` (health report) and `npm run maint:reindex`
(REINDEX DATABASE + REFRESH COLLATION VERSION + ANALYZE) for post-
upgrade maintenance, modelled after Nextcloud's occ maintenance.
Documented in README.
Also relaxes postgres image from digest pin back to tag-pin
(pgvector/pgvector:pg16) — the auto-REINDEX-on-drift check in
database.js and the COLLATE "C" protection on critical indexes
make the digest pin redundant while blocking ordinary `compose
pull` updates.
idx_users_email and idx_sessions_token_hash now use byte-order
collation so a future ICU library bump cannot silently corrupt the
indexes the way it did this week. The columns themselves retain
their default collation; only the index comparison is C, which is
safe for these because:
- users.email is lowercased ASCII in practice
- user_sessions.token_hash is SHA-256 hex (pure ASCII)
Both are used for equality lookups only, never ORDER BY. Migration
is idempotent, gated on app_settings.migration.text_indexes_c.
Slug indexes on learning_* tables left at default for now — those
are also ASCII in practice but under lighter load; the startup
drift check + auto-REINDEX covers them.
Root cause of recent "invalid credentials on correct password" was
a silent btree index corruption: pgvector/pgvector:pg16 was pulled
with a different ICU library than the one used to build existing
indexes. Queries returned 0 rows even though matching heap rows
existed. Postgres logged nothing (corrupt index → empty result set
is a "successful" query) and the login path never logged unknown-
user attempts (enumeration protection).
Three defenses:
1. Pin postgres image by digest in docker-compose.yml so a
silent pull can't change ICU under our feet.
2. Startup collation-drift check in src/db/database.js:
compares pg_database.datcollversion to the library's actual
version and, on mismatch, runs REINDEX DATABASE + ALTER
DATABASE REFRESH COLLATION VERSION. Logs "Collation versions:
aligned" on clean boot.
3. Server-side console.warn on login lookup-miss (no email, no
audit row — preserves enumeration protection but gives
Grafana/Loki a signal for unusual miss rates).
Root cause for "invalid credentials" on correct password was a
corrupt btree index (idx_users_email) causing user lookups to miss
existing rows. Fixed by REINDEX DATABASE. Keeping a typed catch
around passwords.verify() so any future verify throw is logged
cleanly instead of bubbling as 500.
Percentile displays in growth charts, BMI, and mid-parental height
now show 2 dp (e.g. "37.42th") instead of 1 dp ("37.4th") for more
precision at tail percentiles.
Previously any form of zero total ("0 days", all blank) rejected
with "Enter age". Newborns at birth are a legitimate entry —
distinguish blank-all (error) from explicit-zero (valid).
Replace single text input with three number fields — years, months,
days — that all combine into fractional months. Fill any subset:
leave years blank for a newborn, leave months blank for "2 years",
enter just days for a 10-day-old.
Live hint below ("= 2 yr 5 mo (29 mo total)") still shows the
interpreted total. Parser from prior commit retained on window
for reuse elsewhere.
Replace [years] + [months dropdown] with a single text field that
accepts:
3y / 3 years / 3 yr
29m / 29 months / 29 mo
2y5m / 2 years 5 months / 2 yr 5 mo
3.5 years / 36 (plain number = months)
15 days / 2 weeks / 3y 2m 10d
Enables fractional ages so newborns can be plotted accurately
(WHO/CDC growth curves are continuous — "0 months" means at birth,
not a 0-27 day bucket, so a 15-day-old should plot at ~0.5 months).
Live hint below the field shows how the input was interpreted
("= 2 yr 5 mo (29 mo total)").
argon2 requires node-gyp + python3 + g++ + make to build its C
extension. Added as a virtual .build-deps package so it's compiled
during npm install, then purged to keep the Alpine image slim.
- App-layer AES-256-GCM crypto helper (src/utils/crypto.js)
- Nextcloud tokens encrypted at rest; transparent migration on next use
- Audio backups encrypted at rest (version byte 0x01 envelope); legacy
rows still decrypt as-is until overwritten
- argon2id password hashing via src/utils/passwords.js with bcrypt
fallback; bcrypt hashes rehashed to argon2id on next successful login.
argon2 package is optional — server keeps running with bcrypt only
until npm install adds the native dep
- PHI redactor for audit log details (src/utils/redact.js) — strips SSN,
phone, email, DoB, long IDs; caps at 500 chars; detects note bodies
- DOMPurify (cdnjs, SRI-pinned) replaces custom regex sanitizer in
Learning Hub content rendering
- SRI integrity hashes added for Font Awesome CSS and Chart.js
- Magic-byte file-type verification on document uploads
(src/utils/fileType.js)
- Generic 500 error responses via src/utils/errors.js applied to
nextcloud and audioBackups; full detail still logged server-side
- DATA_ENCRYPTION_KEY env documented in .env.example
Deploy: requires rebuild of the container image to pick up the new
files and `npm install` (adds argon2). Existing users keep working
because bcrypt stays available and crypto helpers pass through
plaintext when the key is not yet set in dev.
- JWT_SECRET fails fast at startup in production
- CORS fails closed if APP_URL + CORS_ORIGINS are both missing
- Explicit HSTS (1y, includeSubDomains, preload)
- Rate limit sensitive auth endpoints (change-password, 2FA)
- /api/health now returns {ok:true}; details gated behind admin auth
- Login enumeration removed — generic 401 + dummy bcrypt on miss
- ReDoS guard: 20KB input cap on /suggest-codes
- showToast uses textContent, no innerHTML
- clearSession() clears service worker caches on logout
- OIDC state is now HMAC-signed and stateless (survives restart)
- SSRF guard on admin-set OIDC issuer (blocks private IPs, requires HTTPS)
Adds docs/mobile-build.md covering APK build, release, git push,
keystore, and troubleshooting for both PedScribe and PedsHub apps.
Web still uses localStorage; Capacitor native app now routes
token/user/session-id through capacitor-secure-storage-plugin
(iOS Keychain, Android EncryptedSharedPreferences / Keystore).
A thin SecureStorage wrapper detects Capacitor at runtime and
falls back to localStorage elsewhere, keeping a single auth.js
codebase for both targets.
To activate on mobile: cd mobile && npm install && npx cap sync android
Loki logs now include:
- User agent string (browser/device identification)
- Session ID (ties actions to specific login session)
- Status field (success/failure)
New logging:
- encounter_load: logged when user opens a saved encounter (with label)
- copy_to_clipboard: logged when user copies note content (PHI access)
- Client event endpoint: POST /api/logs/client-event (auth required)
Encounter save/delete/load all include the encounter label for
patient identification in audit trail.
HIPAA audit trail now covers: who, what, when, from where, which
device, which session, what patient data, success/failure.
Exchange transfusion data (AAP 2022) now covers GA 35, 36, and 38+ weeks
with and without risk factors, hour-by-hour from 12-96h (510 more data
points). Total bilirubin data: 1020 data points (6 photo + 6 exchange
tables x 85 hours each). No interpolation needed for any hour.
Replace interpolated thresholds with exact hour-by-hour values
extracted from PediTools API for every hour from 12-96h:
- 6 phototherapy tables (GA 35/36/38 x with/without risk factors)
- 85 data points per table = 510 total values
- No interpolation needed — exact AAP 2022 nomogram values
- Exchange transfusion thresholds for GA 38 (with/without risk)
Phototherapy thresholds updated with exact values extracted from
PediTools (validated against AAP 2022 nomograms):
- Separate tables for GA 35, 36, and 38+ weeks
- With and without neurotoxicity risk factors
- Hour-specific values at 12, 24, 36, 48, 60, 72, 84, 96, 120h
Previous approximations were 1-3 mg/dL too low (conservative but
inaccurate). New values match the published AAP 2022 curves exactly.
Exchange transfusion thresholds added for GA 38+ weeks (with/without
risk factors). Displayed alongside phototherapy threshold in results.
GA selection expanded: 35, 36, 37, 38, 39, 40+ weeks.
Chart now shows both phototherapy and exchange transfusion lines.
Also: fixed Loki port conflict (3100->3101), added logs.pedshub.com.
GCS Calculator:
- Child/Adult and Infant versions with toggle
- Eye opening (4), Verbal (5), Motor (6) dropdowns
- Auto-calculates total score with severity classification
(Mild 13-15, Moderate 9-12, Severe/Coma 3-8)
- Infant-modified verbal and motor scales per Kids Kard
- Updates on every dropdown change (no button needed)
Equipment Sizing (Johns Hopkins Kids Kard):
- Select age/weight group (premie through 16+)
- Shows: BVM, oral/nasal airway, blade, ETT, LMA, Glidescope,
IV catheter, central line, NGT/OGT, chest tube, Foley
- All values from Johns Hopkins Children's Center Kids Kard
- ETT formulas shown as reference
Replace static vital signs table with interactive age group dropdown.
Each selection shows: HR (awake/sleeping), RR, SBP, DBP, temperature,
SpO2 target, weight range, and age-specific clinical notes.
10 age groups: preterm through 18 years. Values from Harriet Lane
Handbook 23rd Edition. Includes AAP 2017 BP classification thresholds
for ages 13+, ETT sizing formulas, and clinical pearls (orthostatic
testing, febrile tachycardia, athletic bradycardia, etc.).
Full reference table preserved as collapsible "View All Age Groups".
The WebView has its own permission layer separate from Android runtime
permissions. Both must be granted. Now when the web page requests mic
access, the WebChromeClient checks if Android permission exists, grants
the WebView request if yes, or requests Android permission first then
grants the pending WebView request in the callback.
- MainActivity: request RECORD_AUDIO permission at app start via
ActivityCompat (not WebChromeClient override which broke Capacitor bridge)
- Simplify launcher: remove server reachability check (was failing in
WebView), just save URL and navigate directly
- Remove biometric auth from launcher (Capacitor plugins need ES module
bundler, not available in plain HTML). Biometric can be added later
via the web app with proper Capacitor runtime.
- Add webContentsDebuggingEnabled for development
- MainActivity: override WebChromeClient to auto-grant WebView
permission requests (microphone, camera) so the Android runtime
permission dialog shows instead of WebView silently denying
- Add colors.xml with PedScribe blue (#2563eb / #1d4ed8)
- Update styles.xml: set statusBarColor and navigationBarColor to
match app theme (fixes brown/mismatched bar at top)
- Change base theme to NoActionBar (removes action bar)
WebView blocks no-cors fetch and image probes differently than browsers.
Simplified to a normal fetch that treats CORS errors as 'server reachable'
(CORS error = server responded, just blocked the origin).
- Fix AudioRecordingService ACTION_STOP to use com.pedshub.scribe
- Fix deprecated stopForeground(true) to STOP_FOREGROUND_REMOVE
- Fix launcher.js testServer: prevent double callback, fix onerror
always reporting success (now correctly fails on unreachable servers)
- Update service comment from TWA to Capacitor
New mobile/ directory with Capacitor project:
- Configurable server URL launcher (default: app.pedshub.com)
- Android: foreground service + wake lock for background recording
(AudioRecordingService preserved from existing TWA)
- iOS: background audio mode + microphone permission
- App ID: com.pedshub.scribe
- Both platforms initialized and synced
Existing android/ TWA project untouched — this is a separate project.
Build: cd mobile && npx cap open android (or ios)
New feature: after generating any clinical note, the app automatically
suggests relevant billing codes displayed as clickable chips below the output.
Backend (src/routes/billing.js):
- POST /api/suggest-codes endpoint analyzes note text
- Extracts diagnoses from Assessment section via regex
- Looks up ICD-10 codes: local common pediatric map (40+ conditions)
first, then NLM Clinical Tables API for unknown terms
- Suggests CPT E/M codes based on note type, visit complexity,
ROS/PE system counts, and MDM level estimation
- Supports: outpatient (new/established), well visit (age-based),
ED, inpatient (admit/subsequent/discharge)
Frontend (public/js/app.js):
- suggestBillingCodes() renders collapsible card with ICD-10 and CPT chips
- Click any chip to copy the code to clipboard
- Shows E/M level assessment (diagnosis count, ROS, PE, MDM complexity)
- Disclaimer: "Suggestions only. Always verify codes."
Integration: called after note generation in all 6 tabs
(encounter, SOAP, sick visit, well visit, hospital course, chart review)
Calculators tab with 7 tools:
- BP Percentile (AAP 2017) with age/sex/height classification
- BMI Percentile (CDC 2000) with extended obesity classification
(Class 1/2/3 using % of 95th percentile per CDC 2022)
- Growth Charts: weight-for-age, length-for-age, head circumference,
weight-for-length (WHO/CDC LMS), Fenton preterm (22-50 weeks)
- Bilirubin: AAP 2022 phototherapy threshold + Bhutani nomogram
with Nelson Table 137.1 risk factors for severe hyperbilirubinemia
- Vital Signs by Age (Harriet Lane) with quick reference formulas
(estimated weight, min SBP, ETT size, maintenance fluids 4-2-1)
- Body Surface Area (Mosteller formula)
- Weight-Based Dosing with max cap and volume calculation
Fix growth chart sub-tab navigation (pills scoped separately from
top-level nav to prevent panel disappearing)
- Fix: DELETE all other sessions query used empty string fallback when
req.sessionId was undefined, causing id != '' to match ALL rows
(including current session). Now skips deletion if sessionId unknown.
- Fix: Revoke All endpoint returns error if current session not identified
- Fix: var confirm shadowing window.confirm in password change handler
- 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)
- Add FAQ tab with accordion sections: Getting Started, AI & Models,
Voice & Transcription, Saving & Export, Privacy & Security,
Well Visit & Sick Visit, Learning Hub, Troubleshooting
- Documents how AI learns from physician edits (correction tracker)
- Fix FAQ accordion (CSP was blocking inline script, moved to app.js)
- Patch all 5 npm vulnerabilities: nodemailer 8.0.5, xmldom, basic-ftp,
path-to-regexp (npm audit now reports 0 vulnerabilities)
- Remove model category grouping from dropdown (flat list, no optgroups)
- Fix model dropdown dark background on options (white bg, dark text)
- Update FAQ model guidance to reflect admin-managed model selection
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
- New docs/developer-guide.md: full walkthrough of frontend SPA architecture,
backend middleware stack, database layer, AI integration, settings system,
how to add features/routes/tables, key design decisions, file references
- Expand ai-providers.md: detailed admin model management (add custom models
with ID/name/cost/category, discover from provider, enable/disable, set default)
- Update README docs index
- Add Cloudflare Turnstile to login, register, and password reset forms
- Switch AI provider to LiteLLM, transcription to OpenAI Whisper
- Change domain to scribe.pedshub.com
- Fix PPTX export: add tables, bold/italic, numbered lists, code blocks, blockquotes
- Fix announcement banner close button (CSP was blocking inline onclick)
- Fix auth middleware: empty Bearer token now falls through to cookie auth
- Fix audio backups: only save on transcription failure, stop auto-deleting on success
- Soften AI correction injection to prevent model hallucination from correction history
- Fix LiteLLM TTS model name handling (no incorrect openai/ prefix)
- Expand AI instructions textarea in Learning Hub CMS
- Update README for v6 with all features and providers
- Add comprehensive docs/: architecture, API reference, database schema,
authentication, AI providers, speech, learning hub, configuration, deployment
- 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
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