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 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)").
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".
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)
- 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
- 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
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
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
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
- 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
- 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
- Add local Whisper (whisper.cpp / faster-whisper) as transcription provider
Set TRANSCRIBE_PROVIDER=local with configurable model size and binary path
- Upgrade all refine/instruction inputs to resizable textareas across
encounter, dictation, hospital course, chart review, well visit, sick visit
- Make AI memory injection flexible: physician preferences and corrections
are now actively applied (not just "formatting reference"), while still
overridable by current prompt instructions
- Add type="button" to all CMS action buttons to prevent default submit
- Change overflow:hidden to overflow:visible on .cms-main to prevent clipping
- Add z-index to .cms-editor-actions for reliable click targeting
- Replace AI "Topic" text input with a descriptive textarea context box
so users can tell AI what to generate (AI creates its own title)
- Rename AI tab from "By Topic" to "Describe Content"
- New GET /api/learning/content/:slug/slides (authMiddleware only)
Renders Marp markdown via marp-core, returns {css, slides[]} for
any authenticated user — no moderator access required
- showViewer: when content_type === 'presentation', hides the body card
and shows a presentation card with title, slide count, and View Slides button
Auto-opens the slide modal immediately on load; button allows re-opening
- openSlidesFromSlug(): fetches slides from user-accessible endpoint
then opens the existing slide preview modal (arrow keys, swipe, dots)
- Viewer already correctly hides feed/categories/search when viewing content;
"Back to Feed" button returns to the main list
pdf-parse (was v2, broken API):
- Downgraded to v1.1.1 — default export is a function again
- pdfParse is not a function error fixed
WebDAV file selection (style.display bug, same cascade issue):
- lh-ai-webdav-selected had inline style="display:flex" always visible
- selectWebdavFile() / deselectWebdavFile() now use element.style.display
- On select: browser div hides, selected indicator shows with file name + X
- On deselect (X): indicator hides, browser shows again
Topic context for Upload and Nextcloud tabs:
- Both tabs now have optional "Topic / context" field
- Sent to backend as 'topic' param to help AI focus the generated content
Inline refine bar (replaces window.prompt):
- Refine Body button shows/hides an amber inline input bar below generate buttons
- Enter instructions, press Apply — no browser dialog, works in all contexts
CSP: remove unsafe-inline from scriptSrc (security issue #2):
- Converted all 26 onclick= handlers in component HTML files to
data-action / data-target / data-label attributes
- Added delegated click handler in app.js for copy/speak/nc-export actions
- script-src now 'self' only; script-src-attr 'none'
WebDAV path endpoint (security issue #5):
- New POST /api/user/webdav-path (authMiddleware only, not moderator)
- nextcloud.js updated to use new endpoint
nodemailer: already at 6.10.1 (vulnerability fixed).
Bug: classList.toggle('hidden') was silently ignored because
.lh-ai-opt-field{display:flex} is defined at CSS line 758, after
.hidden{display:none} at line 242 — same specificity, later rule wins.
Fix: updateAiOptions() now uses element.style.display directly (inline
styles always override class rules).
Also fix initial state: lh-ai-slides-field now uses style="display:none"
instead of class="hidden" so it's correctly hidden on load.
Panel syncs content type from editor on open (editorType → ctype select
→ updateAiOptions called immediately).
Per type:
- Quiz: word count hidden, slides hidden, quiz card visible (no toggle)
- Presentation: word count hidden, slides visible, quiz card hidden
- Article/Pearl: word count visible, slides hidden, quiz card with toggle
Quiz card redesign: gradient blue/indigo background, white header with
icon, clean body — replaces the plain grey rectangle.
AI Generate panel:
- Content type synced from editor on open
- Article/Pearl: optional word count field
- Presentation: optional slide count field
- Questions row: checkbox toggle for article/pearl (optional), always on for quiz,
hidden for presentation; count input shown only when enabled
- Backend: passes wordCount/slideCount to prompt; questionCount=0 skips questions
Delete confirmation:
- Wording built dynamically from content type
- Presentation: no mention of questions
- Quiz: "All quiz questions will be permanently removed"
- Article/pearl: "Any quiz questions attached will also be removed"
Auth refactor — JWT → httpOnly cookie:
- Backend: setAuthCookie() sets ped_auth as httpOnly, Secure, SameSite=Lax, 7d
- Backend: clearAuthCookie() on POST /api/auth/logout (new endpoint)
- Backend: auth middleware reads ped_auth cookie first, falls back to Bearer header
- Login/register: no longer return token in JSON body; cookie is the session
- Frontend: getAuthHeaders() returns only Content-Type (no Authorization header)
- Frontend: session check uses /api/auth/me via cookie (no localStorage token)
- Frontend: clearSession() calls /api/auth/logout to clear cookie server-side
- Frontend: no token in localStorage; window.AUTH_TOKEN removed
- All FormData fetches use credentials:'same-origin' (cookie sent automatically)
- index.html: optimistic auth-screen hide (removed localStorage token check)
Tested: login, logout, /api/auth/me, admin routes, FormData uploads, cookie httpOnly
flag, 401 without cookie, cookie cleared on logout.
Delete bar bug:
- Root cause: .lh-delete-confirm-bar{display:flex} defined after .hidden{display:none}
so flex overrode hidden (no !important on global .hidden rule)
- Fix: remove display:flex from base rule, use .lh-delete-confirm-bar:not(.hidden){display:flex}
so hidden class always wins regardless of CSS order
Slide preview (replace window.open + document.write approach):
- Backend /preview-slides now returns JSON: {css, slides[]} (individual <section> HTML)
- In-page full-screen modal with dark overlay, white slide frame
- Prev/Next arrow buttons, dot indicators, slide counter
- Keyboard navigation (arrow keys, Escape to close)
- Touch/swipe support for mobile
- Overlay click closes modal
- No popups required, works in all browsers
Marp textarea:
- Placeholder updated to guide users: "Click AI Generate above to create slides"
- Makes it clear AI is the primary authoring path; textarea for review/edits
Delete confirmation:
- Replaced browser confirm() popup with inline red warning bar in editor
- Shows content title, Cancel and Delete buttons inline in the editor header
- Category delete uses double-click pattern with toast feedback
Login screen:
- Changed from dark blue/purple gradient to light gray background (var(--g50))
- Card now matches 404 page style: white, subtle blue shadow, indigo border
Presentation content type:
- New 'Presentation' button in CMS toolbar (green style)
- Marp markdown textarea editor (dark code editor style) shown when type=presentation
- Body/Tiptap editor hidden; Marp section shown (toggleEditorMode)
- Preview button: renders Marp HTML via @marp-team/marp-core, opens in new tab
- Download PPTX: pure-JS pptxgenjs (no Chromium), parses slides, exports .pptx
- AI generate: returns Marp markdown directly when contentType=presentation
- Body column stores Marp markdown for presentations
Backend:
- POST /api/admin/learning/generate-pptx — pptxgenjs PPTX from Marp markdown
- POST /api/admin/learning/preview-slides — Marp HTML preview
GitHub repo set to private via API.
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
- Replace textarea+toolbar with Quill.js (free, MIT) for body editor
- Full WYSIWYG: headings, bold/italic, lists, blockquote, code, link (inline, no popup)
- Rich Text toggle button on each question text field and option text field
- Plain textarea by default; toggle activates mini Quill editor per field
- New 'Multiple Select' question type: checkboxes, all correct must be chosen
- Backend scoring: allCorrectChosen && noWrongChosen
- Results show all correct options highlighted, wrong selections flagged
- quiz display uses sanitizeHtml for question/option text (supports formatted HTML)
- sanitizeHtml allows Quill class attrs (ql-indent, etc.)
- 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