Three concurrent themes from this session:
═══════════════════════════════════════════════════════════════════
ED ENCOUNTERS — per-stage cards + consolidate→MDM finalize
═══════════════════════════════════════════════════════════════════
UX redesign per Daniel's feedback ("every stage note should be shown,
if AI is told to modify that particular note then the modified version
is used in final mdm"):
- Each generated stage stays on screen as its own editable card with
its own embedded "Don't Miss" panel. No more single rolling note
element that gets replaced on each generation.
- gatherCurrentNotes() reads contenteditable text from each stage card
before any operation (advance, finalize, persist) so inline edits
flow into the next AI call and the final consolidate.
- Stage badge is now state-accurate. "Stage N (recording)" with yellow
background after Add-more before generation; "Stage N" with gray
after generation. Fixes the bug where the badge flipped to Stage 2
the moment Add-more was clicked.
- Save & Done now runs TWO server-side AI calls in /finalize:
1. edConsolidate (new prompt) → polished single final note that
integrates every stage chronologically (HPI / ROS / PE / ED Course /
A&P with disposition).
2. edFinalize (rewritten with full inline 2023 AMA E/M element
rubric — problems / data / risk definitions, level mapping with
concrete examples) → MDM JSON.
- Two new cards render after finalize: blue-bordered Final Consolidated
Note + green-bordered MDM. Stage cards become read-only.
- partial_data on the saved row now stores {stages, finalNote, mdm,
finalized} so resume re-renders the full state.
Why two-call finalize: a single combined prompt makes the model cut
corners on one task. Two focused calls cost ~2× latency at the very end
of an encounter — acceptable since finalize is a one-time terminal
action, not a per-stage hot path.
Files: public/components/ed-encounter.html, public/js/ed-encounters.js,
src/routes/edEncounters.js, src/utils/prompts.js (edConsolidate added,
edFinalize rewritten).
═══════════════════════════════════════════════════════════════════
EXTENSIONS / PAGERS — visual polish
═══════════════════════════════════════════════════════════════════
Multiple iterations based on Daniel's feedback:
- Layout: align-items:flex-start so action buttons stay pinned top-right
when long numbers wrap (was align-items:center → buttons drifted into
the text area, causing visible overlap).
- Number: word-break:break-all + min-width:0 + font-feature-settings:tnum
so long numbers wrap within their column instead of pushing under the
buttons. Click-to-copy with a 0.55s green flash + ✓ copied badge.
- Phone/pager Font Awesome icon next to the number in the type color —
at-a-glance type signal (replacing an earlier 3px left stripe that
Daniel found visually bulky).
- Name: font-weight 700, font-size 14.5px, color g900, letter-spacing
-0.012em — scan-target headline typography for long lists.
- Alternating subtle backgrounds by index (white vs #fafbfc) so a long
list reads as distinct rows.
- Hover: card lifts 1px with a soft shadow; action buttons fade from
55% to 100% opacity. Cubic-bezier transition on transform.
- Entrance: staggered fade-up animation per card (35ms × index, capped
at 12). prefers-reduced-motion media query disables motion.
- Empty state: 48px FA icon + heading instead of plain gray text.
Files: public/js/extensions.js, public/css/styles.css.
═══════════════════════════════════════════════════════════════════
DOCS REORGANIZATION + APPLICATION-LOGIC DOCS + ADMIN VIEWER
═══════════════════════════════════════════════════════════════════
Document moves (preserving git history via git mv):
BROWSER_WHISPER_SETUP.md → docs/browser-whisper-setup.md
BROWSER_WHISPER_TROUBLESHOOTING.md → docs/browser-whisper-troubleshooting.md
DEVELOPER_GUIDE.md → docs/developer-guide-extended.md
EMBEDDINGS_SETUP.md → docs/embeddings-setup.md
FEATURES_EXPLAINED.md → docs/features-explained.md
IMPROVEMENTS.md → docs/improvements.md
OPENID_SETUP.md → docs/openid-setup.md
TRANSCRIPTION_OPTIONS.md → docs/transcription-options.md
README.md updated with the new paths + a Documentation section that
links to docs/logic/ at the top.
New application-logic doc series (~8,300 lines total) at docs/logic/.
Built with 5 parallel doc-writing agents per Daniel's "use multiple
agents" directive. Each doc explains how a part of the app actually
works — application logic, data flow, design decisions, sacred zones,
how-to-extend recipes — at a depth that lets a new dev (or an AI
assistant) modify the code confidently.
docs/logic/README.md — index + recommended reading order
docs/logic/architecture.md (2166 L) — frontend IIFE pattern, lazy tab
load, backend route convention,
schema, encryption, deployment
docs/logic/clinical-notes.md (1546L) — every note tab + helper trio
docs/logic/bedside-and-calculators.md (1373L) — bedside ES module
pocket + calculators + PE Guide
+ suture selector
docs/logic/auth-admin-learning.md (1281L) — auth (local+OIDC+2FA) +
admin panel + Learning Hub
(Quiz engine logic at sub-detail
only — TODO follow-up)
docs/logic/ai-and-voice.md (1128 L) — callAI 5-provider routing,
prompts, voice/STT, helper trio
docs/logic/ed-encounters.md (821 L) — multi-stage ED + MDM (this
session's worked example)
Admin-only docs viewer:
- New route /api/admin/docs/{tree,file}: recursively walks docs/, returns
the tree as JSON; /file?path=X validates path stays inside docs/ and
renders markdown via marked. Both gated by req.user.role==='admin'.
- New tab "Docs" (book icon) in the sidebar, hidden by default and
revealed in auth.js when user.role==='admin' (same pattern as the
existing Admin and CMS tabs).
- New component public/components/admin-docs.html: split-pane layout
with a tree sidebar + filter input + a markdown reader pane.
- New module public/js/admin-docs.js: lazy-loads the tree on first tab
activation, renders collapsible folders, persists expanded state and
last-opened path via UIState. Server-rendered HTML so no client
markdown parser needed.
- CSS for the viewer (responsive split-pane, code-block styling, table
scrolling, etc.).
- Mounted at /api/admin/docs (NOT /api) — important: mounting a router
with router.use(authMiddleware) at /api accidentally 401s every other
/api/* path (caught and fixed during testing — /api/health was 401'ing).
Files: docs/* (moved + new), README.md, public/components/admin-docs.html
(new), public/js/admin-docs.js (new), src/routes/adminDocs.js (new),
public/index.html (tab + section + script), public/js/auth.js (admin
gate + logout cleanup), public/css/styles.css (viewer styles), server.js
(mount).
═══════════════════════════════════════════════════════════════════
KNOWN GAPS (TODO follow-ups)
═══════════════════════════════════════════════════════════════════
- Learning Hub quiz engine (MCQ / multi-select / T-F scoring + attempt
tracking + progress dashboard) is covered at the architectural level
in docs/logic/auth-admin-learning.md but not drilled into the quiz
data model and scoring flow. Worth a focused follow-up doc.
- ED finalize: if MDM step JSON parse fails, server returns 502 with
the consolidated finalNote in the error payload, but client doesn't
surface the partial result. Add a "MDM failed, retry" affordance.
- No e2e Playwright coverage for ED encounters or the new docs viewer.
9.7 KiB
OpenID Connect (OIDC) / PocketID Setup Guide
This guide explains how to configure Single Sign-On (SSO) authentication using OpenID Connect providers like PocketID, Keycloak, Azure AD, Okta, or Google.
Overview
The application supports OIDC authentication alongside traditional email/password login. Once configured, users can:
- Sign in with their SSO provider (e.g., PocketID)
- Automatically link existing email accounts to their SSO identity
- Admins can optionally disable local password login entirely
Prerequisites
- An OpenID Connect provider (e.g., PocketID instance)
- Admin access to this application
- The public URL where your app is deployed (
APP_URLin.env)
Configuration Steps
1. Configure Your Identity Provider
First, register this application with your OIDC provider. You'll need:
Redirect URI / Callback URL:
https://your-domain.com/api/auth/oidc/callback
Replace your-domain.com with your actual APP_URL value.
Example: PocketID Setup
- Log into your PocketID admin panel
- Navigate to Applications → Add Application
- Set the callback URL:
https://your-domain.com/api/auth/oidc/callback - Copy the Client ID and Client Secret
Example: Keycloak Setup
- Create a new client in your Keycloak realm
- Set Access Type to
confidential - Add Valid Redirect URI:
https://your-domain.com/api/auth/oidc/callback - Save and note the Client ID and Client Secret from the Credentials tab
2. Enable OIDC in Application Settings
Log into your application as an admin user, then:
- Navigate to Admin Panel → Settings (or access
/admin-settings.html) - Look for the OpenID Connect (SSO) section
- Fill in the following fields:
| Field | Description | Example |
|---|---|---|
| Enabled | Toggle to enable OIDC | true |
| Issuer URL | Your provider's discovery endpoint | https://id.example.com or https://keycloak.example.com/realms/myrealm |
| Client ID | Application client ID from your provider | pediatric-scribe-client |
| Client Secret | Application client secret (keep confidential) | a1b2c3d4... |
| Button Label | Text shown on the SSO login button | Sign in with PocketID |
| Disable Local Auth | Hide email/password login (optional) | false (keep disabled initially) |
| Allowed IPs | Restrict SSO to specific IP ranges (optional) | Leave blank for no restriction |
- Click Save Settings
3. Test SSO Login
- Log out or open an incognito browser window
- Visit the login page
- You should see a new button: "Sign in with [Your Provider]"
- Click it and authenticate with your SSO provider
- You'll be redirected back to the application and logged in
Linking Existing Users to SSO
When a user signs in via OIDC for the first time, the system automatically links their account based on email address matching:
Scenario 1: Existing User with Matching Email
If a user already has an account with email doctor@example.com and signs in via SSO with the same email:
- The system finds the existing user by email
- Links the SSO identity (
oidc_sub) to the existing account - The user is logged in
- Future logins can use either method (email/password OR SSO)
Database update performed:
UPDATE users
SET oidc_sub = '<provider-unique-id>',
email_verified = true
WHERE email = 'doctor@example.com';
Scenario 2: New User (No Matching Email)
If the SSO email doesn't match any existing user:
- A new account is automatically created
- The user is assigned the
userrole (first user becomesadmin) - A random password is generated (not used for SSO logins)
- The user is logged in
Scenario 3: Disabled User
If an existing user is disabled (disabled = true in database):
- SSO login is blocked
- User sees an error message
- Admin must re-enable the account from the Admin Panel
Manual Account Linking (CLI)
If you need to manually link an existing user to an SSO identity, use the PostgreSQL database directly:
# Connect to database
docker exec -it pediatric-ai-scribe-postgres psql -U pedscribe -d pedscribe
# Link user by setting their oidc_sub
UPDATE users
SET oidc_sub = 'provider-sub-12345',
email_verified = true
WHERE email = 'doctor@example.com';
Finding the oidc_sub value:
The oidc_sub is the unique identifier from your OIDC provider (usually a UUID or numeric ID). To find it:
- Have the user attempt SSO login once
- Check the application logs for their
subclaim:[OIDC] User logged in: sub=abc-123-def, email=doctor@example.com - Use that
subvalue in the UPDATE statement
Security Considerations
HTTPS Required in Production
OIDC requires HTTPS for security. Ensure your APP_URL uses https://:
APP_URL=https://scribe.example.com
Client Secret Protection
The client secret is stored encrypted in the database. The admin UI masks it after saving (shows ••••••••1234).
Never commit the client secret to Git or share it publicly.
IP Allowlisting (Optional)
To restrict SSO to specific networks (e.g., hospital VPN):
- Set Allowed IPs in admin settings to comma-separated CIDR ranges:
10.0.0.0/8, 192.168.1.0/24 - Users outside these ranges will see an error when attempting SSO
Disable Local Password Login
Once SSO is working, you can optionally disable traditional email/password login:
- In Admin Settings, enable Disable Local Auth
- The login page will only show the SSO button
- Admins can still use the CLI to reset passwords if needed
Warning: Only disable local auth after confirming all users can access SSO. Keep one admin password as backup.
Troubleshooting
"SSO is not enabled" error
- Verify Enabled is set to
truein admin settings - Check application logs for OIDC configuration errors
"Invalid state" or "Expired" error
- The OIDC flow timed out (5 minute window)
- Try logging in again
- If persistent, check server time synchronization
"No email claim" error
Your OIDC provider didn't return an email address. Ensure:
- The
emailscope is requested (default:openid email profile) - Your provider is configured to release email claims
- The user's account has an email address set
Email Mismatch
If a user has different emails in the app vs. SSO provider:
Option 1: Update app email to match SSO
UPDATE users SET email = 'new-email@example.com' WHERE id = 123;
Option 2: Update SSO provider email to match app (Provider-specific — consult your IdP documentation)
Callback URL Not Working
Double-check the redirect URI in your OIDC provider settings matches exactly:
https://your-domain.com/api/auth/oidc/callback
Common mistakes:
- Missing
https:// - Trailing slash (don't include it)
- Wrong domain (must match
APP_URLin.env)
Provider-Specific Examples
PocketID
Issuer URL: https://id.pockethost.io
Client ID: (from PocketID app settings)
Client Secret: (from PocketID app settings)
Redirect URI: https://your-domain.com/api/auth/oidc/callback
Keycloak
Issuer URL: https://keycloak.example.com/realms/medical
Client ID: pediatric-scribe
Client Secret: (from Credentials tab)
Redirect URI: https://your-domain.com/api/auth/oidc/callback
Azure AD / Entra ID
Issuer URL: https://login.microsoftonline.com/{tenant-id}/v2.0
Client ID: (Application ID from Azure)
Client Secret: (from Certificates & secrets)
Redirect URI: https://your-domain.com/api/auth/oidc/callback
Note: Azure requires app registration in Azure Portal first.
Okta
Issuer URL: https://{your-okta-domain}.okta.com
Client ID: (from Okta application settings)
Client Secret: (from Okta application settings)
Redirect URI: https://your-domain.com/api/auth/oidc/callback
Google (Workspace or Gmail)
Issuer URL: https://accounts.google.com
Client ID: (from Google Cloud Console)
Client Secret: (from Google Cloud Console)
Redirect URI: https://your-domain.com/api/auth/oidc/callback
Note: Google requires OAuth consent screen configuration.
Environment Variables (Alternative to UI Config)
For deployment automation, you can set OIDC config via environment variables instead of the admin UI:
# .env file
OIDC_ENABLED=true
OIDC_ISSUER=https://id.example.com
OIDC_CLIENT_ID=my-client-id
OIDC_CLIENT_SECRET=my-client-secret
OIDC_BUTTON_LABEL=Sign in with PocketID
OIDC_DISABLE_LOCAL_AUTH=false
Note: UI settings take precedence over environment variables. If set in both places, the database values are used.
HIPAA Compliance Notes
OIDC does not transmit PHI to the identity provider. Only authentication-related data (email, name) is exchanged.
For HIPAA compliance:
- Ensure your OIDC provider has appropriate safeguards
- Use a self-hosted provider (Keycloak, PocketID) within your secure network
- Or use a HIPAA-compliant SaaS provider with a BAA
- Enable audit logging for all SSO login events (automatically logged in
audit_logtable)
Audit Logging
All SSO login events are logged in the audit_log table:
SELECT * FROM audit_log WHERE action = 'login_oidc' ORDER BY created_at DESC;
Logged fields:
- User ID
- Action:
login_oidc - IP address
- Details: Issuer URL
- Timestamp
Support
For issues specific to:
- This application: Check application logs with
docker logs pediatric-ai-scribe - Your OIDC provider: Consult provider documentation (PocketID, Keycloak, Azure, etc.)
- Network/TLS issues: Verify
APP_URLmatches your reverse proxy configuration
Common log locations:
# Application logs
docker logs pediatric-ai-scribe
# PostgreSQL logs
docker logs pediatric-ai-scribe-postgres