Sessions 5-7a: 955 tests, deployment ready

This commit is contained in:
Kev
2026-06-08 18:35:13 -04:00
parent 06b82624a2
commit 1fa04dc776
371 changed files with 49366 additions and 955 deletions
+193
View File
@@ -0,0 +1,193 @@
/**
* Backend email templates.
*
* These produce the `{subject, html, text}` payload that the broadcast
* layer (Listmonk, Ghost, etc.) ships to subscribers. We do not call SMTP
* directly — the broadcast platform handles deliverability, unsubscribe
* tokens, list management.
*
* Subject lines are content-driven per spec: NOT "VYNDR Daily Cheatsheet
* — May 28"; instead something like "Jokic A+, 3 kills on the Lakers
* game, 8 games tonight".
*
* SECURITY: every interpolated value passes through `esc()` so no
* subscriber-controlled string can inject HTML.
*/
const SITE = process.env.NEXT_PUBLIC_SITE_URL || 'https://vyndr.app';
function esc(s) {
if (s == null) return '';
return String(s)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function htmlShell(body) {
return `<!doctype html>
<html><body style="margin:0;padding:32px 16px;background:#06060B;color:#E8E8F0;font-family:'Instrument Sans',-apple-system,system-ui,sans-serif;line-height:1.6">
<div style="max-width:600px;margin:0 auto;background:#0E0E16;border:1px solid #1E1E2E;border-radius:16px;padding:32px">
<h1 style="font-family:'IBM Plex Mono','JetBrains Mono',monospace;font-size:22px;font-weight:800;letter-spacing:0.10em;margin:0 0 24px;color:#E8E8F0">
VYND<span style="color:#00D4A0;text-shadow:0 0 12px rgba(0,212,160,0.6)">R</span>
</h1>
${body}
<hr style="border:none;border-top:1px solid #1E1E2E;margin:32px 0 16px" />
<p style="font-size:11px;color:#4A4A5E;margin:0;font-family:'IBM Plex Mono',monospace">
VYNDR is an analytics tool, not a sportsbook. Gamble responsibly. 1-800-522-4700.<br/>
Reply STOP to unsubscribe.
</p>
</div>
</body></html>`;
}
// ── Cheatsheet — daily 4:30 PM ET ────────────────────────────────────────
function buildCheatsheetEmail(cheatsheetData = {}) {
const grades = Array.isArray(cheatsheetData.grades) ? cheatsheetData.grades : [];
const top = grades[0];
const gameCount = cheatsheetData.gameCount ?? 0;
// Content-driven subject:
// "Jokic A+, 3 kills on the Lakers game, 8 games tonight"
const parts = [];
if (top) parts.push(`${top.player || 'top'} ${top.grade || ''}`.trim());
if (cheatsheetData.killCount && cheatsheetData.killTeam) {
parts.push(`${cheatsheetData.killCount} kills on the ${cheatsheetData.killTeam} game`);
}
parts.push(`${gameCount} game${gameCount === 1 ? '' : 's'} tonight`);
const subject = parts.join(', ');
const rows = grades.slice(0, 6).map((g) => `
<tr>
<td style="padding:10px 0;color:#00D4A0;font-family:'IBM Plex Mono',monospace;font-weight:800;font-size:18px;width:60px">${esc(g.grade)}</td>
<td style="padding:10px 0;color:#E8E8F0;font-weight:600">${esc(g.player)}</td>
<td style="padding:10px 0;color:#7A7A8E;font-family:'IBM Plex Mono',monospace;text-align:right">${esc(`${g.stat || ''} ${String(g.direction || '').toUpperCase()} ${g.line ?? ''}`)}</td>
</tr>`).join('');
const body = `
<p style="font-size:16px"><strong>Tonight's cheatsheet.</strong></p>
<p style="color:#7A7A8E;font-size:14px">${esc(cheatsheetData.date || '')} · ${esc(String(gameCount))} games</p>
<table style="width:100%;border-collapse:collapse;margin:16px 0">${rows}</table>
${cheatsheetData.imageUrl ? `<p style="margin:16px 0"><img src="${esc(cheatsheetData.imageUrl)}" alt="VYNDR cheatsheet" style="max-width:100%;border-radius:12px;display:block"/></p>` : ''}
<p style="margin-top:24px">
<a href="${esc(SITE)}/dashboard"
style="display:inline-block;padding:12px 24px;background:#1A5A42;color:#E8FFF4;text-decoration:none;border-radius:12px;font-weight:600">
See the full slate →
</a>
</p>`;
const text =
`Tonight's cheatsheet.
${cheatsheetData.date || ''} · ${gameCount} games
${grades.slice(0, 6).map((g) => `${g.grade}\t${g.player}\t${g.stat || ''} ${String(g.direction || '').toUpperCase()} ${g.line ?? ''}`).join('\n')}
See the full slate: ${SITE}/dashboard
VYNDR · vyndr.app · @getvyndr
Reply STOP to unsubscribe.`;
return { subject, html: htmlShell(body), text };
}
// ── Results — daily 9 AM ET ──────────────────────────────────────────────
function buildResultsEmail(resultsData = {}) {
const accuracy = resultsData.accuracy ?? 0;
const hits = resultsData.hits ?? 0;
const total = resultsData.total ?? 0;
const entries = Array.isArray(resultsData.entries) ? resultsData.entries : [];
const subject = `Last night: ${accuracy}% — ${hits} of ${total} grades hit`;
const rows = entries.slice(0, 8).map((e) => {
const hit = e.result === 'hit';
const tint = hit ? 'rgba(0,212,160,0.10)' : 'rgba(255,82,82,0.08)';
const mark = hit ? '✓' : '✗';
const markColor = hit ? '#00D4A0' : '#FF5252';
return `
<tr style="background:${tint}">
<td style="padding:10px 12px;color:${markColor};font-family:'IBM Plex Mono',monospace;font-weight:800;font-size:18px;width:32px">${mark}</td>
<td style="padding:10px 12px;color:#E8E8F0;font-weight:600">${esc(e.player)}</td>
<td style="padding:10px 12px;color:#7A7A8E;font-family:'IBM Plex Mono',monospace;font-size:13px">${esc(`${e.stat || ''} ${String(e.direction || '').toUpperCase()} ${e.line ?? ''}`)}</td>
<td style="padding:10px 12px;color:#00D4A0;font-family:'IBM Plex Mono',monospace;font-weight:800;text-align:right">${esc(e.grade)}</td>
<td style="padding:10px 12px;color:${markColor};font-family:'IBM Plex Mono',monospace;font-weight:700;text-align:right">${hit ? 'HIT' : 'MISS'}</td>
</tr>`;
}).join('');
const body = `
<p style="font-size:16px"><strong>Last night's results.</strong></p>
<p style="color:#7A7A8E;font-size:14px">${esc(resultsData.date || '')}</p>
<p style="font-family:'IBM Plex Mono',monospace;font-size:32px;font-weight:800;color:#00D4A0;text-shadow:0 0 12px rgba(0,212,160,0.6);margin:8px 0">
${accuracy}% <span style="font-size:14px;color:#7A7A8E">(${hits}/${total})</span>
</p>
<table style="width:100%;border-collapse:separate;border-spacing:0 4px;margin:16px 0">${rows}</table>
${resultsData.imageUrl ? `<p style="margin:16px 0"><img src="${esc(resultsData.imageUrl)}" alt="VYNDR recap" style="max-width:100%;border-radius:12px;display:block"/></p>` : ''}
<p style="margin-top:24px">
<a href="${esc(SITE)}/ledger"
style="display:inline-block;padding:12px 24px;background:#1A5A42;color:#E8FFF4;text-decoration:none;border-radius:12px;font-weight:600">
See the full Ledger →
</a>
</p>`;
const text =
`Last night: ${accuracy}% — ${hits} of ${total} grades hit
${resultsData.date || ''}
${entries.slice(0, 8).map((e) => `${e.result === 'hit' ? 'HIT' : 'MISS'}\t${e.player}\t${e.stat || ''} ${String(e.direction || '').toUpperCase()} ${e.line ?? ''}\t${e.grade}`).join('\n')}
Full Ledger: ${SITE}/ledger
VYNDR · vyndr.app · @getvyndr
Reply STOP to unsubscribe.`;
return { subject, html: htmlShell(body), text };
}
// ── Cascade alert — real-time ────────────────────────────────────────────
function buildCascadeAlertEmail(cascadeData = {}) {
const trigger = cascadeData.trigger_detail || cascadeData.detail || {};
const headline = trigger.player && trigger.status
? `${trigger.player} ${trigger.status}`
: (trigger.summary || cascadeData.trigger_type || 'event');
const count = Array.isArray(cascadeData.affected_props) ? cascadeData.affected_props.length : (cascadeData.affected_count ?? 0);
const subject = `🚨 ${headline}${count} prop${count === 1 ? '' : 's'} affected`;
const rows = (cascadeData.affected_props || []).slice(0, 6).map((p) => `
<tr>
<td style="padding:10px 0;color:#E8E8F0;font-weight:600">${esc(p.player)}</td>
<td style="padding:10px 0;color:#7A7A8E;font-family:'IBM Plex Mono',monospace;font-size:13px">${esc(`${p.stat || ''} ${String(p.direction || '').toUpperCase()} ${p.line ?? ''}`)}</td>
<td style="padding:10px 0;color:#7A7A8E;font-family:'IBM Plex Mono',monospace;font-weight:700;text-align:right">${esc(p.old_grade || '—')} → <span style="color:#00D4A0">${esc(p.new_grade || '—')}</span></td>
</tr>`).join('');
const body = `
<p style="font-family:'IBM Plex Mono',monospace;font-size:13px;letter-spacing:4px;color:#FFB347;text-transform:uppercase;margin-bottom:8px">CASCADE · ${esc((cascadeData.trigger_type || 'event').toUpperCase())}</p>
<p style="font-size:18px;font-weight:700;margin:0">${esc(headline)}${count} prop${count === 1 ? '' : 's'} affected.</p>
<table style="width:100%;border-collapse:collapse;margin:16px 0">${rows}</table>
${cascadeData.imageUrl ? `<p style="margin:16px 0"><img src="${esc(cascadeData.imageUrl)}" alt="VYNDR cascade" style="max-width:100%;border-radius:12px;display:block"/></p>` : ''}
<p style="margin-top:20px">
<a href="${esc(SITE)}/dashboard"
style="display:inline-block;padding:12px 24px;background:#1A5A42;color:#E8FFF4;text-decoration:none;border-radius:12px;font-weight:600">
See the updated grades →
</a>
</p>`;
const text =
`${headline}${count} prop${count === 1 ? '' : 's'} affected.
${(cascadeData.affected_props || []).slice(0, 6).map((p) => `${p.player}\t${p.stat || ''} ${String(p.direction || '').toUpperCase()} ${p.line ?? ''}\t${p.old_grade || '—'}${p.new_grade || '—'}`).join('\n')}
Updated grades: ${SITE}/dashboard
VYNDR · vyndr.app · @getvyndr
Reply STOP to unsubscribe.`;
return { subject, html: htmlShell(body), text };
}
module.exports = { buildCheatsheetEmail, buildResultsEmail, buildCascadeAlertEmail };