[object Object]

← back to Desktop Dotbar

dotbar: ticket summary centered in the middle band, 5 live states (blocked/open/idle/doing/parked), fix auth + 8MB buffer for the 4.5MB board payload

034744417eebd24dd05b595488340a1e56fc0895 · 2026-09-23 10:12:52 -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 034744417eebd24dd05b595488340a1e56fc0895
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 23 10:12:52 2026 -0700

    dotbar: ticket summary centered in the middle band, 5 live states (blocked/open/idle/doing/parked), fix auth + 8MB buffer for the 4.5MB board payload
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_017VyZZwbLBMfoBmdLGuxBuD
---
 public/index.html | 36 ++++++++++++++++++++++--------------
 server.js         | 40 +++++++++++++++++++++++-----------------
 2 files changed, 45 insertions(+), 31 deletions(-)

diff --git a/public/index.html b/public/index.html
index 477d0b1..147cee7 100644
--- a/public/index.html
+++ b/public/index.html
@@ -67,7 +67,7 @@
   .cnt { font-variant-numeric:tabular-nums; font-weight:700; font-size:14px; }
   .nm { color:var(--dim); font-size:11px; }
   /* the flexible gap that pushes #meta to the far end (right on top; bottom on side) */
-  #spacer { flex:1; -webkit-app-region:drag; }
+  #spacer, #spacer2 { flex:1; -webkit-app-region:drag; }
   #meta { -webkit-app-region:no-drag; color:var(--dim); font-size:11px; display:flex; gap:10px; align-items:center; }
   body.orient-left #meta, body.orient-right #meta { flex-direction:column; align-items:stretch; gap:6px; }
   #meta b { color:var(--fg); font-variant-numeric:tabular-nums; }
@@ -123,7 +123,7 @@ let ram = { ram_pct: '–', cpu_pct: '–', on: false, hogs: [] };
 // 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 };
