← back to Ventura Corridor
iter 170: /calendar.html + /api/magazine/calendar — GitHub-style 90-day heatmap of feature gen rate
59b0a5e3ace2a0db4df6cc52e29248f5da3721e7 · 2026-05-06 20:04:29 -0700 · SteveStudio2
Files touched
A public/calendar.htmlM public/today.htmlM src/server/index.ts
Diff
commit 59b0a5e3ace2a0db4df6cc52e29248f5da3721e7
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Wed May 6 20:04:29 2026 -0700
iter 170: /calendar.html + /api/magazine/calendar — GitHub-style 90-day heatmap of feature gen rate
---
public/calendar.html | 127 +++++++++++++++++++++++++++++++++++++++++++++++++++
public/today.html | 1 +
src/server/index.ts | 21 +++++++++
3 files changed, 149 insertions(+)
diff --git a/public/calendar.html b/public/calendar.html
new file mode 100644
index 0000000..9e840ae
--- /dev/null
+++ b/public/calendar.html
@@ -0,0 +1,127 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Calendar · 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;--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:1100px;margin:0 auto}
+.legend{margin-bottom:24px;display:flex;gap:18px;align-items:center;flex-wrap:wrap;font-size:11px;color:var(--ink-mute)}
+.legend .swatches{display:flex;gap:4px;align-items:center}
+.legend .sw{width:14px;height:14px;border:1px solid var(--rule)}
+.heat{display:grid;grid-template-columns:auto 1fr;gap:18px;align-items:start;background:var(--noir-rise);border:1px solid var(--rule);padding:24px;overflow-x:auto}
+.weekdays{display:grid;grid-template-rows:repeat(7,16px);row-gap:3px;font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--ink-mute);padding-top:18px}
+.grid{display:grid;grid-auto-flow:column;grid-template-rows:repeat(7,16px);gap:3px}
+.month-row{display:grid;grid-auto-flow:column;grid-template-rows:repeat(7,16px);column-gap:3px;row-gap:3px;align-items:center}
+.day{width:16px;height:16px;border:1px solid var(--rule);background:rgba(255,255,255,0.02);cursor:default;transition:transform 0.1s}
+.day:hover{transform:scale(1.4);z-index:5;outline:1px solid var(--metal-glow)}
+.day.l1{background:rgba(184,153,104,0.22)}
+.day.l2{background:rgba(184,153,104,0.45)}
+.day.l3{background:rgba(184,153,104,0.7)}
+.day.l4{background:rgba(212,182,131,1)}
+.month-labels{margin-bottom:6px;display:flex;font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--ink-mute);padding-left:34px;gap:0}
+.month-labels span{flex:1;text-align:left;letter-spacing:.18em;text-transform:uppercase}
+
+.summary{display:grid;grid-template-columns:repeat(4,1fr);gap:18px;margin-bottom:24px}
+.stat{background:var(--noir-rise);border:1px solid var(--rule);padding:24px}
+.stat .v{font-family:'Cormorant Garamond',serif;font-style:italic;font-size:42px;line-height:1;color:var(--metal-glow);font-weight:500}
+.stat .l{font-size:9px;letter-spacing:.32em;text-transform:uppercase;color:var(--ink-mute);margin-top:8px}
+</style>
+</head>
+<body>
+<header>
+ <h1>Publication <em>calendar</em></h1>
+ <div class="sub">last 90 days · feature gen heatmap (PT timezone)</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="/duplicates.html">dupes</a>
+ <a href="/calendar.html" class="active">calendar</a>
+</nav>
+<main>
+<div class="summary">
+ <div class="stat"><div class="v" id="t-total">—</div><div class="l">total features (90d)</div></div>
+ <div class="stat"><div class="v" id="t-pub">—</div><div class="l">published in window</div></div>
+ <div class="stat"><div class="v" id="t-best">—</div><div class="l">best day count</div></div>
+ <div class="stat"><div class="v" id="t-active">—</div><div class="l">active days</div></div>
+</div>
+
+<div class="legend">
+ <span>less</span>
+ <div class="swatches">
+ <span class="sw"></span>
+ <span class="sw l1"></span>
+ <span class="sw l2"></span>
+ <span class="sw l3"></span>
+ <span class="sw l4"></span>
+ </div>
+ <span>more</span>
+ <span style="margin-left:auto;font-style:italic;font-family:'Cormorant Garamond',serif">hover any cell for date + count</span>
+</div>
+
+<div id="grid-host"></div>
+</main>
+<script>
+function fmt(n){return Number(n).toLocaleString('en-US')}
+async function load() {
+ const d = await fetch('/api/magazine/calendar?days=90').then(r => r.json());
+ const map = new Map();
+ for (const r of (d.rows || [])) map.set(r.day, r);
+
+ // Build a calendar covering the window. Anchor end on today PT.
+ const days = 90;
+ const cells = [];
+ const now = new Date();
+ for (let i = days - 1; i >= 0; i--) {
+ const dt = new Date(now);
+ dt.setDate(now.getDate() - i);
+ const k = dt.toISOString().slice(0, 10);
+ const hit = map.get(k);
+ cells.push({ date: dt, key: k, gen: hit ? Number(hit.gen) : 0, pub: hit ? Number(hit.pub) : 0 });
+ }
+
+ // Stats
+ const total = cells.reduce((a, c) => a + c.gen, 0);
+ const pub = cells.reduce((a, c) => a + c.pub, 0);
+ const best = Math.max(0, ...cells.map(c => c.gen));
+ const active = cells.filter(c => c.gen > 0).length;
+ document.getElementById('t-total').textContent = fmt(total);
+ document.getElementById('t-pub').textContent = fmt(pub);
+ document.getElementById('t-best').textContent = fmt(best);
+ document.getElementById('t-active').textContent = fmt(active);
+
+ const max = Math.max(1, best);
+ const tier = (n) => n === 0 ? '' : n / max <= 0.25 ? ' l1' : n / max <= 0.5 ? ' l2' : n / max <= 0.85 ? ' l3' : ' l4';
+ const dayCells = cells.map(c => {
+ const ts = c.date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
+ return `<div class="day${tier(c.gen)}" title="${ts} · ${c.gen} generated · ${c.pub} published"></div>`;
+ });
+
+ // First column might be padded so cells align with weekday rows (Sun=0..Sat=6)
+ const firstDay = cells[0].date.getDay();
+ const blanks = Array.from({ length: firstDay }, () => `<div class="day" style="visibility:hidden"></div>`);
+ const all = [...blanks, ...dayCells];
+
+ // Inject as a single grid; CSS uses auto-flow:column to fill 7-row weeks.
+ const wd = `<div class="weekdays"><span>Sun</span><span></span><span>Tue</span><span></span><span>Thu</span><span></span><span>Sat</span></div>`;
+ const g = `<div class="grid">${all.join('')}</div>`;
+ document.getElementById('grid-host').innerHTML = `<div class="heat">${wd}${g}</div>`;
+}
+load();
+</script>
+</body>
+</html>
diff --git a/public/today.html b/public/today.html
index 0d29594..af0b469 100644
--- a/public/today.html
+++ b/public/today.html
@@ -128,6 +128,7 @@
<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>
diff --git a/src/server/index.ts b/src/server/index.ts
index 82e5338..2876f1e 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -88,6 +88,7 @@ const ADMIN_PATHS = [
/^\/api\/coverage(\/.*)?$/i,
/^\/health(\.html)?\/?$/i,
/^\/duplicates(\.html)?\/?$/i,
+ /^\/calendar(\.html)?\/?$/i,
/^\/sitemap\.xml\/?$/i,
/^\/robots\.txt\/?$/i,
/^\/issue(\/[\w-]+)?\/feed\.xml\/?$/i,
@@ -1933,6 +1934,26 @@ app.get('/api/magazine/random', async (req, res) => {
}
});
+// Calendar — features generated/published per day for the last N days.
+// Powers /calendar.html GitHub-style heatmap.
+app.get('/api/magazine/calendar', async (req, res) => {
+ try {
+ const days = Math.min(365, Math.max(7, parseInt(String(req.query.days || 90), 10) || 90));
+ const r = await query(`
+ SELECT to_char(date_trunc('day', generated_at AT TIME ZONE 'America/Los_Angeles'), 'YYYY-MM-DD') AS day,
+ count(*) AS gen,
+ count(*) FILTER (WHERE status='published') AS pub,
+ count(*) FILTER (WHERE status='draft') AS draft
+ FROM magazine_features
+ WHERE generated_at > NOW() - (INTERVAL '1 day' * $1)
+ GROUP BY 1 ORDER BY 1
+ `, [days]);
+ res.json({ days, count: r.rowCount, rows: r.rows });
+ } catch (e: any) {
+ res.status(500).json({ error: e.message });
+ }
+});
+
// Headline-phrase frequency — finds 3-word phrases ("trigrams") that recur across
// headlines. Cousin of /api/magazine/duplicates: catches templating BEFORE it
// becomes an exact-match duplicate. Surfaces the patterns qwen3 reaches for.
← bfb00de iter 169: gen prompt now blocks the 6 most-overused headline
·
back to Ventura Corridor
·
iter 171: SESSION_LOG_2026-05-06.md — 40-iter overnight debr a55be3f →