[object Object]

← 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

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