← 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 & 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=>({'&':'&','<':'<','>':'>','"':'"'}[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)