Add DEVELOPER_GUIDE.md — full handover documentation
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.
This commit is contained in:
parent
b5bfcc5489
commit
e1ffe3dac2
1 changed files with 734 additions and 0 deletions
734
DEVELOPER_GUIDE.md
Normal file
734
DEVELOPER_GUIDE.md
Normal file
|
|
@ -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 <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.*
|
||||
Loading…
Reference in a new issue