← back to Ventura Corridor
iter 166: /duplicates.html admin viewer — keeps oldest, click-regen runner-ups for variety
1efb250cfae4b53304cd38f8275b244b22a9c5bf · 2026-05-06 19:52:15 -0700 · SteveStudio2
Files touched
A public/duplicates.htmlM public/today.htmlM src/server/index.ts
Diff
commit 1efb250cfae4b53304cd38f8275b244b22a9c5bf
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Wed May 6 19:52:15 2026 -0700
iter 166: /duplicates.html admin viewer — keeps oldest, click-regen runner-ups for variety
---
public/duplicates.html | 109 +++++++++++++++++++++++++++++++++++++++++++++++++
public/today.html | 2 +-
src/server/index.ts | 1 +
3 files changed, 111 insertions(+), 1 deletion(-)
diff --git a/public/duplicates.html b/public/duplicates.html
new file mode 100644
index 0000000..b7ba643
--- /dev/null
+++ b/public/duplicates.html
@@ -0,0 +1,109 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Duplicate headlines · 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;--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}
+.summary{padding:18px;background:var(--noir-rise);border:1px solid var(--rule);margin-bottom:24px;text-align:center;font-family:'Cormorant Garamond',serif;font-style:italic;font-size:18px;color:var(--ink)}
+.summary b{color:var(--metal-glow);font-weight:500}
+.group{background:var(--noir-rise);border:1px solid var(--rule);padding:24px;margin-bottom:18px}
+.group .head{font-family:'Cormorant Garamond',serif;font-style:italic;font-size:24px;color:var(--metal);margin-bottom:6px;font-weight:500}
+.group .count{font-size:9px;letter-spacing:.32em;text-transform:uppercase;color:var(--ink-mute);margin-bottom:14px}
+.group ul{list-style:none;padding:0;margin:0}
+.group li{display:grid;grid-template-columns:50px 1fr auto auto auto;gap:12px;padding:8px 0;border-bottom:1px dotted var(--rule);font-size:13px;align-items:center}
+.group li.oldest{opacity:0.55;font-style:italic}
+.group li .id{font-family:'JetBrains Mono',monospace;color:var(--metal);font-size:11px}
+.group li .biz{color:var(--ink)}
+.group li .meta{font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--ink-mute)}
+.group li .actions{display:flex;gap:6px}
+.group li .actions a, .group li .actions button{font-size:8px;letter-spacing:.22em;text-transform:uppercase;color:var(--metal);border:1px solid var(--rule);padding:4px 8px;text-decoration:none;background:transparent;cursor:pointer;font-family:Inter,sans-serif}
+.group li .actions a:hover, .group li .actions button:hover{border-color:var(--metal)}
+.group li .actions button.regen{color:var(--metal-glow);border-color:var(--metal-glow)}
+.group li .actions button.regen:disabled{opacity:0.5;cursor:default}
+.group .legend{margin-top:10px;padding-top:10px;border-top:1px solid var(--rule);font-size:11px;color:var(--ink-mute);font-style:italic;font-family:'Cormorant Garamond',serif}
+.empty{text-align:center;padding:80px;color:var(--green);font-style:italic;font-family:'Cormorant Garamond',serif;font-size:24px}
+</style>
+</head>
+<body>
+<header>
+ <h1>Duplicate <em>headlines</em></h1>
+ <div class="sub">when qwen3 templates · click regen on the runner-up to introduce variety</div>
+</header>
+<nav>
+ <a href="/today.html">today</a>
+ <a href="/magazine.html">magazine</a>
+ <a href="/issue">issue</a>
+ <a href="/scoreboard.html">scoreboard</a>
+ <a href="/duplicates.html" class="active">dupes</a>
+ <a href="/health.html">health</a>
+</nav>
+<main id="root"><div class="summary">Loading…</div></main>
+<script>
+function escHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); }
+async function load() {
+ const d = await fetch('/api/magazine/duplicates').then(r => r.json());
+ const root = document.getElementById('root');
+ if (d.groups === 0) {
+ root.innerHTML = '<div class="empty">✓ Every headline is unique.</div>';
+ return;
+ }
+ let html = `<div class="summary"><b>${d.groups}</b> duplicate group${d.groups===1?'':'s'} affecting <b>${d.total_affected}</b> features</div>`;
+ for (const g of d.rows) {
+ const features = g.features;
+ // Oldest by generated_at; mark it as the "keeper"
+ const sorted = [...features].sort((a, b) => new Date(a.generated_at) - new Date(b.generated_at));
+ const keeperId = sorted[0].id;
+ html += `<div class="group">
+ <div class="head">"${escHtml(g.headline)}"</div>
+ <div class="count">${g.dup_count} copies · runner-ups eligible for regen</div>
+ <ul>${features.map(f => {
+ const isKeeper = Number(f.id) === Number(keeperId);
+ const ts = new Date(f.generated_at).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
+ return `<li class="${isKeeper ? 'oldest' : ''}">
+ <span class="id">#${f.id}${isKeeper ? ' ★' : ''}</span>
+ <span class="biz">${escHtml(f.biz_name)}</span>
+ <span class="meta">${escHtml(f.category_tag || '—')} · ${escHtml(f.status)} · ${f.editorial_len}c</span>
+ <span class="meta">${escHtml(ts)}</span>
+ <span class="actions">
+ <a href="/magazine/${f.id}" target="_blank">view</a>
+ ${isKeeper ? '' : `<button class="regen" onclick="regen(${f.id}, this)">regen</button>`}
+ </span>
+ </li>`;
+ }).join('')}</ul>
+ <div class="legend">★ = oldest copy (kept). Runner-ups can be re-prompted to introduce variety.</div>
+ </div>`;
+ }
+ root.innerHTML = html;
+}
+async function regen(id, btn) {
+ if (!confirm(`Regenerate feature ${id}? This calls qwen3 again on Mac1; takes ~30s.`)) return;
+ btn.disabled = true;
+ btn.textContent = '…';
+ try {
+ const r = await fetch(`/api/magazine/${id}/regen`, { method: 'POST' });
+ if (!r.ok) throw new Error(await r.text());
+ btn.textContent = '✓';
+ setTimeout(load, 800);
+ } catch (e) {
+ btn.disabled = false;
+ btn.textContent = 'regen';
+ alert('regen failed: ' + e.message);
+ }
+}
+load();
+</script>
+</body>
+</html>
diff --git a/public/today.html b/public/today.html
index 16cc0f7..0d29594 100644
--- a/public/today.html
+++ b/public/today.html
@@ -127,7 +127,7 @@
<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="/api/magazine/duplicates" id="nav-dupes" target="_blank" style="color:var(--ink-mute)">⊕ dupes</a>
+ <a href="/duplicates.html" id="nav-dupes" style="color:var(--ink-mute)">⊕ dupes</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 f458566..4b275d3 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -87,6 +87,7 @@ const ADMIN_PATHS = [
/^\/coverage(\.html)?\/?$/i,
/^\/api\/coverage(\/.*)?$/i,
/^\/health(\.html)?\/?$/i,
+ /^\/duplicates(\.html)?\/?$/i,
/^\/sitemap\.xml\/?$/i,
/^\/robots\.txt\/?$/i,
/^\/issue(\/[\w-]+)?\/feed\.xml\/?$/i,
← 680bcb7 iter 165: /api/magazine/duplicates + nav pill — surfaces qwe
·
back to Ventura Corridor
·
iter 167: /api/magazine/headline-stems + /duplicates.html te 6a0afbb →