← back to Ventura Corridor
public/scoreboard.html
142 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Scoreboard · 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;0,500;1,400;1,500&family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@200;300;400&display=swap');
:root{--noir:#0a0a0c;--noir-rise:#131316;--ink:#f0ece2;--ink-mute:#888475;--metal:#b89968;--metal-glow:#d4b683;--green:#6a9b73;--rule:#2a2622}
*{box-sizing:border-box}
html,body{margin:0;background:var(--noir);color:var(--ink);font-family:'Inter',sans-serif;font-weight:300;min-height:100vh}
header{padding:48px 24px 24px;text-align:center;border-bottom:1px solid var(--rule)}
header .kicker{font-size:10px;letter-spacing:.4em;text-transform:uppercase;color:var(--metal);font-weight:500}
header h1{font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:500;font-size:54px;margin:8px 0 0;color:var(--ink)}
header h1 em{color:var(--metal-glow);font-style:normal}
nav{padding:12px 24px;text-align:center;border-bottom:1px solid var(--rule);font-size:9px;letter-spacing:.22em;text-transform:uppercase}
nav a{color:var(--ink-mute);margin:0 10px;text-decoration:none;padding:4px 0}
nav a:hover{color:var(--metal)}
main{display:grid;grid-template-columns:repeat(auto-fit,minmax(360px,1fr));gap:24px;padding:32px;max-width:1400px;margin:0 auto}
.board{background:var(--noir-rise);border:1px solid var(--rule);padding:20px 22px}
.board h2{font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:500;font-size:22px;color:var(--metal);margin:0 0 4px}
.board .deck{font-size:9px;letter-spacing:.26em;color:var(--ink-mute);text-transform:uppercase;margin-bottom:14px}
.board ol{list-style:none;padding:0;margin:0;counter-reset:rank}
.board li{counter-increment:rank;display:grid;grid-template-columns:auto 1fr auto;gap:12px;padding:8px 0;border-bottom:1px dotted var(--rule);align-items:center}
.board li:last-child{border-bottom:none}
.board li::before{content:counter(rank,decimal-leading-zero);font-family:'Cormorant Garamond',serif;font-style:italic;font-size:18px;color:var(--metal);min-width:30px}
.board li a{color:var(--ink);text-decoration:none}
.board li a:hover{color:var(--metal-glow)}
.board li .name{font-family:'Cormorant Garamond',serif;font-size:15px;color:var(--ink-mute);font-style:italic}
.board li .v{font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--metal-glow);text-align:right}
.empty{padding:24px;text-align:center;color:var(--ink-mute);font-style:italic}
</style>
</head>
<body>
<header>
<div class="kicker">The Corridor · Newsroom</div>
<h1>Score<em>board</em></h1>
</header>
<nav>
<a href="/today.html">today</a>
<a href="/magazine.html">magazine</a>
<a href="/issue">public issue</a>
<a href="/scoreboard.html" style="color:var(--metal);border-bottom:1px solid var(--metal)">scoreboard</a>
<a href="/buildings.html">buildings</a>
<a href="/api/magazine.csv" download style="color:var(--metal-glow);border:1px solid var(--rule);padding:3px 9px">⬇ CSV</a>
</nav>
<main id="boards"><div class="empty">Loading…</div></main>
<section style="max-width:1100px;margin:0 auto 60px;padding:0 24px">
<div class="board" style="background:var(--noir-rise);border:1px solid var(--rule);padding:20px 22px">
<h2 style="font-family:var(--serif);font-style:italic;font-weight:500;font-size:22px;color:var(--metal);margin:0 0 4px">$ Ad-signal heatmap by vertical</h2>
<div style="font-size:9px;letter-spacing:.26em;color:var(--ink-mute);text-transform:uppercase;margin-bottom:14px">which categories already buy ads · monetization targets</div>
<table style="width:100%;border-collapse:collapse;font-size:12px">
<thead><tr style="border-bottom:1px solid var(--rule)">
<th style="text-align:left;padding:6px 0;color:var(--ink-mute);font-size:9px;letter-spacing:.22em;text-transform:uppercase;font-weight:400">Vertical</th>
<th style="text-align:right;padding:6px 8px;color:var(--ink-mute);font-size:9px;letter-spacing:.22em;text-transform:uppercase;font-weight:400">Features</th>
<th style="text-align:right;padding:6px 8px;color:var(--ink-mute);font-size:9px;letter-spacing:.22em;text-transform:uppercase;font-weight:400">Sponsors</th>
<th style="text-align:right;padding:6px 8px;color:var(--ink-mute);font-size:9px;letter-spacing:.22em;text-transform:uppercase;font-weight:400">% sponsor</th>
<th style="text-align:right;padding:6px 8px;color:var(--ink-mute);font-size:9px;letter-spacing:.22em;text-transform:uppercase;font-weight:400">Google</th>
<th style="text-align:right;padding:6px 8px;color:var(--ink-mute);font-size:9px;letter-spacing:.22em;text-transform:uppercase;font-weight:400">Meta</th>
<th style="text-align:right;padding:6px 8px;color:var(--ink-mute);font-size:9px;letter-spacing:.22em;text-transform:uppercase;font-weight:400">TikTok</th>
<th style="text-align:right;padding:6px 8px;color:var(--ink-mute);font-size:9px;letter-spacing:.22em;text-transform:uppercase;font-weight:400">LinkedIn</th>
</tr></thead>
<tbody id="heatmap-rows"></tbody>
</table>
</div>
</section>
<script>
function escHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); }
const CATEGORY_EMOJI = {
restaurant: '🍴', food: '🍴', cafe: '☕', bar: '🍷', bakery: '🥐',
professional: '⚖️', legal: '⚖️', accounting: '📒', insurance: '📑', 'real-estate': '🏠',
medical: '🩺', dental: '🦷', wellness: '🌿', therapy: '🧘',
beauty: '✨', salon: '💇', barbershop: '💈', spa: '🛁', nails: '💅',
fitness: '💪', yoga: '🧘♀️', studio: '🎬',
shop: '🛍', boutique: '👗', jewelry: '💍', florist: '💐', furniture: '🛋', hardware: '🔧',
hospitality: '🏨', hotel: '🏨', event: '🎉',
pet: '🐶', auto: '🚗', cleaning: '🧼', creative: '🎨', music: '🎵',
feature: '✦', uncategorized: '✦', misc: '✦',
};
function emojiForCat(c) { return CATEGORY_EMOJI[String(c || '').toLowerCase()] || '✦'; }
async function load() {
const d = await fetch('/api/magazine/scoreboard').then(r => r.json());
const boards = [
{
title: 'Most viewed', deck: 'reader engagement', rows: d.top_viewed,
render: r => `<a href="/magazine/${r.id}">${escHtml(r.headline || r.biz_name)}</a> <span class="name">${escHtml(r.biz_name)}</span><span class="v">${r.views} 👁</span>`
},
{
title: 'Hottest verticals', deck: 'feature counts by category', rows: d.hot_verticals,
render: r => `<span><span style="margin-right:8px;font-size:16px">${emojiForCat(r.category_tag)}</span><span style="text-transform:capitalize">${escHtml((r.category_tag || 'uncategorized').replace(/-/g, ' '))}</span></span><span></span><span class="v">${r.n}</span>`
},
{
title: 'Sponsor candidates', deck: 'paid-ad runners worth pitching', rows: d.sponsor_candidates,
render: r => `<a href="/magazine/${r.id}">${escHtml(r.headline || r.biz_name)}</a><span class="name">${escHtml(r.biz_name)}</span><span class="v">$ ${r.ads}× ads</span>`
},
{
title: 'Top tapped', deck: 'features driving outbound clicks', rows: d.top_tapped,
render: r => {
const breakdown = Object.entries(r.taps_breakdown || {}).map(([k, v]) => `${k}:${v}`).join(' ');
return `<a href="/magazine/${r.id}">${escHtml(r.headline || r.biz_name)}</a><span class="name">${escHtml(breakdown)}</span><span class="v">↗ ${r.taps}</span>`;
}
},
{
title: 'Latest features', deck: 'fresh off the press', rows: d.latest,
render: r => `<a href="/magazine/${r.id}">${escHtml(r.headline || r.biz_name)}</a><span class="name">${emojiForCat(r.category_tag)} ${escHtml((r.category_tag || '').replace(/-/g,' '))}</span><span class="v">${new Date(r.generated_at).toLocaleDateString('en-US',{month:'short',day:'numeric'})}</span>`
},
{
title: 'Longest editorial', deck: 'qwen3 ran with the prompt', rows: d.longest_editorial,
render: r => `<a href="/magazine/${r.id}">${escHtml(r.headline || r.biz_name)}</a><span class="name">${escHtml(r.biz_name)}</span><span class="v">${r.len}c</span>`
},
];
document.getElementById('boards').innerHTML = boards.map(b => `
<section class="board">
<h2>${escHtml(b.title)}</h2>
<div class="deck">${escHtml(b.deck)}</div>
${(b.rows && b.rows.length) ? `<ol>${b.rows.map(r => `<li>${b.render(r)}</li>`).join('')}</ol>` : `<div class="empty">No data yet.</div>`}
</section>
`).join('');
}
async function loadHeatmap() {
const d = await fetch('/api/magazine/ad-heatmap').then(r => r.json()).catch(() => ({rows:[]}));
const cell = (n, hi) => `<td style="text-align:right;padding:6px 8px;font-family:var(--mono);${Number(n) >= hi ? 'color:var(--metal-glow);font-weight:500' : 'color:var(--ink-mute)'}">${n || '·'}</td>`;
document.getElementById('heatmap-rows').innerHTML = (d.rows || []).map(r => `
<tr style="border-bottom:1px dotted var(--rule)">
<td style="padding:8px 0;font-family:var(--serif);font-size:14px;color:var(--metal);text-transform:capitalize">${escHtml((r.vertical || '').replace(/-/g,' '))}</td>
<td style="text-align:right;padding:8px;font-family:var(--mono);color:var(--ink)">${r.features}</td>
<td style="text-align:right;padding:8px;font-family:var(--mono);color:var(--metal-glow);font-weight:500">${r.sponsor_candidates}</td>
<td style="text-align:right;padding:8px;font-family:var(--mono);color:${Number(r.sponsor_rate_pct) >= 30 ? 'var(--metal-glow)' : 'var(--ink-mute)'}">${r.sponsor_rate_pct ?? '·'}${r.sponsor_rate_pct ? '%' : ''}</td>
${cell(r.google_ads, 5)}${cell(r.meta_pixel, 5)}${cell(r.tiktok_pixel, 3)}${cell(r.linkedin_insight, 3)}
</tr>`).join('') || '<tr><td colspan="8" style="padding:20px;text-align:center;color:var(--ink-mute)">No data yet.</td></tr>';
}
load();
loadHeatmap();
setInterval(load, 60_000);
setInterval(loadHeatmap, 60_000);
</script>
</body>
</html>