Clean email templates (Linear/Resend style) + LiteLLM Gemini STT

Emails: white card, clean typography, dark button, no gradients.
Same minimal aesthetic as Linear/Resend/Notion emails.
Verify page responses also updated to match.
This commit is contained in:
Daniel Onyejesi 2026-03-30 20:51:11 -04:00
parent bb459f57b8
commit ce8e959ea4
2 changed files with 69 additions and 39 deletions

View file

@ -66,11 +66,12 @@ router.post('/config/test-email', async function(req, res) {
}
var emailWrapper = require('./auth').__emailWrapper;
var btnStyle = require('./auth').__btnStyle;
var btnHtml = require('./auth').__btnHtml;
var html = emailWrapper(
'<p style="color:#4b5563;margin:0 0 20px;line-height:1.6;">' + bodyText.replace(/\n/g, '<br>') + '</p>' +
(btnStyle ? '<a href="#" style="' + btnStyle() + '">Test Button</a>' : '')
'<p style="margin:0 0 6px;font-size:18px;font-weight:600;color:#111827;">Test Email</p>' +
'<p style="color:#6b7280;margin:12px 0 20px;line-height:1.6;font-size:14px;">' + bodyText.replace(/\n/g, '<br>') + '</p>' +
btnHtml('#', 'Test Button')
);
var ok = await sendEmail(to, '[TEST] ' + subject, html);

View file

@ -38,49 +38,78 @@ function safeAppUrl() {
}
// ============================================================
// EMAIL TEMPLATES — responsive, max-width constrained
// EMAIL TEMPLATES — clean, minimal (Linear / Resend style)
// ============================================================
function emailWrapper(body) {
return `<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<style>
body,table,td{font-family:'Segoe UI',Roboto,Arial,sans-serif;}
@media only screen and (max-width:600px){
.email-outer{padding:16px 8px !important;}
.email-inner{width:100% !important;min-width:0 !important;}
.email-body{padding:24px 20px !important;}
.email-header{padding:20px 20px !important;}
.email-footer{padding:12px 20px 20px !important;}
.email-btn{padding:12px 24px !important;font-size:14px !important;}
}
</style>
var siteName = process.env.SITE_NAME || 'Pediatric AI Scribe';
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>${escHtml(siteName)}</title>
<style>
body,table,td,p,a{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;}
@media only screen and (max-width:600px){
.wrap{width:100% !important;padding:0 16px !important;}
.inner{padding:28px 24px !important;}
}
</style>
</head>
<body style="margin:0;padding:0;background:#f3f4f6;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f3f4f6;">
<tr><td class="email-outer" style="padding:32px 16px;" align="center">
<table role="presentation" class="email-inner" cellpadding="0" cellspacing="0" border="0" style="background:white;border-radius:12px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.08);max-width:520px;width:100%;">
<tr><td class="email-header" style="background:linear-gradient(135deg,#2563eb 0%,#7c3aed 100%);padding:24px 32px;">
<p style="margin:0;color:white;font-size:20px;font-weight:700;">Pediatric AI Scribe</p>
<p style="margin:4px 0 0;color:rgba(255,255,255,0.8);font-size:12px;">AI-Powered Clinical Documentation</p>
</td></tr>
<tr><td class="email-body" style="padding:32px;">${body}</td></tr>
<tr><td class="email-footer" style="padding:14px 32px 24px;border-top:1px solid #e5e7eb;">
<p style="margin:0;color:#9ca3af;font-size:11px;line-height:1.5;">This email was sent by Pediatric AI Scribe. If you did not request this, you can safely ignore it.</p>
</td></tr>
</table>
</td></tr>
</table>
</body></html>`;
<body style="margin:0;padding:0;background:#fafafa;-webkit-text-size-adjust:100%;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#fafafa;">
<tr><td align="center" style="padding:40px 24px;">
<!-- Logo / wordmark -->
<table role="presentation" class="wrap" cellpadding="0" cellspacing="0" border="0" style="max-width:520px;width:100%;margin-bottom:24px;">
<tr><td>
<p style="margin:0;font-size:15px;font-weight:600;color:#111827;letter-spacing:-0.2px;">${escHtml(siteName)}</p>
</td></tr>
</table>
<!-- Card -->
<table role="presentation" class="wrap" cellpadding="0" cellspacing="0" border="0" style="max-width:520px;width:100%;background:#ffffff;border:1px solid #e5e7eb;border-radius:8px;">
<tr><td class="inner" style="padding:36px 40px;">
${body}
</td></tr>
</table>
<!-- Footer -->
<table role="presentation" class="wrap" cellpadding="0" cellspacing="0" border="0" style="max-width:520px;width:100%;margin-top:20px;">
<tr><td>
<p style="margin:0;font-size:12px;color:#9ca3af;line-height:1.6;">
${escHtml(siteName)} &middot; AI-powered clinical documentation<br>
If you didn&rsquo;t request this email, you can safely ignore it.
</p>
</td></tr>
</table>
</td></tr>
</table>
</body>
</html>`;
}
function btnHtml(href, label) {
return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:24px auto;"><tr><td align="center" style="border-radius:8px;background:linear-gradient(135deg,#2563eb,#7c3aed);">
<a class="email-btn" href="${escHtml(href)}" target="_blank" style="display:inline-block;color:white;padding:13px 32px;border-radius:8px;text-decoration:none;font-weight:600;font-size:15px;font-family:'Segoe UI',Roboto,Arial,sans-serif;">${escHtml(label)}</a>
</td></tr></table>`;
return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:28px 0 4px;">
<tr><td style="border-radius:6px;background:#111827;">
<a href="${escHtml(href)}" target="_blank"
style="display:inline-block;color:#ffffff;padding:12px 24px;border-radius:6px;
text-decoration:none;font-weight:500;font-size:14px;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;
letter-spacing:-0.1px;">${escHtml(label)}</a>
</td></tr>
</table>`;
}
function linkFallback(url) {
return `<p style="color:#6b7280;font-size:12px;margin:16px 0 4px;">Button not working? Copy this link into your browser:</p>
<p style="background:#f3f4f6;border-radius:6px;padding:10px 14px;font-size:11px;word-break:break-all;color:#374151;margin:0;max-width:100%;overflow-wrap:break-word;">${escHtml(url)}</p>`;
return `<p style="margin:20px 0 6px;font-size:13px;color:#6b7280;">
Or copy and paste this link into your browser:
</p>
<p style="margin:0;background:#f3f4f6;border-radius:4px;padding:10px 12px;
font-size:12px;font-family:'SFMono-Regular',Consolas,monospace;
word-break:break-all;color:#374151;line-height:1.5;">${escHtml(url)}</p>`;
}
// Email helper — DB settings override env vars
@ -183,11 +212,11 @@ router.get('/verify-email', async (req, res) => {
if (!token) return res.status(400).send('Missing token');
var user = await db.get('SELECT id, name FROM users WHERE verify_token = ? AND verify_expires > ?', [token, Date.now()]);
if (!user) {
return res.send('<html><body style="font-family:sans-serif;text-align:center;padding:60px;"><h2 style="color:#ef4444;">Invalid or Expired Link</h2><p><a href="' + safeAppUrl() + '">Go to app</a></p></body></html>');
return res.send('<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Link Expired</title></head><body style="margin:0;padding:40px 24px;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Helvetica,Arial,sans-serif;background:#fafafa;text-align:center;"><p style="font-size:15px;font-weight:600;color:#111827;margin:0 0 8px;">Link expired or invalid</p><p style="color:#6b7280;font-size:14px;margin:0 0 24px;">This verification link has expired. Request a new one from the app.</p><a href="' + safeAppUrl() + '" style="display:inline-block;background:#111827;color:#fff;padding:10px 22px;border-radius:6px;text-decoration:none;font-size:14px;font-weight:500;">Go to app</a></body></html>');
}
await db.run('UPDATE users SET email_verified = true, verify_token = NULL, verify_expires = NULL WHERE id = ?', [user.id]);
await db.run('INSERT INTO audit_log (user_id, action) VALUES (?, ?)', [user.id, 'email_verified']);
res.send('<html><body style="font-family:sans-serif;text-align:center;padding:60px;"><h2 style="color:#10b981;">Email Verified!</h2><p>Welcome, ' + escHtml(user.name) + '!</p><p style="margin-top:20px;"><a href="' + safeAppUrl() + '" style="background:#2563eb;color:white;padding:12px 30px;border-radius:8px;text-decoration:none;">Open App</a></p></body></html>');
res.send('<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Email Verified</title></head><body style="margin:0;padding:40px 24px;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Helvetica,Arial,sans-serif;background:#fafafa;text-align:center;"><p style="font-size:15px;font-weight:600;color:#111827;margin:0 0 8px;">Email verified</p><p style="color:#6b7280;font-size:14px;margin:0 0 24px;">Welcome, ' + escHtml(user.name) + '. You\'re all set.</p><a href="' + safeAppUrl() + '" style="display:inline-block;background:#111827;color:#fff;padding:10px 22px;border-radius:6px;text-decoration:none;font-size:14px;font-weight:500;">Open app</a></body></html>');
} catch (err) { console.error('[Auth] Verify error:', err.message); res.status(500).send('Verification failed. Please try again.'); }
});