← back to Desktop Dotbar
dotbar: merge ticket-board summary (BLOCKED/DOING/OPEN counts) + retire TicketBar (launchd bootout, code preserved)
4ad7b466d433b49d4a703d60fb2cd2981e6b6fc1 · 2026-09-23 10:05:26 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VyZZwbLBMfoBmdLGuxBuD
Files touched
M public/index.htmlM server.js
Diff
commit 4ad7b466d433b49d4a703d60fb2cd2981e6b6fc1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 23 10:05:26 2026 -0700
dotbar: merge ticket-board summary (BLOCKED/DOING/OPEN counts) + retire TicketBar (launchd bootout, code preserved)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VyZZwbLBMfoBmdLGuxBuD
---
public/index.html | 17 +++++++++++++++++
server.js | 29 +++++++++++++++++++++++++++++
2 files changed, 46 insertions(+)
diff --git a/public/index.html b/public/index.html
index d971a5d..477d0b1 100644
--- a/public/index.html
+++ b/public/index.html
@@ -122,11 +122,14 @@ let ram = { ram_pct: '–', cpu_pct: '–', on: false, hogs: [] };
// RAM pressure bands (Steve 2026-09-23): green < 50, amber < 75, red >= 75. Input is the
// memory-pressure-based used% from the server, so these thresholds mean real strain, not cache.
function ramColor(p){ p = parseFloat(p); if (isNaN(p)) return 'var(--fg)'; return p < 50 ? '#4ade80' : (p < 75 ? '#fbbf24' : '#f87171'); }
+// Ticket-board summary (BLOCKED / DOING / OPEN counts + latest event), polled from /api/tickets (Steve 2026-09-23).
+let tickets = { updated: 0, blocked: 0, doing: 0, open: 0, latest: null };
// The four needs-Steve states pulse; green/pink stay solid (Steve 2026-09-15 dot-flash directive).
const WAIT = new Set(['lightblue','orange','purple','yellow']);
const ORIENT_LABEL = { top: '▲ Top', left: '◀ Left', right: '▶ Right' };
async function fetchDots(){ try { const r = await fetch('/api/dots'); return await r.json(); } catch { return null; } }
+async function fetchTickets(){ try { const r = await fetch('/api/tickets'); return await r.json(); } catch { return null; } }
function resize(open){
// Electron shell owns the window bounds via IPC; fall back to resizeTo in a plain browser.
@@ -160,6 +163,17 @@ function renderBar(){
pchip.onclick = () => toggle('parked');
bar.appendChild(pchip);
}
+ // Ticket-board summary chips (Steve 2026-09-23): BLOCKED / DOING / OPEN counts + link to board.
+ // Show only if updated. Click opens the fleet viewer at 127.0.0.1:9794.
+ if (tickets.updated) {
+ const tchip = document.createElement('div');
+ tchip.className = 'chip';
+ tchip.title = `Board: ${tickets.blocked} blocked, ${tickets.doing} doing, ${tickets.open} open${tickets.latest ? ' • ' + tickets.latest : ''}`;
+ tchip.innerHTML = `<span class="dot" style="background:#9ca3af;opacity:0.7"></span>`
+ + `<span class="cnt" style="font-size:11px">🔴${tickets.blocked} 🔵${tickets.doing} ⚪${tickets.open}</span>`;
+ tchip.onclick = () => { window.open('http://127.0.0.1:9794/?section=all&layout=grid', 'fleet'); };
+ bar.appendChild(tchip);
+ }
// RAM Sniper lives in the LEFT chip cluster (always visible on the compact bar; the
// right-side #meta gets clipped off the pill). RAM+CPU are ONE atomic .cnt span so
// they can never render/clip asymmetrically (the earlier split hid RAM, showed CPU).
@@ -318,6 +332,9 @@ async function tick(){
const d = await fetchDots();
if (!d) return;
data = d;
+ // Fetch ticket-board summary in parallel (Steve 2026-09-23).
+ const t = await fetchTickets();
+ if (t && t.updated) tickets = t;
const openStillHasItems = openColor==='ram'
? true // RAM panel is always valid; never auto-close it
: openColor==='parked'
diff --git a/server.js b/server.js
index f81100b..1fcad10 100755
--- a/server.js
+++ b/server.js
@@ -187,8 +187,34 @@ function send(res, code, body, type = 'application/json') {
// serve a cached snapshot — /api/dots must return instantly for a smooth bar.
let snapshot = { updated: 0, total: 0, groups: [], stale: true };
let ramSnapshot = { updated: 0, ram_pct: 0, cpu_pct: 0, on: false, hogs: [] };
+let ticketsSnapshot = { updated: 0, blocked: 0, doing: 0, open: 0, latest: null };
let refreshing = false;
async function refreshRam() { try { ramSnapshot = await getRam(); } catch (e) { /* keep last */ } }
+// Fetch ticket-board summary (BLOCKED / DOING / OPEN counts + latest event).
+async function getTickets() {
+ const { stdout: boardData } = await run('curl', ['-s', '-m', '4', 'http://127.0.0.1:9794/api/tickets?status=doing,blocked,open&fields=summary']);
+ let tickets = { blocked: 0, doing: 0, open: 0, latest: null };
+ try {
+ const j = JSON.parse(boardData || '[]');
+ if (Array.isArray(j)) {
+ const g = { blocked: [], doing: [], open: [] };
+ for (const row of j) {
+ const status = row.status || '';
+ if (status === 'blocked' && g.blocked.length < 100) g.blocked.push(row);
+ else if (status === 'doing' && g.doing.length < 100) g.doing.push(row);
+ else if (status === 'open' && g.open.length < 100) g.open.push(row);
+ }
+ tickets.blocked = g.blocked.length;
+ tickets.doing = g.doing.length;
+ tickets.open = g.open.length;
+ // Latest = newest event from any status group
+ const all = [...g.blocked, ...g.doing, ...g.open].sort((a, b) => (b.updated_ts || 0) - (a.updated_ts || 0));
+ if (all[0]) tickets.latest = all[0].summary || `TK-${all[0].id}` || null;
+ }
+ } catch (e) { /* keep last */ }
+ return { updated: Date.now(), ...tickets };
+}
+async function refreshTickets() { try { ticketsSnapshot = await getTickets(); } catch (e) { /* keep last */ } }
async function refresh() {
if (refreshing) return;
refreshing = true;
@@ -213,6 +239,7 @@ const server = http.createServer(async (req, res) => {
if (url.pathname === '/api/ram/toggle' && req.method === 'POST') {
const r = await toggleSniper(); await refreshRam(); return send(res, 200, { ...ramSnapshot, ...r });
}
+ if (url.pathname === '/api/tickets') { if (!ticketsSnapshot.updated) await refreshTickets(); return send(res, 200, ticketsSnapshot); }
if (url.pathname === '/api/reveal' && req.method === 'POST') {
let raw = '';
req.on('data', c => (raw += c));
@@ -246,6 +273,8 @@ function listen(port, tries = 20) {
setInterval(refresh, 2500); // keep it fresh in the background
refreshRam(); // warm RAM/CPU stats
setInterval(refreshRam, 2000); // keep RAM/CPU fresh
+ refreshTickets(); // warm ticket board summary
+ setInterval(refreshTickets, 4000); // refresh board every 4s
});
}
listen(parseInt(process.env.PORT || '9787', 10));
← b05080a dotbar: RAM chip shows real memory pressure, not cache-infla
·
back to Desktop Dotbar
·
dotbar: ticket summary centered in the middle band, 5 live s 0347444 →