Covers: architecture, directory structure, all env vars, full DB schema, auth system, every API endpoint with auth level, frontend tab loading pattern (critical: tabChanged not DOMContentLoaded), AI integration, Learning Hub content types, deployment commands, security notes, admin password reset via Docker console, and how to add new features.
734 lines
30 KiB
Markdown
734 lines
30 KiB
Markdown
# 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:<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)
|
|
```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 `<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.
|
|
|
|
```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 <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:
|
|
```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": "<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
|
|
```bash
|
|
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)
|
|
```bash
|
|
# 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
|
|
```bash
|
|
# 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 CSP** — `scriptSrc` 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`:
|
|
```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.*
|