← back to Slack Idea Board
idea-board: dedupe by URL + full pagination + Actionable/Yes/All filters (default Actionable); pinned under pm2
8231daf9273a1f39dea28ec5e446ba8e5cdbb7d3 · 2026-07-13 00:49:57 -0700 · Steve Abrams
Files touched
M public/index.htmlM server.js
Diff
commit 8231daf9273a1f39dea28ec5e446ba8e5cdbb7d3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jul 13 00:49:57 2026 -0700
idea-board: dedupe by URL + full pagination + Actionable/Yes/All filters (default Actionable); pinned under pm2
---
public/index.html | 36 +++++++++++++++++++++++++++++++-----
server.js | 24 ++++++++++++++++++++----
2 files changed, 51 insertions(+), 9 deletions(-)
diff --git a/public/index.html b/public/index.html
index f57c194..f0e6aee 100644
--- a/public/index.html
+++ b/public/index.html
@@ -39,12 +39,21 @@
.detail{margin-top:6px;color:var(--dim)}
.pending{color:var(--dim);font-style:italic}
.empty{padding:40px;text-align:center;color:var(--dim)}
+ .filters{display:inline-flex;gap:6px}
+ .filters button{background:var(--chip);color:var(--dim);border:1px solid var(--line);border-radius:999px;
+ padding:3px 11px;font-size:12px;cursor:pointer}
+ .filters button.on{background:var(--gold);color:#1a1a1a;border-color:var(--gold);font-weight:700}
</style>
</head>
<body>
<header>
<h1><b>Idea Board</b> — Slack feed triage</h1>
<span class="meta" id="stat">loading…</span>
+ <span class="filters" id="filters">
+ <button data-f="actionable" class="on">Actionable</button>
+ <button data-f="yes">Yes only</button>
+ <button data-f="all">All</button>
+ </span>
<span class="spacer"></span>
<span class="live"><span class="dot"></span> live · auto-refresh 20s</span>
</header>
@@ -60,14 +69,32 @@
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]));
function fmt(iso){try{return new Date(iso).toLocaleString(undefined,{month:'short',day:'numeric',hour:'numeric',minute:'2-digit'})}catch{return iso}}
+let LAST = { ideas:[], model:'' }, FILTER = 'actionable';
+function passes(i){
+ if(FILTER==='all') return true;
+ if(i.pending) return true; // keep visible until judged
+ if(FILTER==='yes') return i.fit==='Yes';
+ return i.fit!=='No' && !/^skip$/i.test(i.next_step||''); // actionable
+}
+document.getElementById('filters').addEventListener('click', e=>{
+ if(e.target.tagName!=='BUTTON') return;
+ FILTER=e.target.dataset.f;
+ [...document.querySelectorAll('#filters button')].forEach(b=>b.classList.toggle('on', b.dataset.f===FILTER));
+ render();
+});
async function load(){
try{
- const r = await fetch('/api/ideas'); const d = await r.json();
+ const r = await fetch('/api/ideas'); LAST = await r.json(); render();
+ }catch(e){ document.getElementById('stat').textContent='error: '+e.message; }
+}
+function render(){
+ const d = LAST;
+ const shown = d.ideas.filter(passes);
document.getElementById('stat').textContent =
- `${d.count} ideas · ${d.pending} analyzing · model ${d.model}`;
+ `${shown.length} shown / ${d.count} total · ${d.pending} analyzing · ${d.model}`;
const rows = document.getElementById('rows');
- if(!d.ideas.length){ rows.innerHTML='<tr><td colspan=3 class=empty>No links posted yet. Drop an x.com/GitHub link in #claude-to-steve or #claude-chat.</td></tr>'; return; }
- rows.innerHTML = d.ideas.map(i=>{
+ 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 or #claude-chat.</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>`;
@@ -85,7 +112,6 @@ async function load(){
<td class="col3">${c3}</td>
</tr>`;
}).join('');
- }catch(e){ document.getElementById('stat').textContent='error: '+e.message; }
}
load(); setInterval(load, 20000);
</script>
diff --git a/server.js b/server.js
index 7f45c0c..80d96d7 100644
--- a/server.js
+++ b/server.js
@@ -76,9 +76,15 @@ Respond with ONLY a JSON object, no prose:
async function collectIdeas() {
const ideas = [];
for (const cid of CHANNELS) {
- const h = await slack('conversations.history', { channel: cid, limit: '200' });
- if (!h.ok) continue;
- for (const m of h.messages || []) {
+ let cursor = '', msgs = [];
+ for (let page = 0; page < 20; page++) { // paginate ALL history (up to 20×200)
+ const h = await slack('conversations.history', { channel: cid, limit: '200', cursor });
+ if (!h.ok) break;
+ msgs.push(...(h.messages || []));
+ cursor = h.response_metadata && h.response_metadata.next_cursor;
+ if (!cursor) break;
+ }
+ for (const m of msgs) {
if (m.subtype || m.bot_id) continue;
if (STEVE && m.user !== STEVE) continue;
const url = firstUrl(m); if (!url) continue;
@@ -88,7 +94,17 @@ async function collectIdeas() {
}
}
ideas.sort((a, b) => parseFloat(b.ts) - parseFloat(a.ts));
- return ideas;
+ // DEDUPE by canonical URL (strip tracking params, www, trailing slash) — keep newest.
+ const seen = new Set(), unique = [];
+ for (const i of ideas) { const k = canon(i.url); if (seen.has(k)) continue; seen.add(k); unique.push(i); }
+ return unique;
+}
+function canon(u) {
+ try {
+ const x = new URL(u); x.hash = '';
+ for (const p of [...x.searchParams.keys()]) if (/^(fbclid|utm_|s|t|si|feature|ref|ref_src|ref_url|mibextid)$/i.test(p)) x.searchParams.delete(p);
+ return (x.hostname.replace(/^www\./, '') + x.pathname.replace(/\/$/, '') + (x.search || '')).toLowerCase();
+ } catch { return (u || '').toLowerCase(); }
}
let ENRICH_BUSY = false;
← edba999 slack-idea-board: 3-column Slack-feed idea triage (link/abou
·
back to Slack Idea Board
·
idea-board: parallel enrichment (4 concurrent) + continuous c2393df →