pediatric-ai-scribe-v3/DEVELOPER_GUIDE.md
Daniel Onyejesi a6fd59fab4 Update DEVELOPER_GUIDE: PDF/embedding rationale, scalability, security notes
- Section 17: PDF uploads — why no vector embeddings needed (single-file
  generation uses full-text extraction which is better for this use case;
  explains when embeddings would be appropriate and how to increase truncation limit)
- Section 18: Scalability — what already scales, bottlenecks (rate limiting,
  file uploads), horizontal scaling pattern with Redis + nginx, cloud options
- Section 19: Security — localStorage vs httpOnly cookie decision documented
  with rationale; existing XSS protections listed; path to httpOnly if needed
- Section 15: Version history updated and moved below new sections
- Version bumped to 3.19, current Docker tag updated
2026-03-24 02:57:04 -04:00

38 KiB

Pediatric AI Scribe — Developer Guide

Version: 3.19 | Stack: Node.js / Express / PostgreSQL / Vanilla JS


Table of Contents

  1. Project Overview
  2. Architecture
  3. Directory Structure
  4. Environment Variables
  5. Database Schema
  6. Authentication System
  7. Backend API Reference
  8. Frontend Architecture
  9. AI Integration
  10. Learning Hub & CMS
  11. Deployment
  12. Known Issues & Security Notes
  13. Adding New Features
  14. Resetting Admin Password via Console

1. Project Overview

Pediatric AI Scribe is a clinical documentation platform for pediatric healthcare providers. It uses AI (via OpenRouter, AWS Bedrock, or Azure OpenAI) to generate:

  • HPI notes from live encounter recordings
  • SOAP notes from dictation
  • Hospital course summaries
  • Chart reviews
  • Well-visit notes (including SSHADESS, ROS/PE, milestones)
  • Sick visit notes
  • Learning Hub content (articles, quizzes, clinical pearls, presentations)

