[object Object]

← back to Slack Idea Board

idea-board: harden post/open links with scheme allowlist + ship #claude-to-steve column refactor

65a7c868306ccc423fe921c29414d1f7167a2ada · 2026-09-16 09:31:42 -0700 · Steve Abrams

safeUrl() blocks javascript:/data: hrefs (esc() only stopped attribute breakout,
not the scheme) on both the c1 post link and the c3 Open link — the board is
auth-gated and can POST /api/build, so a hostile feed URL must not be clickable script.

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

Files touched

Diff

commit 65a7c868306ccc423fe921c29414d1f7167a2ada
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 16 09:31:42 2026 -0700

    idea-board: harden post/open links with scheme allowlist + ship #claude-to-steve column refactor
    
    safeUrl() blocks javascript:/data: hrefs (esc() only stopped attribute breakout,
    not the scheme) on both the c1 post link and the c3 Open link — the board is
    auth-gated and can POST /api/build, so a hostile feed URL must not be clickable script.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 public/index.html | 95 +++++++++++++++++++++++++++----------------------------
 1 file changed, 46 insertions(+), 49 deletions(-)

diff --git a/public/index.html b/public/index.html
index 44465e3..f721902 100644
--- a/public/index.html
+++ b/public/index.html
@@ -26,7 +26,7 @@
 })();
 </script>
 <meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