+let tickets = { updated: 0, blocked: 0, open: 0, idle: 0, doing: 0, parked: 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' };
@@ -163,17 +163,6 @@ 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).
@@ -190,7 +179,26 @@ function renderBar(){
   snbtn.textContent = ram.on ? '🔫 ON' : '🔫 OFF';
   snbtn.onclick = toggleSniper;
   bar.appendChild(snbtn);
-  const sp = document.createElement('div'); sp.id='spacer'; bar.appendChild(sp);
+  // Left spacer, then the ticket-board summary CENTERED in the middle band (Steve 2026-09-23),
+  // then a right spacer — the two-spacer flexbox trick centers tickets between the sniper button
+  // and #meta. Five live states: blocked / open / idle(stopped) / doing / parked. Click → fleet board.
+  const sp1 = document.createElement('div'); sp1.id='spacer'; bar.appendChild(sp1);
+  if (tickets.updated) {
+    const tchip = document.createElement('div');
+    tchip.className = 'chip tickets';
+    tchip.title = `Board — ${tickets.blocked} blocked · ${tickets.open} open · ${tickets.idle} idle · ${tickets.doing} doing · ${tickets.parked} parked${tickets.latest ? '\n↳ latest: ' + tickets.latest : ''}\nclick → open Fleet board`;
+    const seg = (emoji, n, color) => `<span style="color:${color};font-weight:700">${emoji}${n}</span>`;
+    tchip.innerHTML = `<span class="cnt" style="font-size:12px;display:flex;gap:9px;align-items:center">`
+      + seg('🔴', tickets.blocked, '#f87171')
+      + seg('⚪', tickets.open, '#e5e7eb')
+      + seg('⚫', tickets.idle, '#9ca3af')
+      + seg('🔵', tickets.doing, '#60a5fa')
+      + seg('🩷', tickets.parked, '#f9a8d4')
+      + `</span>`;
+    tchip.onclick = () => { window.open('http://127.0.0.1:9794/?section=all&layout=grid', 'fleet'); };
+    bar.appendChild(tchip);
+  }
+  const sp2 = document.createElement('div'); sp2.id='spacer2'; sp2.className='spacer-grow'; bar.appendChild(sp2);
   const meta = document.createElement('div'); meta.id='meta';
   const nextO = cfg.orientation==='top'?'left':cfg.orientation==='left'?'right':'top';
   meta.innerHTML = `<button class="arrbtn${arranging?' busy':''}" ${arranging?'disabled':''} title="run the master dot-screen arrangement now" onclick="arrange()">${arranging?'⧉ Arranging…':(arrangeMsg?('⧉ '+arrangeMsg):'⧉ Arrange master')}</button>`
diff --git a/server.js b/server.js
index 1fcad10..27330be 100755
--- a/server.js
+++ b/server.js
@@ -26,9 +26,9 @@ const META = {
   none:      { emoji: '⚪', css: '#8a8a8a', name: 'no dot' },
 };
 
-function run(cmd, args, timeoutMs = 8000) {
+function run(cmd, args, timeoutMs = 8000, maxBuffer = 1 << 20) {
   return new Promise((resolve) => {
-    execFile(cmd, args, { timeout: timeoutMs, maxBuffer: 1 << 20 }, (err, stdout) => {
+    execFile(cmd, args, { timeout: timeoutMs, maxBuffer }, (err, stdout) => {
       resolve({ err, stdout: stdout || '' });
     });
   });
@@ -187,29 +187,35 @@ 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 ticketsSnapshot = { updated: 0, blocked: 0, open: 0, idle: 0, doing: 0, parked: 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).
+// Fetch ticket-board summary: blocked / open / idle(stopped) / doing / parked + latest event.
+// Board needs Basic Auth (admin:DW2024!); rows carry status + a `parked` boolean flag.
 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 };
+  // The board returns the full ~4.5MB ticket array (it ignores fields=), so give curl an 8MB
+  // buffer — the default 1MB silently truncated the response and every count parsed as 0.
+  const { stdout: boardData } = await run('curl', ['-s', '-m', '4', '-u', 'admin:DW2024!',
+    'http://127.0.0.1:9794/api/tickets?status=doing,blocked,open,stopped'], 6000, 8 << 20);
+  let tickets = { blocked: 0, open: 0, idle: 0, doing: 0, parked: 0, latest: null };
   try {
     const j = JSON.parse(boardData || '[]');
     if (Array.isArray(j)) {
-      const g = { blocked: [], doing: [], open: [] };
+      let newest = null;
       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);
+        // Peel-out (mirrors the board / old TicketBar): a ticket with parked=true in an active
+        // status counts as PARKED and is removed from its status bucket. "idle" = stopped tickets.
+        const isParked = row.parked && ['open', 'blocked', 'doing'].includes(status);
+        if (isParked) tickets.parked++;
+        else if (status === 'blocked') tickets.blocked++;
+        else if (status === 'open') tickets.open++;
+        else if (status === 'doing') tickets.doing++;
+        else if (status === 'stopped') tickets.idle++;
+        const ts = Date.parse(row.updated_at || row.status_since || '') || 0;
+        if (!newest || ts > newest.ts) newest = { ts, label: row.title ? `${row.id}: ${row.title}` : row.id };
       }
-      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;
+      if (newest) tickets.latest = newest.label;
     }
   } catch (e) { /* keep last */ }
   return { updated: Date.now(), ...tickets };
@@ -274,7 +280,7 @@ function listen(port, tries = 20) {
     refreshRam();                    // warm RAM/CPU stats
     setInterval(refreshRam, 2000);   // keep RAM/CPU fresh
     refreshTickets();                // warm ticket board summary
-    setInterval(refreshTickets, 4000); // refresh board every 4s
+    setInterval(refreshTickets, 6000); // refresh board every 6s (~4.5MB payload; keep it light)
   });
 }
 listen(parseInt(process.env.PORT || '9787', 10));

← 4ad7b46 dotbar: merge ticket-board summary (BLOCKED/DOING/OPEN count  ·  back to Desktop Dotbar  ·  dotbar: each ticket state (blocked/open/idle/doing/parked) i fa6b41e →