← back to Approvals Viewer
add top-10 fan-out rollup board (:9793) tracking TK-10012 task states
9ff42144729c4f380df8ea3f18446140a870012f · 2026-07-28 10:02:44 -0700 · Steve Abrams
Files touched
Diff
commit 9ff42144729c4f380df8ea3f18446140a870012f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Jul 28 10:02:44 2026 -0700
add top-10 fan-out rollup board (:9793) tracking TK-10012 task states
---
rollup.js | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 86 insertions(+)
diff --git a/rollup.js b/rollup.js
new file mode 100644
index 0000000..12e1584
--- /dev/null
+++ b/rollup.js
@@ -0,0 +1,86 @@
+#!/usr/bin/env node
+// Top-10 fan-out rollup — one-glance convergence board for TK-10012's 10 tasks.
+// Polls the ticket board (:9794), finds the parent ticket, and derives each task's
+// state from the LATEST action/comment line that mentions "task N/10". Shows a
+// summary bar: opened · prepped · awaiting-GO · done. Auto-refreshes. Zero deps.
+const http = require('http');
+
+const PORT = Number(process.env.PORT || 9793);
+const PARENT = process.env.PARENT || 'TK-10012';
+const BOARD = { host: '127.0.0.1', port: 9794, auth: 'admin:DW2024!' };
+
+function boardTickets() {
+ return new Promise((resolve) => {
+ const req = http.get({ host: BOARD.host, port: BOARD.port, path: '/api/tickets',
+ headers: { Authorization: 'Basic ' + Buffer.from(BOARD.auth).toString('base64') } }, (r) => {
+ let b = ''; r.on('data', c => b += c); r.on('end', () => { try { resolve(JSON.parse(b)); } catch { resolve([]); } });
+ });
+ req.on('error', () => resolve([]));
+ req.setTimeout(4000, () => { req.destroy(); resolve([]); });
+ });
+}
+function classify(text) {
+ const s = (text || '').toLowerCase();
+ if (/\b(done|shipped|executed|landed|complete[d]?|✅)\b/.test(s)) return 'done';
+ if (/\b(await|go\s*\/\s*no|go-?no|gated|needs steve|awaiting steve|blocked|approve to|steve.?s go)\b/.test(s)) return 'awaiting-GO';
+ if (/\b(prepped|staged|dry-?run|verified|ready|preconditions|diagnos)\b/.test(s)) return 'prepped';
+ return 'opened';
+}
+async function rollup() {
+ const t = (await boardTickets()).find(x => x.id && x.id.startsWith(PARENT));
+ const tasks = {};
+ if (t) {
+ const acts = [].concat(t.actions || [], t.comments || [])
+ .map(a => ({ ts: +new Date(a.ts), text: a.text || '' }))
+ .sort((a, b) => a.ts - b.ts);
+ for (const a of acts) {
+ const m = a.text.match(/task\s*(\d{1,2})\s*\/\s*10/i);
+ if (m) { const n = +m[1]; tasks[n] = { text: a.text, ts: a.ts, state: classify(a.text) }; }
+ }
+ }
+ const rows = [];
+ for (let n = 1; n <= 10; n++) rows.push(Object.assign({ n, state: 'opened', text: '(no update yet)', ts: 0 }, tasks[n] || {}));
+ return { found: !!t, id: t ? t.id : PARENT, status: t ? t.status : '?', rows };
+}
+
+const STATECOLOR = { opened: '#64748b', prepped: '#3b82f6', 'awaiting-GO': '#d97706', done: '#16a34a' };
+const esc = s => String(s).replace(/[&<>]/g, c => ({ '&': '&', '<': '<', '>': '>' }[c]));
+
+http.createServer(async (req, res) => {
+ if (req.url === '/api/rollup') {
+ const r = await rollup();
+ res.writeHead(200, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify(r));
+ }
+ res.writeHead(200, { 'Content-Type': 'text/html' });
+ res.end(`<!doctype html><meta charset=utf8><meta name=viewport content="width=device-width,initial-scale=1">
+<title>Top-10 rollup</title><style>
+body{margin:0;font:15px/1.5 -apple-system,Segoe UI,Roboto,sans-serif;background:#0b1020;color:#e7ecf5}
+.wrap{max-width:900px;margin:0 auto;padding:18px}h1{font-size:18px;margin:0 0 2px}.sub{color:#94a3b8;font-size:13px}
+.bar{display:flex;gap:10px;margin:14px 0;flex-wrap:wrap}
+.pill{padding:8px 14px;border-radius:10px;background:#0f172a;border:1px solid #1e293b;font-weight:700}
+.row{display:flex;gap:12px;align-items:flex-start;background:#0f172a;border:1px solid #1e293b;border-radius:10px;padding:10px 14px;margin:8px 0}
+.n{font-weight:700;color:#64748b;min-width:28px}.st{font-size:11px;font-weight:700;padding:3px 9px;border-radius:999px;white-space:nowrap}
+.tx{color:#cbd5e1;font-size:13px;flex:1}.ts{color:#475569;font-size:11px}
+</style><div class=wrap>
+<h1>🎯 Top-10 approved-drafts fan-out</h1><div class=sub id=head>loading…</div>
+<div class=bar id=bar></div><div id=list></div>
+<div class=sub style="margin-top:14px">auto-refresh 5s · board <a style=color:#60a5fa href="http://127.0.0.1:9794">:9794</a></div>
+</div><script>
+const TITLES=${JSON.stringify([
+ 'HomesOnSpec prod cutover','Consulting portal deploy','DW signup/sample/rep go-live',
+ 'Strip stale Needs-Image (16k)','Live SKU collisions fix','GMC feed remediation',
+ 'Prod provenance migration','Sample-only backlog → sellable','De-badge 36 PR items','York cadence (622)'])};
+const C=${JSON.stringify(STATECOLOR)};
+async function tick(){
+ const r=await (await fetch('/api/rollup')).json();
+ document.getElementById('head').textContent=(r.found?r.id+' · ticket '+r.status:'ticket not found ('+r.id+')');
+ const c={opened:0,prepped:0,'awaiting-GO':0,done:0};r.rows.forEach(x=>c[x.state]++);
+ document.getElementById('bar').innerHTML=Object.keys(c).map(k=>'<span class=pill style="color:'+C[k]+'">'+c[k]+' '+k+'</span>').join('');
+ document.getElementById('list').innerHTML=r.rows.map(x=>'<div class=row><div class=n>'+x.n+'</div>'
+ +'<span class=st style="background:'+C[x.state]+'22;color:'+C[x.state]+'">'+x.state+'</span>'
+ +'<div class=tx><b>'+esc(TITLES[x.n-1]||'')+'</b><br><span class=ts>'+(x.text||'').slice(0,180)+'</span></div></div>').join('');
+}
+function esc(s){return String(s).replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c]));}
+tick();setInterval(tick,5000);
+</script>`);
+}).listen(PORT, () => console.log('rollup on http://localhost:' + PORT + ' (parent ' + PARENT + ')'));
← a0197a4 approvals-viewer: fix blank page — nested-quote SyntaxError
·
back to Approvals Viewer
·
rollup: detect gated packet-memos per task (real convergence 4529ded →