-<title>Idea Board · Slack feed</title>
+<title>Idea Board · #claude-to-steve</title>
 <style>
   :root{--bg:#0f1117;--panel:#161922;--line:#262b38;--text:#e6e8ee;--dim:#9aa3b2;--gold:#c9a227;
     --yes:#2ecc71;--maybe:#e0b000;--no:#e05a5a;--chip:#1d2230;}
@@ -46,9 +46,14 @@
     text-transform:uppercase;color:var(--dim);padding:10px 16px;border-bottom:1px solid var(--line)}
   tbody td{padding:14px 16px;border-bottom:1px solid var(--line);vertical-align:top}
   tbody tr:hover{background:#12151d}
-  .col1{width:30%} .col2{width:22%} .col3{width:22%} .col4{width:26%}
+  .col1{width:46%} .col2{width:20%} .col3{width:34%}
   .aatarget{display:inline-block;margin-top:6px;font-size:11.5px;font-weight:600;color:var(--gold);
     border:1px solid var(--line);border-radius:999px;padding:2px 9px;background:var(--chip)}
+  /* verbatim post text — the exact words Steve pasted, preserved with line breaks */
+  .post{white-space:pre-wrap;word-break:break-word;color:var(--text);margin:2px 0 6px;
+    font-size:13.5px;line-height:1.5;border-left:2px solid var(--line);padding-left:10px}
+  .author{display:inline-block;font-size:10px;color:var(--dim);border:1px solid var(--line);
+    border-radius:999px;padding:1px 7px;margin-left:6px}
   a.link{color:#7db4ff;text-decoration:none;font-weight:600;word-break:break-word}
   a.link:hover{text-decoration:underline}
   .about{color:var(--text);margin-top:5px}
@@ -97,15 +102,13 @@
 </head>
 <body>
 <header>
-  <h1><b>Idea Board</b> — Slack feed triage</h1>
+  <h1><b>Idea Board</b> — exact posts from <b>#claude-to-steve</b></h1>
   <span class="meta" id="stat">loading…</span>
   <input type="search" id="q" class="search" placeholder="Search ideas, links, reasons, targets…" autocomplete="off" spellcheck="false">
   <span class="filters" id="filters">
-    <button data-f="actionable" class="on">Actionable</button>
-    <button data-f="yes">Yes only</button>
-    <button data-f="aayes" title="Only ideas judged a Yes for an Agent Abrams build / existing project">AA · Yes</button>
-    <button data-f="aafit" title="Ideas that fit an Agent Abrams build / project — Yes or Maybe">AA · fit</button>
-    <button data-f="all">All</button>
+    <button data-f="actionable" class="on" title="Good builds — Yes or Maybe (hides the No's)">Actionable</button>
+    <button data-f="yes" title="Only posts judged a clear Yes to build">Good builds</button>
+    <button data-f="all">All posts</button>
   </span>
   <span class="spacer"></span>
   <button id="accel" title="Launch the Claude web-dev accelerator in a new terminal to rapidly prototype & launch a client project" onclick="launchAccelerator(this)">🚀 Launch Web-Dev Accelerator</button>
@@ -113,17 +116,19 @@
 </header>
 <table>
   <thead><tr>
-    <th class="col1">1 · Link &amp; what the idea is</th>
-    <th class="col2">2 · Good for your builds? Why</th>
-    <th class="col3">3 · Good for an Agent Abrams build / project?</th>
-    <th class="col4">4 · Next step</th>
+    <th class="col1">The post (exact)</th>
+    <th class="col2">Good build?</th>
+    <th class="col3">What it's good for</th>
   </tr></thead>
-  <tbody id="rows"><tr><td colspan="4" class="empty">Loading your feed…</td></tr></tbody>
+  <tbody id="rows"><tr><td colspan="3" class="empty">Loading #claude-to-steve…</td></tr></tbody>
 </table>
 <div id="toast" role="status" aria-live="polite"></div>
 <script>
 const stepClass = s => /build new project/i.test(s)?'build':/skill/i.test(s)?'skill':/agent/i.test(s)?'agent':'skip';
 const esc = s => (s||'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
+// scheme-allowlist a URL before it goes in an href — esc() stops attribute breakout but NOT a
+// javascript:/data: scheme, which would run in this authenticated, /api/build-capable origin.
+const safeUrl = u => /^https?:\/\//i.test(u||'') ? u : '#';
 // Build a ready-to-run Claude Code kickoff command for an idea's next_step. SAFE — this is
 // only a STRING copied to the clipboard; nothing here shells out or executes anything.
 function kickoffCmd(i){
@@ -184,7 +189,7 @@ async function launchAction(id, step, label, btn){
       if(Date.now() + RETRY_MS > deadline) break;    // give up → falls through to the throttle toast
       for(let s=Math.round(RETRY_MS/1000); s>0; s--){ setLbl(`throttled · retry ${s}s`); await sleep(1000); }
     }
-    if(j.launched){ toast(`🚀 <b>${esc(label||step)}</b> launched in a new iTerm2 window<br><code>${esc((j.prompt||'').slice(0,160))}…</code>`); }
+    if(j.launched){ toast(`🚀 <b>${esc(label||step)}</b> launched in a new tab of the Idea Builds window<br><code>${esc((j.prompt||'').slice(0,160))}…</code>`); }
     else if(j.localOnly){ toast('🔒 '+esc(j.reason||'spawn is localhost-only')+'<br>Tip: Shift-click to copy the command instead.'); }
     else if(j.throttled){ toast('⏳ Still throttled after retries — '+esc(j.reason||'try again shortly')); }
     else if(j.needsConfirm){ toast('Launch blocked — server requires confirm'); }
@@ -206,20 +211,17 @@ let LAST = { ideas:[], model:'' }, FILTER = 'actionable', SEARCH = '';
 // Free-text search over every caption field the model produced for an idea.
 function matchesSearch(i){
   if(!SEARCH) return true;
-  const hay = [i.title,i.about,i.text,i.note,i.url,i.channel,i.fit,i.fit_reason,
-    i.aa_fit,i.aa_target,i.aa_reason,i.next_step,i.next_detail].filter(Boolean).join(' ').toLowerCase();
+  const hay = [i.post_text,i.title,i.about,i.text,i.note,i.url,i.channel,i.author,
+    i.good_build,i.good_build_reason,i.good_for,i.good_for_target,i.next_step,i.next_detail]
+    .filter(Boolean).join(' ').toLowerCase();
   return hay.includes(SEARCH);
 }
 function passes(i){
   if(!matchesSearch(i)) return false; // search gates first, independent of filter/pending
   if(FILTER==='all') return true;
-  if(i.pending) return true; // whole record not judged yet
-  if(FILTER==='aayes' || FILTER==='aafit'){
-    if(i.aa_fit===undefined) return true; // AA verdict still backfilling — keep visible
-    return FILTER==='aayes' ? i.aa_fit==='Yes' : (i.aa_fit==='Yes' || i.aa_fit==='Maybe');
-  }
-  if(FILTER==='yes') return i.fit==='Yes';
-  return i.fit!=='No' && !/^skip$/i.test(i.next_step||''); // actionable
+  if(i.pending || i.good_build===undefined) return true; // not judged yet — keep visible
+  if(FILTER==='yes') return i.good_build==='Yes';
+  return i.good_build!=='No'; // actionable = Yes or Maybe
 }
 document.getElementById('filters').addEventListener('click', e=>{
   if(e.target.tagName!=='BUTTON') return;
@@ -239,41 +241,36 @@ function render(){
     document.getElementById('stat').textContent =
       `${shown.length} shown / ${d.count} total · ${d.pending} analyzing · ${d.model}`;
     const rows = document.getElementById('rows');
-    if(!shown.length){ rows.innerHTML='<tr><td colspan=4 class=empty>Nothing in this filter yet. Drop an x.com/GitHub link in #claude-to-steve or #claude-chat.</td></tr>'; return; }
+    if(!shown.length){ rows.innerHTML='<tr><td colspan=3 class=empty>Nothing in this filter yet. Drop an x.com / GitHub link in #claude-to-steve.</td></tr>'; return; }
     rows.innerHTML = shown.map(i=>{
-      const host = (()=>{try{return new URL(i.url).hostname.replace('www.','')}catch{return i.url}})();
-      const c2 = i.pending ? '<span class="pending">analyzing…</span>'
-        : `<span class="fit ${i.fit}">${esc(i.fit)}</span><div class="reason">${esc(i.fit_reason)}</div>`;
-      // Agent Abrams / existing-project fit. aa_fit may be undefined on records enriched
-      // before this column existed — those show "analyzing…" until the backfill fills them.
-      const cAA = (i.pending || i.aa_fit===undefined) ? '<span class="pending">analyzing…</span>'
-        : `<span class="fit ${i.aa_fit}">${esc(i.aa_fit)}</span>`
-          + (i.aa_target && i.aa_target!=='—' ? `<div class="aatarget">${esc(i.aa_target)}</div>` : '')
-          + (i.aa_reason ? `<div class="reason">${esc(i.aa_reason)}</div>` : '');
-      const c3 = i.pending ? '<span class="pending">…</span>'
+      const host = (()=>{try{return new URL(i.url).hostname.replace('www.','')}catch{return i.url||''}})();
+      // Column 1 — the EXACT post, verbatim, plus the unfurled context + link + timestamp.
+      const postBody = i.post_text || i.note || '';
+      const c1 = `${postBody?`<div class="post">${esc(postBody)}</div>`:''}`
+        + (i.url?`<a class="link" href="${esc(safeUrl(i.url))}" target="_blank" rel="noopener noreferrer">${esc(i.title||host)} ↗</a>`:'')
+        + (i.about && i.about!==postBody ? `<div class="about">${esc(i.about)}</div>` : (i.text?`<div class="about">${esc(i.text)}</div>`:''))
+        + `<div class="when">🕓 ${fmt(i.when)}<span class="author">${esc(i.author||'')}</span><span class="chan">${esc(i.channel)}</span></div>`;
+      // Column 2 — Good build? (the "if" verdict + why).
+      const c2 = (i.pending || i.good_build===undefined) ? '<span class="pending">analyzing…</span>'
+        : `<span class="fit ${i.good_build}">${esc(i.good_build)}</span>`
+          + (i.good_build_reason?`<div class="reason">${esc(i.good_build_reason)}</div>`:'');
+      // Column 3 — What it's good for (concrete build/project targets) + the one-click launch action.
+      const c3 = (i.pending || i.good_build===undefined) ? '<span class="pending">…</span>'
         : (()=>{
+            const target = (i.good_for_target && i.good_for_target!=='—') ? `<div class="aatarget">${esc(i.good_for_target)}</div>` : '';
+            const good = i.good_for ? `<div class="reason">${esc(i.good_for)}</div>` : '';
             const step = esc(i.next_step||''), sc = stepClass(i.next_step);
             const cmd = kickoffCmd(i);
             const isSkip = sc==='skip' || !cmd;
-            const primary = isSkip
-              ? `<span class="act skip" title="${esc(i.next_detail||'No action for this idea')}">Skip</span>`
+            const primary = isSkip ? ''
               : `<button class="act ${sc}" data-id="${esc(i.id)}" data-step="${esc(i.next_step||'')}" data-cmd="${esc(cmd)}" data-label="${step}" title="Click to LAUNCH a Claude session in a new terminal · Shift-click to copy the command instead">${step} ▶</button>`;
-            const open = i.url ? `<a class="act open" href="${esc(i.url)}" target="_blank" rel="noopener noreferrer">Open link ↗</a>` : '';
-            return `<span class="step ${sc}">${step}</span>`
-              + `<div class="detail">${esc(i.next_detail)}</div>`
-              + `<div class="actions">${primary}${open}</div>`;
+            const open = i.url ? `<a class="act open" href="${esc(safeUrl(i.url))}" target="_blank" rel="noopener noreferrer">Open ↗</a>` : '';
+            return target + good + `<div class="actions">${primary}${open}</div>`;
           })();
       return `<tr>
-        <td class="col1">
-          <a class="link" href="${esc(i.url)}" target="_blank" rel="noopener noreferrer">${esc(i.title||host)} ↗</a>
-          <span class="chan">${esc(i.channel)}</span>
-          <div class="about">${esc(i.about||i.text||'')}</div>
-          ${i.note?`<div class="note">“${esc(i.note.slice(0,140))}”</div>`:''}
-          <div class="when">🕓 ${fmt(i.when)}</div>
-        </td>
+        <td class="col1">${c1}</td>
         <td class="col2">${c2}</td>
-        <td class="col3">${cAA}</td>
-        <td class="col4">${c3}</td>
+        <td class="col3">${c3}</td>
       </tr>`;
     }).join('');
 }

← b9fa503 slack-idea-board: fix stale aa_fit->good_build comment + ser  ·  back to Slack Idea Board  ·  (newest)