← 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
M public/index.htmlM server.js
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 →