Key design principle: Single-page application. All tabs are lazy-loaded HTML components (/public/components/*.html). JavaScript modules initialize only when their tab is first activated via the tabChanged custom event.


2. Architecture

Browser (Vanilla JS + Tiptap)
       |
       | HTTP (JWT Bearer token in Authorization header)
       |
Express.js (Node.js) — server.js
       |
       |— Helmet (CSP, security headers)
       |— CORS (restricted to APP_URL in production)
       |— express-rate-limit (login: 5/hr, register: 5/hr, general: 100/15min)
       |— cookie-parser
       |— Routes (/src/routes/)
       |
PostgreSQL (pg driver, no ORM)
       |
       |— users, app_settings, audit_log, saved_encounters
       |— user_memories, learning_*, access_log, api_log

AI providers (configured via environment variables, in priority order):

  1. AWS Bedrock (if AWS_BEDROCK_REGION is set)
  2. Azure OpenAI (if AZURE_OPENAI_ENDPOINT is set)
  3. OpenRouter (default, requires OPENROUTER_API_KEY)

3. Directory Structure

/
├── server.js                   # Express app entry point, route registration, CSP
├── package.json
├── Dockerfile
├── docker-compose.yml          # Production compose
├── docker-compose.local.yml    # Local development (port 3552)
├── DEVELOPER_GUIDE.md          # This file
│
├── src/
│   ├── db/
│   │   └── database.js         # DB connection pool, schema init, migrations
│   ├── middleware/
│   │   ├── auth.js             # authMiddleware, adminMiddleware, moderatorMiddleware
│   │   └── logging.js          # Access log middleware
│   ├── routes/
│   │   ├── auth.js             # Login, register, 2FA, password reset, /me
│   │   ├── admin.js            # User management (admin only)
│   │   ├── adminConfig.js      # Site settings, feature flags, AI prompts, models
│   │   ├── encounters.js       # Save/load/delete draft encounters
│   │   ├── memories.js         # User templates (physical exam, ROS, etc.)
│   │   ├── hpi.js              # Generate HPI from encounter/dictation transcript
│   │   ├── soap.js             # Generate SOAP note
│   │   ├── hospitalCourse.js   # Generate hospital course summary
│   │   ├── chartReview.js      # Generate outpatient chart review
│   │   ├── milestones.js       # Generate developmental milestone narrative
│   │   ├── wellVisit.js        # Well-visit note generation (ROS/PE/ICD-10)
│   │   ├── sickVisit.js        # Sick visit note generation
│   │   ├── refine.js           # Refine/shorten any generated document
│   │   ├── transcribe.js       # Whisper audio transcription
│   │   ├── tts.js              # Text-to-speech (if configured)
│   │   ├── nextcloud.js        # Nextcloud WebDAV connect/export/disconnect
│   │   ├── learningHub.js      # User-facing: feed, content, quiz submission
│   │   ├── learningAdmin.js    # CMS: categories, content, questions CRUD
│   │   ├── learningAI.js       # AI generation for Learning Hub content
│   │   └── logs.js             # Usage/audit/API/access logs + client error
│   └── utils/
│       ├── ai.js               # callAI() — multi-provider AI client
│       ├── models.js           # Model ID lists, defaults, Bedrock ID mapping
│       ├── prompts.js          # Prompt templates, DB override loader
│       ├── config.js           # App configuration helpers
│       └── logger.js           # Winston logger (file + console)
│
├── public/
│   ├── index.html              # Single HTML shell, loads all components
│   ├── 404.html                # Custom 404 page
│   ├── css/
│   │   └── styles.css          # All CSS (single file, ~750 lines)
│   ├── js/
│   │   ├── app.js              # Core: tab switching, loadComponent, global helpers
│   │   ├── auth.js             # Login/register UI, JWT localStorage, getAuthHeaders()
│   │   ├── admin.js            # Admin panel UI
│   │   ├── liveEncounter.js    # Live recording tab
│   │   ├── voiceDictation.js   # Dictation tab
│   │   ├── hospitalCourse.js   # Hospital course tab
│   │   ├── chartReview.js      # Chart review tab
│   │   ├── soap.js             # SOAP tab
│   │   ├── milestones.js       # Milestones tab (inside Well Visit)
│   │   ├── wellVisit.js        # Well Visit guide + vaccine schedule
│   │   ├── shadess.js          # SSHADESS form + Well Visit note generation
│   │   ├── sickVisit.js        # Sick Visit tab
│   │   ├── nextcloud.js        # Nextcloud settings UI
│   │   ├── encounters.js       # Save/load encounter UI (all tabs)
│   │   ├── memories.js         # User templates UI (Settings)
│   │   ├── learningHub.js      # Learning Hub + CMS (entire module, ~1400 lines)
│   │   ├── milestonesData.js   # Static milestone data by age group
│   │   └── pediatricScheduleData.js  # Vaccine schedule data
│   ├── components/             # Lazy-loaded tab HTML (injected by loadComponent)
│   │   ├── encounter.html      ├── dictation.html   ├── hospital.html
│   │   ├── chart.html          ├── soap.html        ├── wellvisit.html
│   │   ├── sickvisit.html      ├── vaxschedule.html ├── catchup.html
│   │   ├── learning.html       ├── cms.html         ├── admin.html
│   │   └── settings.html
│   └── vendor/
│       └── tiptap.bundle.js    # Tiptap 2 + extensions (esbuild bundle, self-hosted)

4. Environment Variables

Set in .env file (copy .env.example to get started):

# ── Required ──────────────────────────────────────────────────
DATABASE_URL=postgresql://user:<password>@host:5432/dbname
JWT_SECRET=change-this-to-a-random-64-char-string

# ── AI Provider (choose one or let it default to OpenRouter) ──
OPENROUTER_API_KEY=sk-or-...         # Default provider
# OR
AZURE_OPENAI_ENDPOINT=https://...    # Azure (HIPAA eligible)
AZURE_OPENAI_API_KEY=...
AZURE_DEPLOYMENT_NAME=gpt-4o-mini
AZURE_OPENAI_API_VERSION=2024-08-01-preview
# OR
AWS_BEDROCK_REGION=us-east-1         # AWS Bedrock (HIPAA eligible)
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...

# ── Optional ──────────────────────────────────────────────────
OPENAI_API_KEY=sk-...                # For Whisper transcription only
APP_URL=https://yourdomain.com       # Enables secure CORS + Secure cookies
NODE_ENV=production                  # Enables production optimizations
PORT=3000                            # Default: 3000

# ── Email (for password reset, registration verification) ──────
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=noreply@example.com
SMTP_PASS=...
SMTP_FROM=Pediatric AI Scribe <noreply@example.com>

Note: If no SMTP is configured, registration auto-verifies and password reset won't work. Configure SMTP or use the console reset method (see Section 14).


5. Database Schema

All tables are created automatically on first run by src/db/database.js. The file runs CREATE TABLE IF NOT EXISTS for every table, followed by ALTER TABLE ... ADD COLUMN IF NOT EXISTS migrations for upgrades.

Core Tables

users

Column Type Notes
id SERIAL PK
email TEXT UNIQUE Lowercase
password TEXT bcrypt hash (cost 12)
name TEXT Display name
role TEXT 'user' | 'moderator' | 'admin'
totp_enabled BOOLEAN 2FA status
totp_secret TEXT TOTP secret (base32)
disabled BOOLEAN Soft disable
email_verified BOOLEAN
verify_token / verify_expires TEXT / BIGINT Email verification
reset_token / reset_expires TEXT / BIGINT Password reset
nextcloud_url / nextcloud_user / nextcloud_token / nextcloud_folder TEXT Nextcloud integration
webdav_learning_path TEXT Default WebDAV path for Learning Hub file picker
created_at TIMESTAMPTZ

app_settings

Key-value store for all site configuration. Read via db.getSetting(key), written via admin panel or direct DB.

Important keys:

  • registration_enabled'true' / 'false'
  • announcement.enabled / announcement.text / announcement.type
  • smtp.* — SMTP config (overrides env vars)
  • ai.prompt.* — AI prompt overrides
  • model.* — enabled/disabled models

saved_encounters

Draft encounters (7-day auto-expiry). Columns: label, enc_type, transcript, generated_note, partial_data (JSON), status, expires_at.

user_memories

User templates fed into AI generation. category is one of: physical_exam, ros, encounter_format, family_history, assessment_plan, custom.

Learning Hub Tables

learning_categories

Simple category list with name, slug, sort_order.

learning_content

Articles, quizzes, pearls, presentations. Key columns: title, slug, body (HTML for articles/pearls/quizzes; Marp markdown for presentations), content_type (article | quiz | pearl | presentation), published, author_id.

learning_questions

Quiz questions linked to learning_content. question_type: mcq | true_false | multi. explanation = general explanation shown after answering.

learning_options

Answer options for quiz questions. is_correct: boolean, explanation = shown when this wrong option is chosen.

learning_progress

Quiz attempt scores per user per content item.


6. Authentication System

Current implementation: JWT in localStorage

Flow

  1. POST /api/auth/login → returns { success, token, user }
  2. Frontend stores token in localStorage as ped_scribe_token and in window.AUTH_TOKEN
  3. All API calls include Authorization: Bearer <token> header via getAuthHeaders()
  4. src/middleware/auth.js validates the Bearer token, attaches req.user
  5. Logout: clearSession() removes token from localStorage (client-side only)

Token

  • Signed with JWT_SECRET env var
  • 7-day expiry
  • Payload: { userId: number }

Roles

  • user — standard access (clinical tools only)
  • moderator — can create/edit Learning Hub content
  • admin — full access including user management and site settings

Middleware

  • authMiddleware — validates JWT, populates req.user
  • adminMiddleware — run after auth, requires role === 'admin'
  • moderatorMiddleware — run after auth, requires role === 'admin' OR 'moderator'

2FA

Uses TOTP (speakeasy). If enabled, login returns { requires2FA: true } and the client must POST the TOTP code to complete login.

Session Check on Page Load (auth.js)

var savedToken = localStorage.getItem('ped_scribe_token');
if (savedToken) {
  fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + savedToken } })
  .then(/* if ok → enterApp(), else → clearSession() */);
}

