[object Object]

← 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

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 →