← back to Ventura Corridor
public/today.html
843 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Today · DW · Ventura Corridor</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<script>(function(){try{var t=localStorage.getItem('vc-theme');document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(e){document.documentElement.dataset.theme='dark';}})();</script>
<link rel="stylesheet" href="/theme.css">
<script src="/theme.js" defer></script>
<style>
:root {
--noir: #0a0a0c; --noir-rise: #131316; --ink: #f0ece2;
--ink-mute: #888475; --metal: #b89968; --metal-glow: #d4b683;
--rule: #2a2622;
--serif: 'Cormorant Garamond', 'Times New Roman', serif;
--sans: 'Inter', -apple-system, system-ui, sans-serif;
--mono: 'JetBrains Mono', ui-monospace, monospace;
}
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;1,300;1,400&family=Inter:wght@100;200;300;400;600&family=JetBrains+Mono:wght@200;300;400&display=swap');
html, body { margin: 0; background: var(--noir); color: var(--ink); font-family: var(--sans); font-weight: 300; }
* { box-sizing: border-box; }
header { position: sticky; top: 0; z-index: 10;
background: linear-gradient(180deg, rgba(10,10,12,.97) 70%, rgba(10,10,12,0));
padding: 22px 32px 16px; border-bottom: 1px solid var(--rule);
display: flex; justify-content: space-between; align-items: baseline; gap: 24px; flex-wrap: wrap; }
header h1 { font-family: var(--serif); font-weight: 400; font-size: 28px; margin: 0; letter-spacing: -0.01em; }
header h1 em { font-style: italic; color: var(--metal); }
.sub { font-size: 11px; letter-spacing: .26em; text-transform: uppercase; color: var(--ink-mute); margin-top: 2px; }
nav.tabs { display: flex; gap: 4px; flex-wrap: wrap; }
nav.tabs a { font-size: 9px; letter-spacing: .26em; text-transform: uppercase;
color: var(--ink-mute); text-decoration: none; padding: 5px 10px; border: 1px solid var(--rule); }
nav.tabs a.active, nav.tabs a:hover { color: var(--metal); border-color: var(--metal); }
.day-hero {
padding: 36px 32px 24px;
background: radial-gradient(ellipse at top, rgba(184,153,104,0.08), transparent 70%);
border-bottom: 1px solid var(--rule);
}
.day-hero .date {
font-family: var(--mono); font-size: 11px; letter-spacing: .3em; color: var(--metal);
text-transform: uppercase;
}
.day-hero h2 {
font-family: var(--serif); font-style: italic; font-weight: 300;
font-size: 48px; line-height: 1.05; margin: 8px 0 4px;
letter-spacing: -0.02em;
}
.day-hero h2 em { color: var(--metal); font-style: normal; }
.day-hero p { color: var(--ink-mute); font-size: 13px; max-width: 700px; line-height: 1.5; margin: 12px 0 0; }
.stack { padding: 24px 32px; max-width: 1100px; }
section.card {
margin-bottom: 24px; padding: 22px 26px;
background: var(--noir-rise); border: 1px solid var(--rule);
transition: border-color 200ms ease;
}
section.card:hover { border-color: var(--metal); }
section.card .head {
display: flex; align-items: baseline; justify-content: space-between; gap: 16px; flex-wrap: wrap;
margin-bottom: 14px;
}
section.card .head .num {
font-family: var(--serif); font-style: italic; font-weight: 400;
font-size: 56px; line-height: 0.9; color: var(--metal);
font-variant-numeric: tabular-nums;
}
section.card .head .num em { font-style: normal; font-size: 12px; letter-spacing: .26em; text-transform: uppercase; color: var(--ink-mute); display: block; margin-top: 4px; }
section.card .head .title {
font-family: var(--serif); font-style: italic; font-weight: 400; font-size: 22px; color: var(--ink);
flex: 1;
}
section.card .head .title small { display: block; font-family: var(--sans); font-style: normal; font-size: 10px; letter-spacing: .2em; color: var(--ink-mute); text-transform: uppercase; margin-top: 4px; }
section.card .head .actions a, section.card .head .actions button {
font-size: 9px; letter-spacing: .26em; text-transform: uppercase;
color: var(--metal); text-decoration: none; padding: 6px 12px;
border: 1px solid var(--metal); background: transparent; cursor: pointer;
margin-left: 6px; font-family: inherit;
}
section.card .head .actions a:hover, section.card .head .actions button:hover { background: rgba(184,153,104,0.06); }
.item {
display: grid; grid-template-columns: auto 1fr auto; gap: 12px;
padding: 10px 0; border-top: 1px solid var(--rule); align-items: center;
font-size: 12px;
}
.item:first-child { border-top: 0; }
.item .ico { font-size: 16px; color: var(--metal); width: 24px; text-align: center; }
.item .nm { color: var(--ink); }
.item .nm small { display: block; color: var(--ink-mute); font-size: 10px; margin-top: 2px; font-family: var(--mono); letter-spacing: .08em; }
.item .acts a, .item .acts button {
font-size: 9px; letter-spacing: .2em; text-transform: uppercase;
color: var(--ink-mute); text-decoration: none; padding: 4px 8px;
border: 1px solid var(--rule); background: transparent; cursor: pointer; margin-left: 4px; font-family: inherit;
}
.item .acts a:hover, .item .acts button:hover { color: var(--metal); border-color: var(--metal); }
.empty { padding: 24px; text-align: center; font-style: italic; color: var(--ink-mute); font-size: 12px; }
.totals { display: flex; gap: 20px; flex-wrap: wrap; margin-top: 12px; font-size: 10px; letter-spacing: .15em; color: var(--ink-mute); text-transform: uppercase; }
.totals span b { color: var(--metal); font-family: var(--serif); font-style: italic; font-size: 18px; margin-right: 6px; font-variant-numeric: tabular-nums; }
</style>
</head>
<body>
<header>
<div>
<h1>Today · <em>DW pipeline</em></h1>
<div class="sub">your one-page action plan <span id="freshness-pill" style="margin-left:14px;font-size:9px;letter-spacing:.18em;padding:2px 8px;border:1px solid var(--rule);color:var(--ink-mute)" title="Click for ingest details">data: —</span></div>
</div>
<nav class="tabs">
<a href="/">map</a>
<a href="/atlas.html">atlas</a>
<a href="/pitches.html">pitches</a>
<a href="/linkedin.html">linkedin</a>
<a href="/pitches-map.html">map</a>
<a href="/walk-route.html">walk route</a>
<a href="/responses.html">responses</a>
<a href="/postcards.html">postcards</a>
<a href="/buildings.html">buildings</a>
<a href="/magazine.html" style="color:var(--metal-glow)">📖 magazine</a>
<a href="/issue" style="color:var(--accent,#a8763a)">★ issue</a>
<a href="/scoreboard.html">🏆 score</a>
<a href="/feedback.html" id="nav-feedback" style="color:var(--ink-mute)">✎ feedback</a>
<a href="/admin/ig-drafts/" target="_blank">📷 ig drafts</a>
<a href="/standup">🎙 standup</a>
<a href="/cover-picker.html">⛧ cover</a>
<a href="/coverage.html">▰ coverage</a>
<a href="/magazine.html?quality=1" id="nav-quality" style="color:var(--ink-mute)">🔧 quality</a>
<a href="/health.html">♥ health</a>
<a href="/duplicates.html" id="nav-dupes" style="color:var(--ink-mute)">⊕ dupes</a>
<a href="/calendar.html">▦ calendar</a>
<a href="/crawl-derby.html">🏇 derby</a>
<a href="/today.html" class="active">today</a>
<span class="theme-toggle-host" style="display:inline-flex;align-items:center;margin-left:6px"></span>
</nav>
</header>
<section style="padding:14px 32px;border-bottom:1px solid var(--rule);background:rgba(184,153,104,0.03);overflow-x:auto;white-space:nowrap" id="mag-activity-rail">
<span style="font-family:var(--serif);font-style:italic;color:var(--metal);font-size:14px;margin-right:14px">The Corridor · just now:</span>
<span id="mag-activity-strip" style="font-family:var(--mono);font-size:11px;color:var(--ink-mute)">…</span>
</section>
<section style="padding:14px 32px;border-bottom:1px solid var(--rule);background:rgba(184,153,104,0.02);overflow-x:auto;white-space:nowrap" id="gen-queue-rail">
<span style="font-family:var(--serif);font-style:italic;color:var(--metal);font-size:14px;margin-right:14px">Up next on the gen queue:</span>
<span id="gen-queue-strip" style="font-family:var(--mono);font-size:11px;color:var(--ink-mute)">…</span>
</section>
<section style="padding:14px 32px;border-bottom:1px solid var(--rule);background:rgba(212,182,131,0.05);display:none" id="draft-pick-rail">
<span style="font-family:var(--serif);font-style:italic;color:var(--metal-glow);font-size:14px;margin-right:14px">Publish next:</span>
<a id="draft-pick-link" href="#" style="font-family:var(--serif);font-style:italic;color:var(--ink);font-size:15px;text-decoration:none">…</a>
<span id="draft-pick-meta" style="font-family:var(--mono);font-size:10px;color:var(--ink-mute);margin-left:14px">…</span>
</section>
<section class="day-hero">
<div class="date" id="today-date">—</div>
<h2>Three things, <em>in order</em>.</h2>
<p>Walk first (zero cost, highest conversion). LinkedIn DMs second (warm 1st-degree, 20-25 max for safe rate). Postcards third (cold, paid, scale). Skip whatever doesn't fit your day — pipeline doesn't care if you skip a tier.</p>
<div class="totals">
<span><b id="t-walk">—</b>walk targets in queue</span>
<span><b id="t-linkedin">—</b>linkedin matches</span>
<span><b id="t-postcard">—</b>postcard ready</span>
<span><b id="t-research">—</b>need research</span>
</div>
</section>
<section style="padding:20px 32px;border-bottom:1px solid var(--rule);background:radial-gradient(ellipse at top right, rgba(212,182,131,0.05), transparent 70%)">
<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:12px;flex-wrap:wrap;gap:10px">
<h3 style="font-family:var(--serif);font-style:italic;font-weight:400;font-size:22px;color:var(--metal-glow);margin:0">Walk here next</h3>
<a href="/buildings.html" style="color:var(--metal);text-decoration:none;font-size:10px;letter-spacing:.18em;text-transform:uppercase;border:1px solid var(--rule);padding:4px 10px">Full list ↗</a>
</div>
<div id="prio-strip" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:8px"></div>
</section>
<section id="fotd-section" style="display:none;padding:24px 32px;border-bottom:1px solid var(--rule);background:linear-gradient(135deg,rgba(212,182,131,0.06),transparent 70%)">
<div style="display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap;gap:10px">
<h3 style="font-family:var(--serif);font-style:italic;font-weight:400;font-size:22px;color:var(--metal-glow);margin:0">📜 Feature of the day</h3>
<a id="fotd-link" href="/magazine.html" style="color:var(--metal);text-decoration:none;font-size:10px;letter-spacing:.18em;text-transform:uppercase;border:1px solid var(--rule);padding:4px 10px">Open ↗</a>
</div>
<div id="fotd-body" style="margin-top:14px"></div>
</section>
<section id="inq-section" style="display:none;padding:20px 32px;border-bottom:1px solid var(--rule);background:linear-gradient(135deg,rgba(106,58,26,0.08),transparent 60%)">
<div style="display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap;gap:10px">
<h3 style="font-family:var(--serif);font-style:italic;font-weight:400;font-size:22px;color:var(--accent,#a8763a);margin:0">⭐ Sponsor inquiries · last 7</h3>
<span id="inq-stats" style="font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-mute)">—</span>
</div>
<div id="inq-list" style="margin-top:12px;display:flex;flex-direction:column;gap:6px"></div>
</section>
<section id="fotw-section" style="display:none;padding:18px 32px;border-bottom:1px solid var(--rule);background:radial-gradient(ellipse at left, rgba(106,58,26,0.06), transparent 70%)">
<div style="display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap;gap:10px">
<h3 style="font-family:var(--serif);font-style:italic;font-weight:400;font-size:18px;color:var(--accent,#a8763a);margin:0">📰 Feature of the week</h3>
<a id="fotw-link" href="/magazine.html" style="color:var(--metal);text-decoration:none;font-size:10px;letter-spacing:.18em;text-transform:uppercase;border:1px solid var(--rule);padding:3px 8px">Open ↗</a>
</div>
<div id="fotw-body" style="margin-top:8px;font-family:var(--serif);font-style:italic;font-size:14px;color:var(--ink-mute);line-height:1.5"></div>
</section>
<section id="magazine-section" style="padding:20px 32px;border-bottom:1px solid var(--rule);background:rgba(184,153,104,0.04)">
<div style="display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap;gap:10px">
<h3 style="font-family:var(--serif);font-style:italic;font-weight:400;font-size:22px;color:var(--metal-glow);margin:0">📖 The Corridor <span style="font-size:11px;letter-spacing:.18em;color:var(--ink-mute);margin-left:8px">corridor magazine</span></h3>
<a href="/magazine.html" style="color:var(--metal);text-decoration:none;font-size:10px;letter-spacing:.18em;text-transform:uppercase;border:1px solid var(--rule);padding:4px 10px">Open issue ↗</a>
</div>
<div id="magazine-stats" style="margin-top:10px;font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-mute)">—</div>
<div id="magazine-progress" style="margin-top:8px;height:6px;background:var(--noir);border:1px solid var(--rule);overflow:hidden">
<div id="magazine-bar" style="height:100%;background:var(--metal-glow);width:0%;transition:width 0.6s"></div>
</div>
</section>
<section id="stale-section" style="display:none;padding:20px 32px;border-bottom:1px solid var(--rule);background:rgba(182,101,101,0.05)">
<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:12px;flex-wrap:wrap;gap:10px">
<h3 style="font-family:var(--serif);font-style:italic;font-weight:400;font-size:22px;color:#c47b7b;margin:0">Cold leads · re-touch or close</h3>
<span id="stale-summary" style="font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-mute)">—</span>
</div>
<div id="stale-list" style="display:flex;flex-direction:column;gap:6px"></div>
</section>
<section class="trend" style="padding:24px 32px;border-bottom:1px solid var(--rule);background:var(--noir-rise)">
<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:16px;flex-wrap:wrap;gap:12px">
<h3 style="font-family:var(--serif);font-style:italic;font-weight:400;font-size:22px;color:var(--metal);margin:0">14-day corridor velocity</h3>
<div id="trend-stats" style="font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-mute)">—</div>
</div>
<svg id="trend-chart" viewBox="0 0 800 160" preserveAspectRatio="none" style="width:100%;height:160px;background:var(--noir);border:1px solid var(--rule)"></svg>
<div id="trend-legend" style="display:flex;gap:16px;flex-wrap:wrap;margin-top:12px;font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-mute)"></div>
</section>
<section style="padding:24px 32px;border-bottom:1px solid var(--rule);background:var(--noir-rise)">
<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:16px;flex-wrap:wrap;gap:12px">
<h3 style="font-family:var(--serif);font-style:italic;font-weight:400;font-size:22px;color:var(--metal);margin:0">Recent activity</h3>
<div id="activity-stats" style="font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-mute)">—</div>
</div>
<div id="activity-chips" style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px"></div>
<div id="activity-feed" style="display:flex;flex-direction:column;gap:6px"></div>
<div style="margin-top:12px;font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-mute)">
auto-refresh every 60s · click any row to open the pitch
</div>
</section>
<div class="stack">
<section class="card">
<div class="head">
<div class="num">1<em>walk-in</em></div>
<div class="title">Walk over · same building + same block + 5-min walk
<small>highest conversion · zero cost · take a sample book</small></div>
<div class="actions">
<a href="/pitches.html?prox=walking">All walks ↗</a>
</div>
</div>
<div id="walk-list"></div>
</section>
<section class="card" id="inmail-card" style="display:none">
<div class="head">
<div class="num" id="inmail-num">—<em>inmail</em></div>
<div class="title">InMail credits available · Sales Navigator
<small id="inmail-sub">paid messages to non-connections · highest reply rate</small></div>
<div class="actions">
<a href="/sales-nav-signup.html">Manage subscription ↗</a>
<a href="/pitches.html?status=approved">Pick targets ↗</a>
</div>
</div>
<div id="inmail-detail"></div>
</section>
<section class="card">
<div class="head">
<div class="num">2<em>linkedin</em></div>
<div class="title">DM your 1st-degree connections that match the corridor
<small>copy each draft · paste into LinkedIn · max 50/day safe rate</small></div>
<div class="actions">
<a href="/linkedin.html">Open LinkedIn agent ↗</a>
</div>
</div>
<div id="linkedin-list"></div>
</section>
<section class="card">
<div class="head">
<div class="num" id="postcard-count">—<em>postcard</em></div>
<div class="title">Postcard batch · ≤0.5 mi from DW
<small>upload to Lob · ~$0.82/piece · personalized</small></div>
<div class="actions">
<a href="/api/pitches/postcard-batch.csv?min_proximity=walk_10min" download>Download Lob CSV ↓</a>
</div>
</div>
<div id="postcard-summary"></div>
</section>
<section class="card">
<div class="head">
<div class="num">4<em>research</em></div>
<div class="title">Pitches that still need contact info
<small>open Maps · find the right person · paste into the row</small></div>
<div class="actions">
<a href="/pitches.html?status=draft">All draft ↗</a>
</div>
</div>
<div id="research-list"></div>
</section>
</div>
<script>
function escapeHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); }
document.getElementById('today-date').textContent = new Date().toLocaleDateString('en-US', {
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
});
async function loadInMail() {
try {
const r = await fetch('/api/linkedin/subscription').then(r => r.json());
if (r.tier && r.tier !== 'none') {
document.getElementById('inmail-card').style.display = '';
document.getElementById('inmail-num').innerHTML = `${r.credits_remaining||0}<em>inmail</em>`;
const reply = r.used_this_month > 0 ? Math.round((r.replied_this_month||0)/r.used_this_month*100) : 0;
const trial = r.trial_days_remaining != null ? `· trial ends in ${r.trial_days_remaining} days` : '';
document.getElementById('inmail-sub').textContent = `${r.tier} · ${r.used_this_month||0} sent · ${r.replied_this_month||0} replies (${reply}%) ${trial}`;
if (r.trial_status === 'ending_soon') {
document.getElementById('inmail-detail').innerHTML = `<div style="padding:10px 14px;border:1px solid var(--metal);background:rgba(184,153,104,0.06);margin-top:10px;font-size:11px;color:var(--metal-glow)"><strong>Trial decision soon.</strong> Renew if you've sent ≥30 InMails AND ≥10% reply rate. <a href="/sales-nav-signup.html" style="color:var(--metal-glow);margin-left:6px">Decide ↗</a></div>`;
}
}
} catch {}
}
loadInMail();
(async () => {
const data = await fetch('/api/pitches').then(r => r.json());
const rows = data.rows || [];
// 1. Walk targets — same_building + same_block + walk_2min + walk_5min, status not in skip/sent/won/lost
const walking = rows.filter(r =>
['same_building','same_block','walk_2min','walk_5min'].includes(r.dw_proximity)
&& !['skip','sent','won','lost'].includes(r.status)
).sort((a,b) => (a.priority||99) - (b.priority||99));
document.getElementById('t-walk').textContent = walking.length;
document.getElementById('walk-list').innerHTML = walking.slice(0, 8).map(r => {
const ico = r.dw_proximity === 'same_building' ? '⚡' : r.dw_proximity === 'same_block' ? '★' : '◉';
const dist = r.dw_proximity === 'same_building' ? 'same building' : r.dw_proximity === 'same_block' ? 'same block' : '<5 min walk';
const gmaps = r.research_links?.gmaps;
return `
<div class="item">
<div class="ico">${ico}</div>
<div class="nm">${escapeHtml(r.name)}<small>${escapeHtml(r.address || '')} · ${dist} · ${escapeHtml(r.pitch_type || '')}</small></div>
<div class="acts">
${gmaps ? `<a href="${gmaps}" target="_blank">Maps ↗</a>` : ''}
<a href="/pitches.html?id=${r.id}">Detail ↗</a>
</div>
</div>`;
}).join('') || `<div class="empty">No walking-distance targets queued. All walked!</div>`;
// 2. LinkedIn — pitches with li_connection_status = '1st' AND li_dm_sent_at IS NULL (matched 1st-degree)
const liReady = rows.filter(r => r.li_connection_status === '1st' && !r.li_dm_sent_at && !['skip','sent','won','lost'].includes(r.status));
document.getElementById('t-linkedin').textContent = liReady.length;
document.getElementById('linkedin-list').innerHTML = liReady.slice(0, 10).map(r => `
<div class="item">
<div class="ico">💼</div>
<div class="nm">${escapeHtml(r.name)}<small>${escapeHtml(r.address || '')} · ${escapeHtml(r.pitch_type || '')}</small></div>
<div class="acts">
<a href="/linkedin.html">Open queue ↗</a>
</div>
</div>
`).join('') || `<div class="empty">No 1st-degree LinkedIn matches yet. Import your Connections.csv on /linkedin.html.</div>`;
// 3. Postcard count
const postcardReady = rows.filter(r =>
r.outreach_channel === 'postcard'
&& ['same_building','same_block','walk_2min','walk_5min','walk_10min'].includes(r.dw_proximity)
&& !['skip','sent','won','lost'].includes(r.status)
);
document.getElementById('t-postcard').textContent = postcardReady.length;
document.getElementById('postcard-count').innerHTML = `${postcardReady.length}<em>postcard</em>`;
const cost = (postcardReady.length * 0.82).toFixed(0);
document.getElementById('postcard-summary').innerHTML = `
<div class="item">
<div class="ico">✉️</div>
<div class="nm">Lob batch ready · ${postcardReady.length} addresses<small>~$${cost} at Lob's ~$0.82/piece (4×6 personalized + USPS) · or ~$${(postcardReady.length*0.25).toFixed(0)} via USPS EDDM (no personalization)</small></div>
<div class="acts"></div>
</div>`;
// 4. Research queue — pitches with no contact_name AND priority ≤ 5 (top tier)
const research = rows.filter(r =>
!r.contact_name && (r.priority || 99) <= 5 && r.status === 'draft'
).sort((a,b) => (a.priority||99) - (b.priority||99));
document.getElementById('t-research').textContent = research.length;
document.getElementById('research-list').innerHTML = research.slice(0, 8).map(r => {
const gmaps = r.research_links?.gmaps;
return `
<div class="item">
<div class="ico">🔍</div>
<div class="nm">${escapeHtml(r.name)}<small>${escapeHtml(r.address || '')} · pri ${r.priority} · ${escapeHtml(r.pitch_type || '')}</small></div>
<div class="acts">
${gmaps ? `<a href="${gmaps}" target="_blank">Maps ↗</a>` : ''}
<a href="/pitches.html?id=${r.id}">Fill in ↗</a>
</div>
</div>`;
}).join('') || `<div class="empty">No research backlog! All top-tier pitches have contact info.</div>`;
})();
// ─── 14-day velocity trend chart (totals: sent / replied / walked / won) ───
(async () => {
const data = await fetch('/api/snapshots?dim=totals&days=14').then(r => r.json()).catch(() => null);
if (!data || !data.dates || data.dates.length === 0) {
document.getElementById('trend-stats').textContent = 'no snapshot data yet';
return;
}
const series = data.series || {};
const dates = data.dates;
// We chart the action metrics (skip 'all' since it's static at 5,386)
const KEYS = [
{ key: 'sent', color: '#b89968', label: 'sent' },
{ key: 'replied', color: '#d4b683', label: 'replied' },
{ key: 'walked', color: '#6a9b73', label: 'walked' },
{ key: 'won', color: '#d4b683', label: 'won', dashed: true },
];
const W = 800, H = 160, PAD_L = 36, PAD_R = 12, PAD_T = 12, PAD_B = 24;
const innerW = W - PAD_L - PAD_R;
const innerH = H - PAD_T - PAD_B;
let maxY = 0;
for (const k of KEYS) {
if (series[k.key]) maxY = Math.max(maxY, ...series[k.key]);
}
if (maxY < 5) maxY = 5; // floor to keep zero-baseline readable
const xStep = dates.length > 1 ? innerW / (dates.length - 1) : innerW;
const svg = document.getElementById('trend-chart');
let svgInner = '';
// Y-axis grid + labels
for (const yFrac of [0, 0.25, 0.5, 0.75, 1]) {
const y = PAD_T + innerH * (1 - yFrac);
const v = Math.round(maxY * yFrac);
svgInner += `<line x1="${PAD_L}" y1="${y}" x2="${W - PAD_R}" y2="${y}" stroke="#2a2622" stroke-width="0.5"/>`;
svgInner += `<text x="${PAD_L - 6}" y="${y + 3}" text-anchor="end" fill="#888475" font-size="9" font-family="JetBrains Mono">${v}</text>`;
}
// X-axis labels (first / mid / last)
for (const i of [0, Math.floor(dates.length / 2), dates.length - 1]) {
if (dates[i]) {
const x = PAD_L + xStep * i;
const lbl = dates[i].slice(5); // "MM-DD"
svgInner += `<text x="${x}" y="${H - 6}" text-anchor="middle" fill="#888475" font-size="9" font-family="JetBrains Mono">${lbl}</text>`;
}
}
// Series lines
for (const k of KEYS) {
const arr = series[k.key];
if (!arr) continue;
const pts = arr.map((v, i) => `${PAD_L + xStep * i},${PAD_T + innerH * (1 - v / maxY)}`).join(' ');
const dash = k.dashed ? 'stroke-dasharray="4 3"' : '';
svgInner += `<polyline points="${pts}" fill="none" stroke="${k.color}" stroke-width="1.5" ${dash}/>`;
// End-point dot
const last = arr[arr.length - 1];
const lastX = PAD_L + xStep * (arr.length - 1);
const lastY = PAD_T + innerH * (1 - last / maxY);
svgInner += `<circle cx="${lastX}" cy="${lastY}" r="3" fill="${k.color}"/>`;
}
svg.innerHTML = svgInner;
// Legend
document.getElementById('trend-legend').innerHTML = KEYS.map(k => {
const last = (series[k.key] || []).slice(-1)[0] ?? 0;
return `<span style="display:inline-flex;align-items:center;gap:6px"><span style="display:inline-block;width:14px;height:2px;background:${k.color};${k.dashed ? 'background-image:linear-gradient(to right,'+k.color+' 50%,transparent 50%);background-size:6px 2px' : ''}"></span>${k.label} <b style="color:var(--ink);font-family:var(--mono)">${last}</b></span>`;
}).join('');
// Stat strip
const sentNow = series.sent ? series.sent.slice(-1)[0] : 0;
const sentPrev = series.sent && series.sent.length >= 8 ? series.sent[series.sent.length - 8] : 0;
const delta7 = sentNow - sentPrev;
document.getElementById('trend-stats').innerHTML =
`last 7 days: <b style="color:var(--metal-glow);font-family:var(--mono)">+${delta7}</b> sent · <b style="color:var(--metal);font-family:var(--mono)">${dates.length}</b> days captured`;
})();
// ─── Recent activity feed (iter 60) ─────────────────────────────────
const EVENT_ICON = {
status_change: '✎', channel_change: '🔀',
sent: '📤', replied: '📥', closed: '🔒',
reply_text: '💬', followup_scheduled: '⏰',
created: '✨'
};
const EVENT_COLOR = {
sent: 'var(--metal)', replied: 'var(--metal-glow)',
closed: 'var(--green)', won: 'var(--green)',
status_change: 'var(--ink)', channel_change: 'var(--ink)',
reply_text: 'var(--metal-glow)', followup_scheduled: 'var(--metal)',
created: 'var(--ink-mute)'
};
function timeAgo(iso) {
const d = new Date(iso);
const s = Math.round((Date.now() - d.getTime()) / 1000);
if (s < 60) return s + 's ago';
if (s < 3600) return Math.round(s / 60) + 'm ago';
if (s < 86400) return Math.round(s / 3600) + 'h ago';
if (s < 604800) return Math.round(s / 86400) + 'd ago';
return d.toLocaleDateString();
}
function escHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); }
let activityFilter = '';
function renderActivityChips() {
const types = [
{ id: '', label: 'All', color: 'var(--metal)' },
{ id: 'sent', label: '📤 Sent', color: 'var(--metal)' },
{ id: 'replied', label: '📥 Replied', color: 'var(--metal-glow)' },
{ id: 'closed', label: '🔒 Closed', color: 'var(--green)' },
{ id: 'status_change', label: '✎ Status', color: 'var(--ink)' },
{ id: 'channel_change', label: '🔀 Channel', color: 'var(--ink)' },
{ id: 'reply_text', label: '💬 Reply', color: 'var(--metal-glow)' },
{ id: 'followup_scheduled', label: '⏰ Follow-up', color: 'var(--metal)' },
{ id: 'created', label: '✨ Created', color: 'var(--ink-mute)' },
];
document.getElementById('activity-chips').innerHTML = types.map(t => {
const active = activityFilter === t.id;
return `<button data-type="${t.id}" style="background:${active ? 'rgba(184,153,104,0.1)' : 'transparent'};border:1px solid ${active ? t.color : 'var(--rule)'};color:${active ? t.color : 'var(--ink-mute)'};padding:4px 10px;font-size:9px;letter-spacing:.2em;text-transform:uppercase;cursor:pointer;font-family:var(--sans)">${t.label}</button>`;
}).join('');
document.getElementById('activity-chips').querySelectorAll('button').forEach(b => {
b.addEventListener('click', () => { activityFilter = b.dataset.type; renderActivityChips(); loadActivity(); });
});
}
renderActivityChips();
async function loadActivity() {
try {
const url = activityFilter ? `/api/activity?limit=30&event_type=${activityFilter}` : '/api/activity?limit=30';
const data = await fetch(url).then(r => r.json());
const stats = data.stats || {};
document.getElementById('activity-stats').innerHTML =
`<b style="color:var(--metal-glow);font-family:var(--mono)">${stats.last_24h || 0}</b> events · 24h ` +
`<b style="color:var(--metal);font-family:var(--mono);margin-left:14px">${stats.sent_7d || 0}</b> sent · 7d ` +
`<b style="color:var(--metal-glow);font-family:var(--mono);margin-left:14px">${stats.replied_7d || 0}</b> replied · 7d`;
const events = data.events || [];
const feed = document.getElementById('activity-feed');
if (!events.length) {
feed.innerHTML = `<div style="padding:24px;text-align:center;color:var(--ink-mute);font-style:italic">No activity yet — first walk-in or DM will show here.</div>`;
return;
}
feed.innerHTML = events.map(ev => {
const icon = EVENT_ICON[ev.event_type] || '·';
const color = EVENT_COLOR[ev.event_type] || 'var(--ink-mute)';
const arrow = ev.old_value ? `${escHtml(ev.old_value)} → ${escHtml(ev.new_value || '')}` : escHtml(ev.new_value || '');
return `
<a href="/pitches.html?id=${ev.pitch_id}" target="_blank" style="display:grid;grid-template-columns:auto 1fr auto auto;gap:14px;align-items:center;padding:8px 12px;background:var(--noir);border:1px solid var(--rule);text-decoration:none;color:var(--ink);font-size:12px">
<span style="color:${color};font-size:16px;width:22px;text-align:center">${icon}</span>
<span><b style="font-family:var(--serif);font-size:14px;color:var(--metal-glow)">${escHtml(ev.biz_name)}</b> <span style="color:var(--ink-mute);margin-left:6px">${escHtml(ev.pitch_type || '')}</span></span>
<span style="color:${color};font-family:var(--mono);font-size:10px;letter-spacing:.15em;text-transform:uppercase">${escHtml(ev.event_type)} ${arrow ? '· ' + arrow : ''}</span>
<span style="color:var(--ink-mute);font-family:var(--mono);font-size:10px">${timeAgo(ev.occurred_at)}</span>
</a>`;
}).join('');
} catch (e) {
document.getElementById('activity-feed').innerHTML = `<div style="color:var(--ink-mute);padding:14px">activity feed offline</div>`;
}
}
loadActivity();
setInterval(loadActivity, 60_000);
// ─── Priority strip (iter 67) — top 5 walkable bldgs at the top of /today ──
async function loadPrioStrip() {
try {
const data = await fetch('/api/buildings/priority?max_miles=0.5&min_unpitched=2&limit=5').then(r => r.json());
const rows = data.rows || [];
const strip = document.getElementById('prio-strip');
if (!rows.length) {
strip.innerHTML = `<div style="color:var(--ink-mute);padding:14px;font-style:italic">No unwalked walkable buildings — all priority targets done.</div>`;
return;
}
strip.innerHTML = rows.map((b, i) => {
const isDW = (b.bldg_address || '').startsWith('15442 ');
return `
<a href="/buildings.html?bldg=${encodeURIComponent(b.bldg_address)}" target="_blank" style="display:block;background:var(--noir-rise);border:1px solid ${isDW ? 'var(--metal-glow)' : 'var(--rule)'};padding:10px 12px;text-decoration:none;color:var(--ink)">
<div style="display:flex;justify-content:space-between;align-items:baseline;gap:8px">
<span style="font-family:var(--serif);font-style:italic;font-size:18px;color:var(--metal)">${String(i+1).padStart(2,'0')}</span>
<span style="font-family:var(--mono);font-size:9px;color:var(--metal-glow);letter-spacing:.1em">${b.dw_miles} mi</span>
</div>
<div style="font-family:var(--serif);font-size:14px;color:var(--ink);margin-top:2px">${b.bldg_address}${isDW ? ' ⚡' : ''}</div>
<div style="display:flex;gap:10px;font-size:9px;letter-spacing:.12em;color:var(--ink-mute);text-transform:uppercase;margin-top:6px">
<span><b style="color:var(--metal);font-family:var(--mono);font-size:13px">${b.unpitched}</b> unpitched</span>
<span style="color:var(--ink-mute)">/ ${b.tenants_total} tenants</span>
</div>
</a>`;
}).join('');
} catch (e) {
document.getElementById('prio-strip').innerHTML = `<div style="color:var(--ink-mute);padding:14px">priority load failed</div>`;
}
}
loadPrioStrip();
// ─── Data freshness pill (iter 69) ──────────────────────────────────
async function loadFreshness() {
try {
const d = await fetch('/api/data-freshness').then(r => r.json());
const pill = document.getElementById('freshness-pill');
if (!pill) return;
const cls = ({ fresh: 'green', stale: 'metal', very_stale: 'red', never: 'red' })[d.status] || 'mute';
const color = ({ fresh: 'var(--green,#6a9b73)', stale: 'var(--metal)', very_stale: 'var(--red,#b66565)', never: 'var(--red,#b66565)' })[d.status];
const icon = ({ fresh: '✓', stale: '⚠', very_stale: '⚠', never: '✕' })[d.status];
const days = d.days_since_ingest;
pill.innerHTML = `data: <b style="color:${color};font-family:var(--mono);font-size:11px">${icon} ${days === null ? '—' : (days === 0 ? 'today' : days === 1 ? '1 day' : days + ' days')}</b> · ${(d.btrc_total || 0).toLocaleString()} BTRC rows`;
pill.style.borderColor = color || 'var(--rule)';
pill.style.color = color || 'var(--ink-mute)';
pill.title = `Last BTRC ingest: ${d.last_btrc_ingest || 'never'}\nSource: ${d.source_url}\nRefresh command: ${d.ingest_command}`;
} catch {}
}
loadFreshness();
setInterval(loadFreshness, 5 * 60_000);
// ─── Stale pitches (iter 71) ────────────────────────────────────────
async function loadStale() {
try {
const data = await fetch('/api/stale-pitches?days=14&limit=10').then(r => r.json());
const rows = data.rows || [];
const sect = document.getElementById('stale-section');
if (!rows.length) { sect.style.display = 'none'; return; }
sect.style.display = 'block';
document.getElementById('stale-summary').innerHTML =
`<b style="color:#c47b7b;font-family:var(--mono)">${rows.length}</b> sent ≥${data.days_threshold}d ago, no reply yet`;
document.getElementById('stale-list').innerHTML = rows.map(r => `
<a href="/responses.html?id=${r.id}" target="_blank" style="display:grid;grid-template-columns:auto 1fr auto auto auto;gap:14px;align-items:center;padding:8px 12px;background:var(--noir);border:1px solid var(--rule);text-decoration:none;color:var(--ink);font-size:12px">
<span style="font-family:var(--mono);font-size:11px;color:#c47b7b;font-weight:500;min-width:50px">${r.days_silent}d</span>
<span><b style="font-family:var(--serif);font-size:14px;color:var(--metal-glow)">${escHtml(r.name)}</b> <span style="color:var(--ink-mute);margin-left:6px">${escHtml(r.pitch_type || '')}</span></span>
<span style="color:var(--ink-mute);font-size:10px;letter-spacing:.12em;text-transform:uppercase">${escHtml(r.outreach_channel || '—')}</span>
<span style="color:var(--ink-mute);font-family:var(--mono);font-size:10px">sent ${new Date(r.sent_at).toLocaleDateString()}</span>
<span style="color:var(--metal);font-size:10px;letter-spacing:.18em;text-transform:uppercase">log →</span>
</a>
`).join('');
} catch {}
}
loadStale();
setInterval(loadStale, 5 * 60_000);
// ─── Magazine progress (iter 74) ────────────────────────────────────
async function loadMagazineStats() {
try {
const d = await fetch('/api/magazine/stats').then(r => r.json());
const total = Number(d.total || 0);
const corr = Number(d.corridor_total || 1);
const pct = Math.min(100, (100 * total / corr));
document.getElementById('magazine-stats').innerHTML =
`<b style="color:var(--metal-glow);font-family:var(--mono);font-size:13px">${total.toLocaleString()}</b> features written · ` +
`<b style="color:var(--metal);font-family:var(--mono)">${d.draft || 0}</b> drafts · ` +
`<b style="color:var(--green,#6a9b73);font-family:var(--mono)">${d.published || 0}</b> published · ` +
`<b style="color:var(--metal);font-family:var(--mono)">${d.gen_24h || 0}</b> generated last 24h · ` +
`coverage <b style="color:var(--metal-glow);font-family:var(--mono)">${pct.toFixed(1)}%</b> of ${corr.toLocaleString()} businesses`;
document.getElementById('magazine-bar').style.width = pct + '%';
} catch {}
}
loadMagazineStats();
setInterval(loadMagazineStats, 30_000);
// Feature of the day (iter 85)
async function loadFOTD() {
try {
const d = await fetch('/api/magazine/feature-of-the-day').then(r => r.json());
const f = d.feature;
if (!f) return;
document.getElementById('fotd-section').style.display = 'block';
document.getElementById('fotd-link').href = '/magazine/' + f.id;
document.getElementById('fotd-body').innerHTML = `
<h2 style="font-family:var(--serif);font-style:italic;font-weight:400;font-size:32px;line-height:1.05;color:var(--ink);margin:0 0 6px">
<a href="/magazine/${f.id}" style="color:inherit;text-decoration:none">${escHtml(f.headline || f.biz_name)}</a>
</h2>
<div style="font-family:var(--serif);font-style:italic;color:var(--ink-mute);font-size:15px;margin-bottom:14px">${escHtml(f.subhead || '')}</div>
<p style="font-family:var(--serif);font-size:15px;line-height:1.55;color:var(--ink);margin:0 0 12px;max-width:720px">${escHtml((f.editorial || '').slice(0, 320))}…</p>
${f.pull_quote ? `<div style="font-family:var(--serif);font-style:italic;color:var(--metal-glow);font-size:18px;border-left:2px solid var(--metal);padding-left:14px;margin:12px 0">"${escHtml(f.pull_quote)}"</div>` : ''}
<div style="font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-mute);margin-top:10px">${escHtml(f.biz_name)}${f.biz_address ? ' · ' + escHtml(f.biz_address) : ''}${f.city ? ' · ' + escHtml(f.city) : ''} · ${escHtml(f.category_tag || '')}</div>
`;
} catch {}
}
loadFOTD();
async function loadFOTW() {
try {
const d = await fetch('/api/magazine/feature-of-the-week').then(r => r.json());
const f = d.feature;
if (!f) return;
document.getElementById('fotw-section').style.display = 'block';
document.getElementById('fotw-link').href = '/magazine/' + f.id;
document.getElementById('fotw-body').innerHTML = `
<a href="/magazine/${f.id}" style="font-family:var(--serif);font-size:18px;font-style:italic;color:var(--ink);text-decoration:none">${escHtml(f.headline || f.biz_name)}</a>
<span style="color:var(--ink-mute);font-size:12px;margin-left:6px">— ${escHtml(f.biz_name)} · ${escHtml(f.category_tag || '')}</span>
`;
} catch {}
}
loadFOTW();
async function loadInquiries() {
try {
const d = await fetch('/api/sponsor/inquiries').then(r => r.json());
const rows = (d.rows || []).slice(0, 7);
if (!rows.length) return;
document.getElementById('inq-section').style.display = 'block';
document.getElementById('inq-stats').innerHTML = `<b style="color:var(--accent,#a8763a);font-family:var(--mono)">${d.count}</b> total · ${rows.filter(r => r.status === 'new').length} new`;
document.getElementById('inq-list').innerHTML = rows.map(r => {
const ts = new Date(r.received_at).toLocaleString('en-US',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'});
const tierColor = ({ cover: 'var(--accent,#a8763a)', spread: 'var(--metal-glow)', ask: 'var(--ink-mute)' })[r.tier] || 'var(--ink-mute)';
const statusColor = ({ new: 'var(--accent,#a8763a)', responded: 'var(--metal)', closed: 'var(--green,#6a9b73)', spam: 'var(--ink-mute)' })[r.status] || 'var(--ink-mute)';
return `<div style="display:grid;grid-template-columns:auto 1fr auto auto auto;gap:14px;align-items:center;padding:7px 10px;background:var(--noir);border:1px solid var(--rule);font-size:12px">
<span style="color:${tierColor};font-family:var(--mono);font-size:9px;letter-spacing:.22em;text-transform:uppercase;font-weight:500">${escHtml(r.tier || 'ask')}</span>
<a href="/magazine/${r.feature_id}" target="_blank" style="color:var(--ink);text-decoration:none"><b style="font-family:var(--serif);font-size:14px;color:var(--metal-glow)">${escHtml(r.biz_name || '?')}</b> <span style="color:var(--ink-mute);font-family:var(--mono);font-size:10px;margin-left:6px">${escHtml(r.contact_name || '')} · ${escHtml(r.contact_email || '')}</span></a>
<span style="color:${statusColor};font-family:var(--mono);font-size:9px;letter-spacing:.18em;text-transform:uppercase">${escHtml(r.status || 'new')}</span>
<span style="color:var(--ink-mute);font-family:var(--mono);font-size:9px">${ts}</span>
<span>
<button onclick="patchInq(${r.id}, 'responded')" style="background:transparent;border:1px solid var(--rule);color:var(--metal);font-size:8px;letter-spacing:.18em;text-transform:uppercase;padding:3px 6px;cursor:pointer">↩ replied</button>
<button onclick="patchInq(${r.id}, 'closed')" style="background:transparent;border:1px solid var(--rule);color:var(--green,#6a9b73);font-size:8px;letter-spacing:.18em;text-transform:uppercase;padding:3px 6px;cursor:pointer">✓ done</button>
</span>
</div>`;
}).join('');
} catch {}
}
async function patchInq(id, status) {
try {
await fetch(`/api/sponsor/inquiries/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) });
loadInquiries();
} catch (e) { alert('Failed: ' + e.message); }
}
loadInquiries();
setInterval(loadInquiries, 60_000);
async function loadQualityPill() {
try {
const d = await fetch('/api/magazine/quality-flags').then(r => r.json());
const a = document.getElementById('nav-quality');
if (!a) return;
if (d.count > 0) {
a.style.color = 'var(--metal-glow)';
a.innerHTML = `🔧 quality <span style="background:var(--metal);color:#0a0a0c;padding:0 6px;margin-left:4px;font-size:8px;letter-spacing:.1em;font-weight:600">${d.count}</span>`;
a.title = `${d.summary.thin} thin · ${d.summary.no_pull_quote} no pull-quote · ${d.summary.stale_draft} stale draft · ${d.summary.no_category} uncategorized`;
} else {
a.style.color = 'var(--green)';
a.textContent = '🔧 quality ✓';
a.title = 'all features pass quality checks';
}
} catch {}
}
loadQualityPill();
setInterval(loadQualityPill, 5 * 60_000);
async function loadMagActivity() {
try {
const d = await fetch('/api/magazine/activity?limit=12').then(r => r.json());
const ICONS = {
'feature.generated': '✨', 'feature.published': '★', 'feature.reviewed': '✓',
'cover.set': '⛧', 'inquiry.received': '$',
'feedback.typo': '✎', 'feedback.correction': '✎', 'feedback.tip': '※',
'feedback.complaint': '!', 'feedback.praise': '♥', 'feedback.note': '·'
};
const items = (d.events || []).map(e => {
const ic = ICONS[e.kind] || '·';
const ago = (() => {
const ms = Date.now() - new Date(e.at).getTime();
const m = Math.floor(ms / 60000);
if (m < 60) return m + 'm ago';
const h = Math.floor(m / 60);
if (h < 24) return h + 'h ago';
return Math.floor(h / 24) + 'd ago';
})();
const label = (e.label || '').toString().replace(/[<>]/g, '').slice(0, 50);
return `<span style="margin-right:24px;color:var(--ink-mute)"><span style="color:var(--metal-glow);margin-right:4px">${ic}</span>${label}<span style="color:var(--metal);margin-left:6px;font-size:9px">${ago}</span></span>`;
});
const pill = document.getElementById('mag-activity-strip');
if (pill) pill.innerHTML = items.join('') || '<i style="font-style:italic;font-family:var(--serif)">no recent activity</i>';
} catch {}
}
loadMagActivity();
setInterval(loadMagActivity, 90_000);
async function loadGenQueue() {
try {
const d = await fetch('/api/magazine/gen-queue?limit=8').then(r => r.json());
const items = (d.queue || []).map(q => {
const ads = Number(q.paid_ads_count || 0);
const adsHtml = ads > 0 ? `<span style="color:var(--accent,#a8763a);margin-left:4px;font-size:9px">$${ads}</span>` : '';
const name = String(q.name || '').slice(0, 28);
const city = q.city ? `<span style="color:var(--metal);margin-left:4px;font-size:9px">${q.city.slice(0, 12)}</span>` : '';
return `<span style="margin-right:24px;color:var(--ink-mute)">${name}${adsHtml}${city}</span>`;
});
const pill = document.getElementById('gen-queue-strip');
if (pill) pill.innerHTML = items.join('') || '<i style="font-style:italic;font-family:var(--serif)">queue empty</i>';
} catch {}
}
loadGenQueue();
setInterval(loadGenQueue, 5 * 60_000);
async function loadDupesPill() {
try {
const d = await fetch('/api/magazine/duplicates').then(r => r.json());
const a = document.getElementById('nav-dupes');
if (!a) return;
if (d.groups > 0) {
a.style.color = 'var(--metal-glow)';
a.innerHTML = `⊕ dupes <span style="background:var(--metal);color:#0a0a0c;padding:0 6px;margin-left:4px;font-size:8px;letter-spacing:.1em;font-weight:600">${d.groups}</span>`;
a.title = `${d.groups} duplicate-headline groups affecting ${d.total_affected} features — qwen3 templating signal`;
} else {
a.style.color = 'var(--green)';
a.textContent = '⊕ dupes ✓';
a.title = 'every headline is unique';
}
} catch {}
}
loadDupesPill();
setInterval(loadDupesPill, 5 * 60_000);
async function loadDraftPick() {
try {
const d = await fetch('/api/magazine/draft-pick-of-the-night').then(r => r.json());
const rail = document.getElementById('draft-pick-rail');
if (!d.pick) { rail.style.display = 'none'; return; }
rail.style.display = 'block';
const link = document.getElementById('draft-pick-link');
link.textContent = d.pick.headline || d.pick.biz_name;
link.href = '/magazine/' + d.pick.id;
const meta = document.getElementById('draft-pick-meta');
meta.textContent = `${d.pick.biz_name} · ${d.pick.editorial_len}c · ${d.pick.category_tag || '—'} · score ${d.pick.score}`;
} catch {}
}
loadDraftPick();
setInterval(loadDraftPick, 5 * 60_000);
async function updateFeedbackPill() {
try {
const d = await fetch('/api/feedback').then(r => r.json());
const unread = (d.rows || []).filter(r => !r.reviewed).length;
const a = document.getElementById('nav-feedback');
if (!a) return;
if (unread > 0) {
a.style.color = 'var(--metal-glow)';
a.innerHTML = `✎ feedback <span style="background:var(--metal);color:#0a0a0c;padding:0 6px;margin-left:4px;font-size:8px;letter-spacing:.1em;font-weight:600">${unread}</span>`;
} else {
a.style.color = 'var(--ink-mute)';
a.textContent = '✎ feedback';
}
} catch {}
}
updateFeedbackPill();
setInterval(updateFeedbackPill, 60_000);
</script>
</body>
</html>