← back to Ventura Corridor
public/health.html
147 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Health · The Corridor</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;1,400;1,500&family=Inter:wght@200;300;400;500&family=JetBrains+Mono:wght@200;300&display=swap');
:root{--noir:#0a0a0c;--noir-rise:#131316;--ink:#f0ece2;--ink-mute:#888475;--metal:#b89968;--metal-glow:#d4b683;--green:#6a9b73;--red:#b66565;--yellow:#c9a942;--rule:#2a2622}
*{box-sizing:border-box}body{margin:0;background:var(--noir);color:var(--ink);font-family:Inter,sans-serif;font-weight:300}
header{padding:30px 32px 16px;border-bottom:1px solid var(--rule)}
header h1{font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:400;font-size:32px;margin:0}
header h1 em{color:var(--metal)}
header .sub{font-size:11px;letter-spacing:.26em;text-transform:uppercase;color:var(--ink-mute);margin-top:4px}
nav{padding:10px 32px;border-bottom:1px solid var(--rule);display:flex;gap:12px;flex-wrap:wrap;font-size:9px;letter-spacing:.26em;text-transform:uppercase}
nav a{color:var(--ink-mute);text-decoration:none;padding:5px 10px;border:1px solid var(--rule)}
nav a:hover, nav a.active{color:var(--metal);border-color:var(--metal)}
main{padding:32px;max-width:1200px;margin:0 auto;display:grid;grid-template-columns:repeat(auto-fit,minmax(360px,1fr));gap:24px}
.panel{background:var(--noir-rise);border:1px solid var(--rule);padding:24px}
.panel.ok{border-left:3px solid var(--green)}
.panel.warn{border-left:3px solid var(--yellow)}
.panel.err{border-left:3px solid var(--red)}
.panel h3{font-family:'Cormorant Garamond',serif;font-style:italic;color:var(--metal);font-size:24px;margin:0 0 4px;font-weight:500}
.panel .what{font-size:9px;letter-spacing:.32em;text-transform:uppercase;color:var(--ink-mute);margin-bottom:14px}
.row{display:grid;grid-template-columns:1fr auto;gap:14px;padding:6px 0;border-bottom:1px dotted var(--rule);font-size:13px;font-family:'JetBrains Mono',monospace;align-items:baseline}
.row .k{color:var(--ink)}
.row .v{color:var(--metal-glow);font-weight:500}
.row .v.ok::before{content:'✓ ';color:var(--green)}
.row .v.warn::before{content:'⚠ ';color:var(--yellow)}
.row .v.err::before{content:'✕ ';color:var(--red)}
.summary{grid-column:1/-1;text-align:center;padding:18px;background:var(--noir-rise);border:1px solid var(--rule);font-family:'Cormorant Garamond',serif;font-style:italic;font-size:24px}
.summary.ok{color:var(--green)}
.summary.warn{color:var(--yellow)}
.summary.err{color:var(--red)}
.last-up{font-size:11px;color:var(--ink-mute);font-family:'JetBrains Mono',monospace;text-align:right;padding:0 32px 24px;font-style:normal}
</style>
</head>
<body>
<header>
<h1>Health <em>check</em></h1>
<div class="sub">whole-pipeline status · refreshed every 30s</div>
</header>
<nav>
<a href="/today.html">today</a>
<a href="/magazine.html">magazine</a>
<a href="/issue">issue</a>
<a href="/coverage.html">coverage</a>
<a href="/health.html" class="active">health</a>
</nav>
<main id="root"><div class="summary">Loading…</div></main>
<div class="last-up" id="last-up"></div>
<script>
function fmt(n) { return Number(n).toLocaleString('en-US'); }
function escHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); }
async function load() {
let q, c, h, gen, sb, fb, inq;
try {
[q, c, h, gen, sb, fb, inq] = await Promise.all([
fetch('/api/magazine/quality-flags').then(r => r.json()),
fetch('/api/coverage').then(r => r.json()),
fetch('/api/coverage/history').then(r => r.json()),
fetch('/api/magazine/gen-queue?limit=1').then(r => r.json()),
fetch('/api/magazine/scoreboard').then(r => r.json()),
fetch('/api/feedback').then(r => r.json()),
fetch('/api/sponsor/inquiries').then(r => r.json()),
]);
} catch (e) {
document.getElementById('root').innerHTML = `<div class="summary err">cannot reach API: ${escHtml(e.message)}</div>`;
return;
}
const lastSnap = (h.snapshots || []).at(-1);
const sinceLastSnap = lastSnap ? ((Date.now() - new Date(lastSnap.taken_at).getTime()) / 3600000) : null;
const fbUnread = (fb.rows || []).filter(r => !r.reviewed).length;
const inqOpen = (inq.rows || []).filter(r => r.status === 'new').length;
const queueDepth = (gen.queue || []).length === 0 ? 'empty' : `${(gen.queue || []).length}+ waiting`;
const checks = [
{ ok: c.totals.published >= 1, k: 'Published features', v: fmt(c.totals.published) },
{ ok: c.totals.draft >= 0, k: 'Draft features', v: fmt(c.totals.draft) },
{ ok: c.totals.reviewed >= 0, k: 'Reviewed (awaiting publish)', v: fmt(c.totals.reviewed) },
{ ok: q.count === 0, k: 'Quality flags', v: q.count === 0 ? 'none' : `${q.count} issues`, status: q.count === 0 ? 'ok' : 'warn' },
{ ok: c.totals.total_biz >= 1000, k: 'Businesses indexed', v: fmt(c.totals.total_biz) },
{ ok: queueDepth !== 'empty', k: 'Gen queue', v: queueDepth, status: queueDepth === 'empty' ? 'warn' : 'ok' },
{ ok: lastSnap && sinceLastSnap < 26, k: 'Last coverage snapshot', v: sinceLastSnap == null ? 'never' : `${sinceLastSnap.toFixed(1)}h ago`, status: sinceLastSnap == null ? 'warn' : sinceLastSnap < 26 ? 'ok' : 'warn' },
{ ok: fbUnread === 0, k: 'Reader feedback unread', v: fbUnread === 0 ? '0' : `${fbUnread} unread`, status: fbUnread === 0 ? 'ok' : 'warn' },
{ ok: inqOpen === 0, k: 'Sponsor inquiries open', v: inqOpen === 0 ? '0' : `${inqOpen} open`, status: inqOpen === 0 ? 'ok' : 'warn' },
];
const overall = checks.every(x => (x.status || (x.ok ? 'ok' : 'err')) === 'ok')
? 'ok' : checks.some(x => (x.status || (x.ok ? 'ok' : 'err')) === 'err') ? 'err' : 'warn';
const overallText = overall === 'ok' ? 'All systems normal.' : overall === 'warn' ? 'Some attention items.' : 'Something is broken.';
let html = `<div class="summary ${overall}">${escHtml(overallText)}</div>`;
html += `<div class="panel ${overall}">
<h3>Pipeline</h3>
<div class="what">core counts · queues · flags</div>
${checks.map(c => {
const status = c.status || (c.ok ? 'ok' : 'err');
return `<div class="row"><span class="k">${escHtml(c.k)}</span><span class="v ${status}">${escHtml(c.v)}</span></div>`;
}).join('')}
</div>`;
// Top boards
const top = (sb.top_tapped || []).slice(0, 5);
html += `<div class="panel ok">
<h3>Top tapped this week</h3>
<div class="what">most outbound clicks</div>
${top.length === 0 ? '<div style="font-style:italic;color:var(--ink-mute);font-family:Cormorant Garamond,serif">no taps yet</div>' :
top.map(t => `<div class="row"><span class="k"><a href="/magazine/${t.id}" style="color:var(--ink);text-decoration:none">${escHtml((t.headline || '').slice(0, 40))}</a></span><span class="v">${t.taps} taps</span></div>`).join('')}
</div>`;
// Coverage growth
const snaps = h.snapshots || [];
if (snaps.length >= 2) {
const first = snaps[0], last = snaps[snaps.length - 1];
const days = (new Date(last.taken_at) - new Date(first.taken_at)) / 86400000;
const grew = Number(last.total_features) - Number(first.total_features);
const rate = days > 0 ? (grew / days).toFixed(1) : '—';
html += `<div class="panel ok">
<h3>Growth</h3>
<div class="what">since first snapshot</div>
<div class="row"><span class="k">Days observed</span><span class="v">${days.toFixed(1)}</span></div>
<div class="row"><span class="k">Features added</span><span class="v">+${grew}</span></div>
<div class="row"><span class="k">Avg per day</span><span class="v">${rate}</span></div>
<div class="row"><span class="k">Snapshots taken</span><span class="v">${snaps.length}</span></div>
</div>`;
} else {
html += `<div class="panel warn">
<h3>Growth</h3>
<div class="what">need ≥2 snapshots for trend</div>
<div class="row"><span class="k">Snapshots taken</span><span class="v">${snaps.length}</span></div>
<div class="row"><span class="k">Next snapshot</span><span class="v">11:55 PM</span></div>
</div>`;
}
document.getElementById('root').innerHTML = html;
document.getElementById('last-up').textContent = `last refresh ${new Date().toLocaleTimeString()}`;
}
load();
setInterval(load, 30_000);
</script>
</body>
</html>