The has-session CSS class on <html> hides the auth screen immediately when a localStorage token exists, preventing a white flash.


7. Backend API Reference

All routes are prefixed /api. Routes requiring auth are marked (A). Admin-only: (ADM). Moderator+: (MOD).

Auth — /api/auth/

Method Path Auth Description
POST /login Email + password login. Returns { token, user }
POST /register Create account (checks registration_enabled setting)
GET /me A Returns current user object
POST /logout Clears server-side state (currently no-op, kept for future)
POST /setup-2fa A Generates TOTP secret + QR code
POST /verify-2fa A Confirms TOTP code, enables 2FA
POST /disable-2fa A Disables 2FA (requires password)
POST /forgot-password Sends reset email
POST /reset-password Sets new password via reset token
GET /registration-status Returns { registrationEnabled: bool }
GET /verify-email Verifies email via token in query string

Clinical — AI Generation

Method Path Auth Description
POST /generate-hpi-encounter A HPI from live encounter transcript
POST /generate-hpi-dictation A HPI from dictation
POST /generate-soap A SOAP note
POST /generate-hospital-course A Hospital course summary
POST /generate-chart-review A Chart review
POST /generate-milestone-narrative A Milestone narrative
POST /generate-milestone-summary A 3-sentence milestone summary
POST /well-visit/note A Full well-visit note
POST /sick-visit/note A Sick visit SOAP
POST /transcribe A Whisper audio → text (multipart/form-data, field: audio)
POST /refine A Refine existing document
POST /shorten A Shorten existing document
POST /clarify A Find missing info in a document

