Improve email templates and update README

- Replace bare-bones email HTML with branded templates: gradient header,
  action button, plain-text URL fallback, security footer notice
- Welcome/verify email: friendly greeting with user's name
- Password reset email: clear instructions, 1-hour expiry notice,
  explicit 'if you didn't request this, ignore it' message
- README: updated features list, vendor model 4.6 models, plain-text output note
This commit is contained in:
Daniel Onyejesi 2026-03-21 22:43:19 -04:00
parent a752de60ab
commit 797b22e58b
2 changed files with 76 additions and 20 deletions

View file

@ -1,19 +1,23 @@
# 🏥 Pediatric AI Scribe v3
# 🩺 Pediatric AI Scribe v3
AI-powered clinical documentation platform for pediatric medicine. Generates HPIs, hospital courses, chart reviews, SOAP notes, and developmental milestone assessments from voice recordings or dictation.
AI-powered clinical documentation platform for pediatric medicine. Generates HPIs, hospital courses, chart reviews, SOAP notes, and developmental milestone assessments from voice recordings or dictation — in seconds, in plain copy-ready text.
## Features
- **Live Encounter → HPI** — record doctor-patient conversation, AI generates structured HPI
- **Voice Dictation → HPI / SOAP** — dictate your narrative, AI restructures it
- **Hospital Course Generator** — paste progress notes by date, AI generates organized discharge summary (prose, day-by-day, organ-system, or psych format)
- **Chart Review / Precharting** — summarize outpatient, subspecialty, and ED notes
- **Live Encounter → HPI** — record a live doctor-patient conversation, AI generates a structured OLDCARTS HPI
- **Voice Dictation → HPI / SOAP** — dictate your narrative, AI cleans and restructures it
- **Hospital Course Generator** — paste progress notes, AI generates prose, day-by-day, organ-system (ICU), or psych format summaries
- **Chart Review / Precharting** — summarize outpatient, subspecialty, and ED notes into a precharting brief
- **SOAP Note Generator** — full SOAP or subjective-only from dictation
- **Developmental Milestones** — AAP/Nelson milestone tracker with narrative output
- **Admin Panel** — user management, registration control, usage stats
- **Per-tab model selector** — choose fast vs. smart models per task
- **Nextcloud integration** — export documents to your Nextcloud instance
- **Developmental Milestones** — AAP/Nelson milestone tracker with narrative, structured list, or 3-sentence summary output
- **Plain text output** — all documents generated without markdown, ready to paste into any EHR
- **Read Aloud** — browser TTS reads generated documents; ElevenLabs (Adam voice) supported
- **Copy & Export** — one-click copy or export to Nextcloud
- **Refine & Shorten** — edit any document with plain-language AI instructions
- **Per-tab model selector** — choose fast vs. smart vs. reasoning models per task
- **Admin Panel** — user management, registration control, audit logs
- **2FA** — TOTP-based two-factor authentication
- **Multi-provider AI** — OpenRouter, AWS Bedrock, or Azure OpenAI
---
@ -126,9 +130,11 @@ AWS_SECRET_ACCESS_KEY=...
Or use an IAM role (no keys needed when running on EC2/ECS — just set the region).
Available Bedrock models (auto-selected when `AI_PROVIDER=bedrock`):
- vendor model Opus 4.6 — best language nuance (`anthropic.agent-config-opus-4-6-20251001-v1:0`)
- vendor model Sonnet 4.6 — recommended (`anthropic.agent-config-sonnet-4-6-20251001-v1:0`)
- vendor model Sonnet 4 (`anthropic.agent-config-sonnet-4-20250514-v1:0`)
- vendor model 3.5 Sonnet (`anthropic.agent-config-3-5-sonnet-20241022-v2:0`)
- vendor model 3 Haiku — default, cheapest (`anthropic.agent-config-3-haiku-20240307-v1:0`)
- vendor model 3 Haiku — cheapest (`anthropic.agent-config-3-haiku-20240307-v1:0`)
- Llama 3.1 70B / 8B
- Mistral Large

View file

