[object Object]

← 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

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