Encounters (Save/Load)

Method Path Auth Description
GET /encounters A List user's saved encounters
POST /encounters A Save/update encounter draft
DELETE /encounters/:id A Delete a draft

User Templates

Method Path Auth Description
GET /memories A List user's templates
POST /memories A Create template
PUT /memories/:id A Update template
DELETE /memories/:id A Delete template
GET /memories/context A Returns templates formatted for AI injection

Nextcloud

Method Path Auth Description
POST /nextcloud/connect A Connect + test Nextcloud credentials
POST /nextcloud/export A Export text file to Nextcloud
POST /nextcloud/disconnect A Remove Nextcloud credentials

Learning Hub (User-Facing)

Method Path Auth Description
GET /learning/categories A List categories
GET /learning/feed A Paginated published content
GET /learning/category/:slug A Content by category
GET /learning/content/:slug A Single content item + questions
POST /learning/submit-quiz A Submit quiz answers, returns scored results
GET /learning/search A Full-text search

Learning Hub CMS (Moderator+)

Method Path Auth Description
GET /admin/learning/categories MOD All categories with counts
POST /admin/learning/categories MOD Create category
PUT /admin/learning/categories/:id MOD Update category
DELETE /admin/learning/categories/:id MOD Delete category
GET /admin/learning/content MOD All content (including drafts)
GET /admin/learning/content/:id MOD Single item with questions
POST /admin/learning/content MOD Create content
PUT /admin/learning/content/:id MOD Update content
DELETE /admin/learning/content/:id MOD Delete content + questions
POST /admin/learning/content/:id/questions MOD Add question to content
PUT /admin/learning/questions/:id MOD Update question + options
DELETE /admin/learning/questions/:id MOD Delete question
GET /admin/learning/stats MOD Dashboard stats

Learning Hub AI (Moderator+)

Method Path Auth Description
POST /admin/learning/ai-generate MOD Generate content from topic/file/Nextcloud (multipart/form-data)
POST /admin/learning/ai-refine MOD Refine body HTML with instructions
POST /admin/learning/preview-slides MOD Render Marp markdown → { css, slides[] } for preview
POST /admin/learning/generate-pptx MOD Marp markdown → .pptx download (pptxgenjs)
GET /admin/learning/webdav-browse MOD PROPFIND Nextcloud folder
POST /admin/learning/webdav-path MOD Save user's default WebDAV path

Admin (Admin Only)