@ -8,6 +8,33 @@ const crypto = require('crypto');
const db = require('../db/database');
const { JWT_SECRET, authMiddleware } = require('../middleware/auth');
// ============================================================
// EMAIL TEMPLATES
// ============================================================
function emailWrapper(body) {
return `<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
<body style="margin:0;padding:0;background:#f3f4f6;font-family:'Segoe UI',Arial,sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f3f4f6;padding:40px 20px;">
<tr><td align="center">
<table width="520" cellpadding="0" cellspacing="0" style="background:white;border-radius:12px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.08);">
<tr><td style="background:linear-gradient(135deg,#2563eb 0%,#7c3aed 100%);padding:28px 36px;">
<p style="margin:0;color:white;font-size:22px;font-weight:700;">🩺 Pediatric AI Scribe</p>
<p style="margin:6px 0 0;color:rgba(255,255,255,0.8);font-size:13px;">AI-Powered Clinical Documentation</p>
</td></tr>
<tr><td style="padding:36px;">${body}</td></tr>
<tr><td style="padding:16px 36px 28px;border-top:1px solid #e5e7eb;">
<p style="margin:0;color:#9ca3af;font-size:12px;">This email was sent from Pediatric AI Scribe. If you did not request this, you can safely ignore it no action is needed and your account remains secure.</p>
</td></tr>
</table>
</td></tr>
</table>
</body></html>`;
}
function btnStyle() {
return 'display:inline-block;background:linear-gradient(135deg,#2563eb,#7c3aed);color:white;padding:13px 32px;border-radius:8px;text-decoration:none;font-weight:600;font-size:15px;';
}
// Email helper
async function sendEmail(to, subject, html) {
if (!process.env.SMTP_HOST) {
@ -64,13 +91,16 @@ router.post('/register', async (req, res) => {
var userId = result.lastInsertRowid;
var verifyUrl = (process.env.APP_URL || 'http://localhost:3000') + '/api/auth/verify-email?token=' + verifyToken;
await sendEmail(email, 'Verify your email - Pediatric AI Scribe',
'<div style="font-family:sans-serif;max-width:500px;margin:0 auto;padding:20px;">' +
'<h2 style="color:#2563eb;">Welcome to Pediatric AI Scribe!</h2>' +
'<p>Hi ' + name + ',</p><p>Verify your email:</p>' +
'<p style="text-align:center;margin:30px 0;"><a href="' + verifyUrl + '" style="background:#2563eb;color:white;padding:12px 30px;border-radius:8px;text-decoration:none;font-weight:600;">Verify Email</a></p>' +
'<p style="color:#6b7280;font-size:13px;">Expires in 24 hours.</p></div>'
);
await sendEmail(email, 'Welcome — please verify your email', emailWrapper(
`<p style="margin:0 0 8px;font-size:22px;">👋 Welcome aboard, ${name}!</p>
<p style="color:#4b5563;margin:12px 0 24px;line-height:1.6;">Great to have you on Pediatric AI Scribe. Before you start generating clinical notes, please confirm your email address by clicking the button below.</p>
<p style="text-align:center;margin:28px 0;">
<a href="${verifyUrl}" style="${btnStyle()}">Verify My Email</a>
</p>
<p style="color:#6b7280;font-size:13px;margin:20px 0 4px;">Button not working? Copy and paste this link into your browser:</p>
<p style="background:#f3f4f6;border-radius:6px;padding:10px 14px;font-size:12px;word-break:break-all;color:#374151;margin:0;">${verifyUrl}</p>
<p style="color:#9ca3af;font-size:12px;margin:20px 0 0;">This link expires in 24 hours.</p>`
));
await db.run('INSERT INTO audit_log (user_id, action, ip_address, details) VALUES (?, ?, ?, ?)',
[userId, 'register', req.ip, role === 'admin' ? 'First user — auto admin' : 'standard user']);
@ -120,7 +150,16 @@ router.post('/resend-verification', async (req, res) => {
var verifyToken = crypto.randomBytes(32).toString('hex');
await db.run('UPDATE users SET verify_token = ?, verify_expires = ? WHERE id = ?', [verifyToken, Date.now() + 86400000, user.id]);
var verifyUrl = (process.env.APP_URL || 'http://localhost:3000') + '/api/auth/verify-email?token=' + verifyToken;
await sendEmail(req.body.email, 'Verify your email', '<p>Click to verify: <a href="' + verifyUrl + '">' + verifyUrl + '</a></p>');
await sendEmail(req.body.email, 'Verify your email — Pediatric AI Scribe', emailWrapper(
`<p style="margin:0 0 8px;font-size:20px;">📧 Verify your email</p>
<p style="color:#4b5563;margin:12px 0 24px;line-height:1.6;">Here's a fresh verification link for your account. Click below to confirm your email and get started.</p>
<p style="text-align:center;margin:28px 0;">
<a href="${verifyUrl}" style="${btnStyle()}">Verify My Email</a>
</p>
<p style="color:#6b7280;font-size:13px;margin:20px 0 4px;">Button not working? Copy and paste this link into your browser:</p>
<p style="background:#f3f4f6;border-radius:6px;padding:10px 14px;font-size:12px;word-break:break-all;color:#374151;margin:0;">${verifyUrl}</p>
<p style="color:#9ca3af;font-size:12px;margin:20px 0 0;">This link expires in 24 hours.</p>`
));
res.json({ success: true, message: 'Verification email sent' });
} catch (err) { res.status(500).json({ error: err.message }); }
});
@ -205,7 +244,18 @@ router.post('/forgot-password', async (req, res) => {
if (!user) return res.json({ success: true, message: 'If account exists, reset email sent' });
var token = crypto.randomBytes(32).toString('hex');
await db.run('UPDATE users SET reset_token = ?, reset_expires = ? WHERE id = ?', [token, Date.now() + 3600000, user.id]);
await sendEmail(req.body.email, 'Password Reset', '<p>Reset: <a href="' + (process.env.APP_URL || 'http://localhost:3000') + '/reset-password?token=' + token + '">Click here</a></p>');
var resetUrl = (process.env.APP_URL || 'http://localhost:3000') + '/reset-password?token=' + token;
await sendEmail(req.body.email, 'Reset your password — Pediatric AI Scribe', emailWrapper(
`<p style="margin:0 0 8px;font-size:20px;">🔐 Password reset request</p>
<p style="color:#4b5563;margin:12px 0 6px;line-height:1.6;">Hi there,</p>
<p style="color:#4b5563;margin:0 0 24px;line-height:1.6;">Someone requested a password reset for your Pediatric AI Scribe account. If that was you, click the button below to choose a new password.</p>
<p style="text-align:center;margin:28px 0;">
<a href="${resetUrl}" style="${btnStyle()}">Reset My Password</a>
</p>
<p style="color:#6b7280;font-size:13px;margin:20px 0 4px;">Button not working? Copy and paste this link into your browser:</p>
<p style="background:#f3f4f6;border-radius:6px;padding:10px 14px;font-size:12px;word-break:break-all;color:#374151;margin:0;">${resetUrl}</p>
<p style="color:#9ca3af;font-size:12px;margin:20px 0 0;">This link expires in 1 hour. If you did not request a password reset, no action is needed your password has not been changed.</p>`
));
res.json({ success: true, message: 'If account exists, reset email sent' });
} catch (err) { res.status(500).json({ error: err.message }); }
});