diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md new file mode 100644 index 0000000..d20b9c6 --- /dev/null +++ b/DEVELOPER_GUIDE.md @@ -0,0 +1,734 @@ +# Pediatric AI Scribe — Developer Guide +**Version:** 3.15 | **Stack:** Node.js / Express / PostgreSQL / Vanilla JS + +--- + +## Table of Contents + +1. [Project Overview](#1-project-overview) +2. [Architecture](#2-architecture) +3. [Directory Structure](#3-directory-structure) +4. [Environment Variables](#4-environment-variables) +5. [Database Schema](#5-database-schema) +6. [Authentication System](#6-authentication-system) +7. [Backend API Reference](#7-backend-api-reference) +8. [Frontend Architecture](#8-frontend-architecture) +9. [AI Integration](#9-ai-integration) +10. [Learning Hub & CMS](#10-learning-hub--cms) +11. [Deployment](#11-deployment) +12. [Known Issues & Security Notes](#12-known-issues--security-notes) +13. [Adding New Features](#13-adding-new-features) +14. [Resetting Admin Password via Console](#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): + +```bash +# ── Required ────────────────────────────────────────────────── +DATABASE_URL=postgresql://user:pass@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 +``` + +**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 ` 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) +```javascript +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 `` 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/.html`. When a tab button is clicked, `loadComponent()` in `app.js` fetches the HTML, injects it into the tab section, then fires `tabChanged` event. + +```javascript +// 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`: + +```javascript +// 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 ' }` | +| `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 `
` | +| `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: +```bash +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.js` — `callAI(messages, options)` + +The single function used by all routes. It routes to the correct provider automatically. + +```javascript +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: +```json +{ + "title": "...", + "subject": "...", + "body": "

HTML content

", + "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 `
` 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 `` +3. Create `public/js/myTab.js`: + ```javascript + (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: + ```javascript + '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: + +```bash +# 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 +```bash +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: +```bash +# 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(); +``` + +--- + +*Last updated: March 2026 — v3.15* +*Generated for developer handover.*