Method Path Auth Description
GET /admin/users ADM List all users
POST /admin/users ADM Create user
PUT /admin/users/:id ADM Update user (role, disable)
DELETE /admin/users/:id ADM Delete user
GET/POST /admin/config/* ADM Site settings (announcement, SMTP, models, prompts, etc.)

Logs & Health

Method Path Auth Description
GET /health Returns { status: 'running', version, provider }
GET /models Returns available AI models list
POST /logs/client-error Client-side error logging (public)
GET /logs/usage ADM API usage log
GET /logs/audit ADM Audit log

8. Frontend Architecture

Tab Loading (Lazy Components)

Every tab's HTML lives in /public/components/<tabname>.html. When a tab button is clicked, loadComponent() in app.js fetches the HTML, injects it into the tab section, then fires tabChanged event.

// app.js
document.dispatchEvent(new CustomEvent('tabChanged', { detail: { tab: tabName } }));

Critical pattern: Every JS module that needs to access tab DOM elements MUST listen for tabChanged, not DOMContentLoaded:

// Correct pattern for every tab module
(function() {
  var _inited = false;
  document.addEventListener('tabChanged', function(e) {
    if (e.detail.tab !== 'myTab' || _inited) return;
    _inited = true;
    // Now safe to querySelector elements — they exist in the DOM
    var btn = document.getElementById('my-btn');
    btn.addEventListener('click', ...);
  });
})();

If you use DOMContentLoaded instead, the elements won't exist yet (they're loaded async) and you'll get null.addEventListener errors.

Global Functions (defined in app.js)

These are available everywhere — no imports needed:

Function Description
getAuthHeaders() Returns { 'Content-Type': 'application/json', 'Authorization': 'Bearer <token>' }
getSelectedModel() Returns model ID from active tab's selector or global selector
showLoading(msg) Shows full-screen loading overlay
hideLoading() Hides loading overlay
showToast(msg, type) Shows toast notification. type: 'success'|'error'|'info'|'warning'
setOutputText(el, text) Sets text on contenteditable div, converting \n to <br>
transcribeAudio(blob) Sends audio blob to /api/transcribe, returns { success, text }
createSpeechRecognition() Returns Web Speech API recognition instance
createTimer(el) Returns timer object with .start() / .stop()

Rich Text Editor (Tiptap)

The body editor in the CMS uses Tiptap 2 (headless, no styling framework). The bundle is pre-built at /public/vendor/tiptap.bundle.js and exposes window.Tiptap = { Editor, StarterKit, Link, Underline, TextStyle, Color }.

To rebuild the bundle after updating Tiptap packages:

cat > tiptap-entry.js << 'EOF'
import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
import Link from '@tiptap/extension-link';
import Underline from '@tiptap/extension-underline';
import { TextStyle } from '@tiptap/extension-text-style';
import { Color } from '@tiptap/extension-color';
window.Tiptap = { Editor, StarterKit, Link, Underline, TextStyle, Color };
EOF
npx esbuild tiptap-entry.js --bundle --format=iife --minify --outfile=public/vendor/tiptap.bundle.js
rm tiptap-entry.js

9. AI Integration

src/utils/ai.jscallAI(messages, options)

The single function used by all routes. It routes to the correct provider automatically.

const { callAI } = require('../utils/ai');

const result = await callAI(
  [{ role: 'user', content: 'Generate a note...' }],
  {
    model: 'google/gemini-2.5-flash', // optional, uses default if omitted
    temperature: 0.3,                 // optional, default 0.3
    maxTokens: 4000                   // optional, default 4000
  }
);

// result = { success: true, content: '...', model: '...', provider: '...', duration: ms }

Prompt System

Prompts are defined in src/utils/prompts.js. Admins can override any prompt via the Admin panel (/admin/config/prompts). Overrides are stored in app_settings table and loaded into memory on startup (with 3s grace period for DB readiness).

To add a new prompt:

  1. Add a default in prompts.js
  2. Use PROMPTS.get('your-prompt-key') in your route
  3. The admin panel will auto-discover it

AI Generate for Learning Hub

The src/routes/learningAI.js file handles all Learning Hub AI generation.

For presentations: The AI is prompted to return raw Marp markdown (not JSON). The response is stored in the body column. Detection: content_type === 'presentation'.

For articles/quizzes/pearls: The AI returns JSON:

{
  "title": "...",
  "subject": "...",
  "body": "<p>HTML content</p>",
  "questions": [
    {
      "question_text": "...",
      "question_type": "mcq",
      "explanation": "...",
      "options": [
        { "option_text": "...", "is_correct": true, "explanation": "..." }
      ]
    }
  ]
}

10. Learning Hub & CMS

Content Types

Type Body format Has questions
article HTML (Tiptap) Optional
quiz HTML (brief intro) Always
pearl HTML Optional
presentation Marp markdown Never

Quiz Question Types

  • mcq — Single choice (radio buttons), 4 options, 1 correct
  • true_false — 2 options: "True" / "False", 1 correct
  • multi — Multiple select (checkboxes), scoring: all correct chosen AND no incorrect chosen

PPTX Generation

POST /admin/learning/generate-pptx parses Marp markdown (splits on ---), extracts # headings as slide titles, bullet points as content, and uses pptxgenjs to create a real .pptx. No Chromium required — pure Node.js.

Slide Preview

POST /admin/learning/preview-slides uses @marp-team/marp-core to render Marp markdown to HTML, then extracts individual <section> elements. Returns { css, slides[] }. The frontend renders these one at a time in a full-screen modal with arrow key + swipe navigation.

Content Display

In the Learning Hub viewer, content body is rendered via sanitizeHtml() in learningHub.js. This function allows a safe subset of HTML tags only (no <script>, no on* attributes, no style attributes except class).


11. Deployment

Local Development

cp .env.example .env   # Fill in your credentials
docker-compose -f docker-compose.local.yml up -d --build
# App runs at http://localhost:3552

Production (Docker Hub image)

# docker-compose.yml (production)
services:
  app:
    image: danielonyejesi/pediatric-ai-scribe-v3:latest
    ports: ["3000:3000"]
    env_file: .env
    depends_on:
      db:
        condition: service_healthy
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: pedscribe
      POSTGRES_USER: pedscribe
      POSTGRES_PASSWORD: your_secure_password
    volumes:
      - pgdata:/var/lib/postgresql/data

Docker Hub

Repository: danielonyejesi/pediatric-ai-scribe-v3

Tags:

  • latest — always points to most recent stable release
  • v3.x — specific version tags (immutable)

Git Repository

Repository: ifedan-ed/pediatric-ai-scribe-v3 (private)

Version tags: v3.1 through v3.15 as of this writing.

Build & Push Process

# After making changes:
git add -A && git commit -m "Description"
git push origin main && git tag v3.x && git push origin v3.x

docker build -t danielonyejesi/pediatric-ai-scribe-v3:v3.x \
             -t danielonyejesi/pediatric-ai-scribe-v3:latest .
docker push danielonyejesi/pediatric-ai-scribe-v3:v3.x
docker push danielonyejesi/pediatric-ai-scribe-v3:latest

# Rebuild local for testing:
docker-compose -f docker-compose.local.yml down
docker-compose -f docker-compose.local.yml up -d --build

12. Known Issues & Security Notes

Active Known Issues

  1. nodemailer HIGH vulnerability — v6.9.x has an email domain interpretation conflict. Upgrade to ^6.10.0 when available.
  2. unsafe-inline in CSPscriptSrc includes 'unsafe-inline' to support inline event handlers in HTML components. Should migrate to event listeners and remove this directive.
  3. JWT in localStorage — Tokens stored in localStorage are readable by JavaScript and therefore vulnerable to XSS attacks. A future migration to httpOnly cookies would eliminate this risk. See notes in auth.js and Section 6.
  4. window.prompt() in runAiRefineBody — Uses browser native prompt, which can be blocked in certain contexts. Should be replaced with an inline input field.
  5. webdav-learning-path endpoint — Sits behind moderatorMiddleware but is a user preference that non-moderator users might reasonably need. Consider moving to plain authMiddleware.

Security Hardening Already In Place

  • Helmet.js with custom CSP (no external script sources)
  • CORS restricted to APP_URL in production
  • Rate limiting on login (5/hr), register (5/hr), forgot-password (5/hr), general API (100/15min)
  • bcrypt cost 12 for password hashing
  • JWT with 7-day expiry
  • SQL injection protection: all queries use parameterized ? / $1 placeholders
  • Dynamic table names validated against an explicit allowlist (ALLOWED_SLUG_TABLES)
  • User input in HTML contexts goes through sanitizeHtml() (tag allowlist, strips on* attributes)
  • File upload MIME type validated by extension + content type
  • Admin/moderator route protection via middleware

13. Adding New Features

Adding a New Clinical Tab

  1. Create public/components/mytab.html with the tab's UI
  2. Add to index.html:
    • Tab button: <button class="tab-btn" data-tab="mytab">...</button>
    • Tab section: <section id="mytab-tab" class="tab-content" data-component="mytab"></section>
    • Script tag: <script defer src="/js/myTab.js"></script>
  3. Create public/js/myTab.js:
    (function() {
      var _inited = false;
      document.addEventListener('tabChanged', function(e) {
        if (e.detail.tab !== 'mytab' || _inited) return;
        _inited = true;
        // Wire up DOM elements here
      });
    })();
    
  4. Create src/routes/myTab.js with the API route
  5. Register in server.js: app.use('/api', require('./src/routes/myTab'));

Adding a New AI Prompt

  1. In src/utils/prompts.js, add to the defaults object:
    'my-prompt': 'You are a pediatric physician...'
    
  2. In your route: const prompt = PROMPTS.get('my-prompt') + '\n\n' + userInput
  3. The admin panel will show an editor for this prompt automatically.

Adding a New Learning Hub Content Type

  1. Add the new type to the content_type selector in cms.html
  2. Handle it in toggleEditorMode() in learningHub.js
  3. Add to the type detection in buildGeneratePrompt() in learningAI.js
  4. Handle rendering in learningHub.js loadContent() function
  5. No DB migration needed — content_type is a free-text column

14. Resetting Admin Password via Console

If you lose admin access and have no SMTP for password reset, use the Docker console:

# Step 1: Get a shell in the running app container
docker exec -it pediatric-ai-scribe sh

# Step 2: Open Node.js REPL
node

# Step 3: Hash your new password
const bcrypt = require('bcryptjs');
const hash = await bcrypt.hash('YourNewPassword123!', 12);
console.log(hash);
// Copy the hash output

# Step 4: Exit Node REPL
.exit

# Step 5: Open a DB shell
# (Exit app container first, then:)
docker exec -it pedscribe-db psql $POSTGRES_USER $POSTGRES_DB

# Step 6: Update the password (paste the hash)
UPDATE users
SET password = '$2a$12$...(your-hash-here)...'
WHERE email = 'your-admin@email.com';

# Verify:
SELECT email, left(password, 7) as hash_prefix FROM users WHERE email = 'your-admin@email.com';

# Exit:
\q

Enabling Registration via Console

docker exec -it pedscribe-db psql $POSTGRES_USER $POSTGRES_DB
UPDATE app_settings SET value = 'true' WHERE key = 'registration_enabled';
\q

Creating First Admin User (empty database)

The first user to register is automatically made admin. Enable registration, register, then disable registration again.

Or directly:

# In the Node REPL inside the app container:
const bcrypt = require('bcryptjs');
const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const hash = await bcrypt.hash('YourPassword', 12);
await pool.query(
  "INSERT INTO users (email, password, name, role, email_verified) VALUES ($1, $2, $3, 'admin', true)",
  ['admin@yourdomain.com', hash, 'Admin']
);
pool.end();

15. Version History (Recent)

Tag Key changes
v3.19 Login flash fixed (auth screen hidden by CSS default); presentation quiz option; feed labels corrected
v3.18 pdf-parse downgraded to v1.1.1; WebDAV selection UX fixed; topic context on upload/WebDAV tabs; inline refine bar replaces window.prompt(); CSP: removed unsafe-inline (all onclick= converted to data-action delegation); webdav-path moved to /api/user/webdav-path (auth-only)
v3.17 AI panel context-aware options fixed (style.display replaces classList — CSS cascade bug); quiz card redesign
v3.16 DEVELOPER_GUIDE.md created
v3.15 Auth reverted to localStorage tokens; slide preview padding fixed
v3.14 AI panel context-aware options (word count, slide count, quiz toggle); delete wording per type
v3.13 Delete confirm inline bar CSS bug fixed; slide preview in-page modal (arrow/swipe nav); Marp textarea placeholder
v3.12 Delete inline confirm bar; lighter login screen; Presentation type (Marp + pptxgenjs PPTX)
v3.11 AI content generation for Learning Hub (topic/file/Nextcloud, pdf-parse, pptxgenjs)
v3.10 Custom 404 page; server returns 404 for unknown paths
v3.8 Quill replaced with Tiptap 2 (self-hosted bundle, inline link bar)

16. Current Docker Image

Latest stable: danielonyejesi/pediatric-ai-scribe-v3:v3.19

docker pull danielonyejesi/pediatric-ai-scribe-v3:v3.19
# or always latest:
docker pull danielonyejesi/pediatric-ai-scribe-v3:latest

17. PDF Uploads — Why No Vector Embeddings

The current approach for PDF/file uploads in the Learning Hub AI generator:

  1. User uploads a file (or picks from Nextcloud)
  2. pdf-parse v1.1.1 extracts the full text from the PDF
  3. The full text (up to 12,000 characters) is sent as context in the AI prompt
  4. AI generates structured content (article body + quiz questions) based on that context

You do NOT need embeddings or RAG (Retrieval Augmented Generation) for this use case. Here's why:

Scenario Use embeddings? Why
Upload 1 file → generate 1 article No Full text fits in context; AI sees everything
Search across 100+ stored documents Yes Too much text for one context window
Long PDF (200+ pages) Maybe Truncation at 12k chars; embeddings enable chunked retrieval
This app's current use case No Single file, single generation, full context is better

Embeddings (e.g. OpenAI text-embedding-ada-002, pgvector in PostgreSQL) would add complexity, cost, and storage requirements with no user-facing benefit for single-document content generation. The current approach is intentionally simple and correct.

If documents exceed 12,000 characters, increase the truncation limit in learningAI.js:

docText.substring(0, 12000)  // increase if needed (watch token costs)

18. Scalability

Current Architecture (Single Instance)

The app runs as a single Node.js process. This is fine for a team/department deployment (tens to hundreds of concurrent users).

What Scales Well Already

  • Stateless JWT auth — no server-side session store; any instance can validate any token
  • PostgreSQL — handles concurrent connections well; supports read replicas
  • Lazy-loaded component HTML — reduces initial page size; tabs load on demand
  • AI calls — fully async; expensive calls don't block other requests

Bottlenecks to Address Before Horizontal Scaling

Issue Current Fix for multi-instance
Rate limiting In-memory (per process) Replace with Redis (rate-limit-redis)
File uploads multer in RAM Route uploads to S3/object storage
Scheduled cleanup setTimeout in server.js Use a dedicated cron job or DB-scheduled task

How to Scale Horizontally

# docker-compose with 3 app replicas + nginx load balancer
services:
  app:
    image: danielonyejesi/pediatric-ai-scribe-v3:latest
    deploy:
      replicas: 3
    environment:
      DATABASE_URL: postgresql://...  # shared external Postgres
      REDIS_URL: redis://redis:6379   # add when rate-limit-redis is wired
  nginx:
    image: nginx:alpine
    # upstream: round-robin across app replicas
  redis:
    image: redis:7-alpine
  postgres:
    image: postgres:16-alpine

Cloud deployment options (all work with the current Docker image):

  • AWS ECS/Fargate — managed containers, easy auto-scaling
  • Railway / Render / Fly.io — simple push-to-deploy with Docker
  • Kubernetes — full control, overkill for most deployments

19. Security Architecture — localStorage vs httpOnly Cookies

The app stores JWT tokens in localStorage. This is a deliberate choice appropriate for this scale. The key security facts:

Current protections in place (more important than storage location):

  • Content-Security-Policy: script-src 'self' — blocks all external scripts and inline JS (v3.18)
  • Input sanitization via sanitizeHtml() allowlist on all user-generated HTML
  • All 26 onclick= inline event handlers removed (v3.18) — reduces XSS surface
  • Rate limiting on auth endpoints
  • Helmet.js security headers
  • Parameterized SQL queries throughout

The reality about localStorage vs httpOnly cookies:

"Unless you're a bank or large enterprise, it doesn't really matter. Focus on preventing XSS, because that's what actually matters... fundamentally, the security benefit of using httpOnly cookies is very minimal. If your site suffers any kind of XSS, it makes it slightly more difficult for an attacker to use the auth token." — Security engineering community consensus

httpOnly cookies prevent token copying but not token use — an XSS attacker can still make authenticated requests on the user's behalf regardless of where the token is stored.

If you later want httpOnly cookies: The infrastructure is already in place (cookie-parser, CORS credentials:true). The change is: (1) set cookie on login, (2) remove token from getAuthHeaders(), (3) add /api/auth/logout to clear cookie. See notes in auth.js. This was implemented and reverted in v3.14 — it works but adds CSRF considerations.

Token lifetime: Currently 7 days. For higher security, reduce to 1-2 hours and add refresh token rotation.


15. Version History (Recent)

Tag Key changes
v3.19 Login flash fixed; presentation quiz option; feed labels corrected
v3.18 pdf-parse v1.1.1; WebDAV selection UX; topic context on upload/WebDAV; inline refine bar; CSP unsafe-inline removed; webdav-path auth fix
v3.17 AI panel CSS cascade bug fixed; quiz card redesign
v3.16 DEVELOPER_GUIDE.md created
v3.15 Auth reverted to localStorage; slide preview padding
v3.14 AI panel context-aware options; delete wording per type
v3.13 Slide preview in-page modal (arrow/swipe/keyboard)
v3.12 Delete inline confirm; lighter login; Presentation type (Marp + PPTX)
v3.11 AI content generation for Learning Hub (topic/file/Nextcloud)
v3.10 Custom 404 page
v3.8 Tiptap 2 (self-hosted, inline link bar, no popup)

Last updated: March 2026 — v3.19 Generated for developer handover.