[object Object]

← back to Ventura Corridor

feat(coverage): idea-loop feed bridge on /coverage.html

dab658539e9a106137eb776040e4fce29292a711 · 2026-05-07 22:23:05 -0700 · SteveStudio2

Read-only bridge to the idea-loop skill so accepted ideas surface in
the corridor coverage page alongside the news analytics. Read from
~/.claude/skills/idea-loop/data/ideas.jsonl on demand, sorted by
score_total DESC, top 20.

Routes:
  GET /api/ideas/recent    {count, rows[]} from ideas.jsonl. 60s cache.
                           Graceful "idea-loop not yet running" if file
                           missing.

UI:
  /coverage.html new "Idea-loop" section above News scrape calendar.
  3-column grid: score / TYPE (color-coded by CONNECTION/PRODUCT/etc.) /
  title + body excerpt. Header links to the standalone viewer at :9920.

Both autonomous loops now have a single visual home — coverage.html
shows news pipeline analytics + creative idea pipeline side by side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit dab658539e9a106137eb776040e4fce29292a711
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Thu May 7 22:23:05 2026 -0700

    feat(coverage): idea-loop feed bridge on /coverage.html
    
    Read-only bridge to the idea-loop skill so accepted ideas surface in
    the corridor coverage page alongside the news analytics. Read from
    ~/.claude/skills/idea-loop/data/ideas.jsonl on demand, sorted by
    score_total DESC, top 20.
    
    Routes:
      GET /api/ideas/recent    {count, rows[]} from ideas.jsonl. 60s cache.
                               Graceful "idea-loop not yet running" if file
                               missing.
    
    UI:
      /coverage.html new "Idea-loop" section above News scrape calendar.
      3-column grid: score / TYPE (color-coded by CONNECTION/PRODUCT/etc.) /
      title + body excerpt. Header links to the standalone viewer at :9920.
    
    Both autonomous loops now have a single visual home — coverage.html
    shows news pipeline analytics + creative idea pipeline side by side.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 public/coverage.html | 32 ++++++++++++++++++++++++++++++++
 src/server/index.ts  | 23 +++++++++++++++++++++++
 2 files changed, 55 insertions(+)

diff --git a/public/coverage.html b/public/coverage.html
index 777a006..93e7537 100644
--- a/public/coverage.html
+++ b/public/coverage.html
@@ -84,6 +84,12 @@ main{padding:32px;max-width:1200px;margin:0 auto}
   <div id="city"></div>
 </div>
 
+<div class="bar-section">
+  <h3>Idea-loop · accepted ideas (top by score)</h3>
+  <p style="font-family:'Cormorant Garamond',serif;font-style:italic;font-size:13px;color:var(--ink-mute);margin:0 0 14px">Surviving ideas from the autonomous idea-loop debate panel (qwen3 + gemma3 + deepseek-r1). Full viewer at <a href="http://127.0.0.1:9920/" target="_blank" rel="noopener" style="color:var(--metal)">:9920</a>.</p>
+  <div id="idea-feed"></div>
+</div>
+
 <div class="bar-section">
   <h3>News scrape calendar · last 90 days</h3>
   <p style="font-family:'Cormorant Garamond',serif;font-style:italic;font-size:13px;color:var(--ink-mute);margin:0 0 14px">Daily count of articles landed by the news scraper. Quiet stretches mean the nightly 03:15 scrape didn't find new posts; bright days mean fresh content.</p>
@@ -154,6 +160,32 @@ async function load() {
   renderBars(d.by_vertical, document.getElementById('vert'));
   renderBars(d.by_city, document.getElementById('city'));
 
+  // Idea-loop feed
+  try {
+    const ideas = await fetch('/api/ideas/recent').then(r => r.json());
+    const elI = document.getElementById('idea-feed');
+    const rows = ideas.rows || [];
+    if (!rows.length) {
+      elI.innerHTML = '<p style="color:var(--ink-mute);font-style:italic;font-family:Cormorant Garamond,serif">No accepted ideas yet — idea-loop will land them as they pass debate panel ≥28/40.</p>';
+    } else {
+      elI.innerHTML = rows.map(r => {
+        const score = r.score_total || 0;
+        const type = (r.type || 'idea').toUpperCase();
+        const tColor = { CONNECTION: 'var(--metal-glow)', PRODUCT: '#7ab8a3', AUTOMATION: '#7b9bc4', RENAME: '#c4a07b', KILL: '#c47b7b' }[type] || 'var(--metal)';
+        return `<div style="padding:10px 0;border-bottom:1px dotted var(--rule);display:grid;grid-template-columns:46px 80px 1fr;gap:14px;align-items:start">
+          <span style="font-family:var(--mono);font-size:14px;color:var(--metal-glow)">${score}</span>
+          <span style="font-family:var(--mono);font-size:9px;letter-spacing:.2em;color:${tColor};padding-top:4px">${escHtml(type)}</span>
+          <div>
+            <div style="font-family:var(--serif);font-size:15px;font-style:italic">${escHtml(r.title || '(untitled)')}</div>
+            ${r.body ? `<div style="font-size:11px;color:var(--ink-mute);margin-top:4px;line-height:1.5">${escHtml(String(r.body).slice(0,260))}${String(r.body).length>260?'…':''}</div>` : ''}
+          </div>
+        </div>`;
+      }).join('');
+    }
+  } catch(e) {
+    document.getElementById('idea-feed').innerHTML = '<p style="color:#c47b7b">idea-loop bridge unavailable</p>';
+  }
+
   // News scrape calendar — 90-day heatmap above the heatmap section
   try {
     const cal = await fetch('/api/news/by-day').then(r => r.json());
diff --git a/src/server/index.ts b/src/server/index.ts
index 53cdf3e..070c535 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -5126,6 +5126,29 @@ app.get('/api/news/archive', async (_req, res) => {
   }
 });
 
+// Bridge to the idea-loop skill at ~/.claude/skills/idea-loop/data/ideas.jsonl.
+// Reads the JSONL on demand (not large, ~kb scale), returns last N parsed
+// entries. Pure read; idea-loop owns its own writes.
+app.get('/api/ideas/recent', async (_req, res) => {
+  try {
+    const fs = await import('node:fs/promises');
+    const path = await import('node:path');
+    const home = process.env.HOME || '/Users/stevestudio2';
+    const jsonlPath = path.join(home, '.claude', 'skills', 'idea-loop', 'data', 'ideas.jsonl');
+    let raw = '';
+    try { raw = await fs.readFile(jsonlPath, 'utf8'); }
+    catch { return res.json({ count: 0, rows: [], note: 'idea-loop not yet running' }); }
+    const ideas = raw.split(/\r?\n/).filter(Boolean).map((line) => {
+      try { return JSON.parse(line); } catch { return null; }
+    }).filter(Boolean);
+    ideas.sort((a, b) => (b.score_total ?? 0) - (a.score_total ?? 0));
+    res.set('Cache-Control', 'public, max-age=60');
+    res.json({ count: ideas.length, rows: ideas.slice(0, 20) });
+  } catch (e: any) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
 // 90-day daily news-fetch heatmap. /coverage.html renders it as a GitHub-
 // style grid so editors see "is the scraper actually landing fresh stuff?"
 app.get('/api/news/by-day', async (_req, res) => {

← f9ee539 feat(news): /api/news/by-day + 90-day calendar heatmap on /c  ·  back to Ventura Corridor  ·  feat(news): news counts in homepage build-status strip b8a21eb →