← 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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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>