[object Object]

← back to Desktop Dotbar

bar: add dw / non-dw ticket chips under the five state chips

a5d20f8afcd00723511d13cbd57b318599159cff · 2026-09-25 11:48:22 -0700 · Steve Abrams

Same active population cut by business instead of state (dw + non-dw == the five
state counts, asserted). Segmentation copies the Fleet board's classifier verbatim;
a test re-reads board.html and fails on drift. Offline board -> both read '-', never 0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeMJYTDYXnN6A6A1mW7Yud

Files touched

Diff

commit a5d20f8afcd00723511d13cbd57b318599159cff
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Sep 25 11:48:22 2026 -0700

    bar: add dw / non-dw ticket chips under the five state chips
    
    Same active population cut by business instead of state (dw + non-dw == the five
    state counts, asserted). Segmentation copies the Fleet board's classifier verbatim;
    a test re-reads board.html and fails on drift. Offline board -> both read '-', never 0.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01AeMJYTDYXnN6A6A1mW7Yud
---
 dw-segment.js              | 26 +++++++++++++++++++++
 public/index.html          | 22 ++++++++++++++----
 server.js                  | 12 ++++++----
 test/dw-segment.test.js    | 57 ++++++++++++++++++++++++++++++++++++++++++++++
 test/ticket-states.test.js | 35 +++++++++++++++++++---------
 ticket-states.js           |  6 +++++
 6 files changed, 138 insertions(+), 20 deletions(-)

diff --git a/dw-segment.js b/dw-segment.js
new file mode 100644
index 0000000..a226144
--- /dev/null
+++ b/dw-segment.js
@@ -0,0 +1,26 @@
+'use strict';
+// DW vs Non-DW segmentation for the bar's ticket-state row (Steve 2026-09-25: "add dw and non dw here").
+//
+// SOURCE OF TRUTH is the Fleet board's own classifier, ~/Projects/ticket-system/board.html
+// (DW_DENY/DW_RE/DW_KW/DW_VENDOR/DW_EXTRA + autoDW/isDW). The board is client-side HTML with no
+// requireable module, so the five constants below are copied VERBATIM. That copy is the drift
+// hazard, so test/dw-segment.test.js re-reads board.html and fails if the two ever diverge —
+// a silent divergence would make the bar's "dw 121" disagree with what the board shows when you
+// click it, which is exactly the kind of number-that-means-nothing this row exists to avoid.
+//
+// A hand-set `segment` on the ticket ('dw' | 'nondw') always wins over the auto guess, same as
+// the board — that is the whole point of the board's per-ticket segment dropdown.
+//
+// NOT the same thing as realm.js, and deliberately so: realm.js sorts live SESSIONS into dw/other
+// by free-text signals (ticket title, `doing` label, process cwd) to scope the bar's dot groups.
+// This sorts TICKETS, and its only job is to agree with the board it links to. Do not merge them.
+const DW_DENY=new Set(['wallpapersback','abramsego','pattern-vault']); // look wallpaper-ish but are SEPARATE businesses
+const DW_RE=/designerwallcoverings|designer-wallcoverings|dw_unified|(^|[^a-z])dw($|[^a-z])|(^|[^a-z])dw-/i;
+const DW_KW=/wallcover|wallpaper/i;                                    // DW wallcovering sister-sites (architectural/contract/hospitality/goldleaf)
+const DW_VENDOR=/(^|[^a-z])(sanderson|stroheim|muralsource|kravet|greenland|malibu|gracie|astek|fabricut|groundworks|rebel-walls|thibaut|schumacher|koroseal|scalamandre|maya-?romanoff|winfield|phillip-?jeffries|brewster|wallquest|carnegie|designtex|momentum|versa|elitis|fromental|fentucci|hollywood|novasuede|philipperomano|mdc)($|[^a-z])/i;
+const DW_EXTRA=new Set(['norma','greenland-onboard','sample-followup','sample-followup-sweep','interiordesignershowroom','marketing-command-center','all-designerwallcoverings','consulting-designerwallcoverings-com','mfr-review-viewer','mfr-review-viewer-corruption','designersalesreps','vendor-command-center','discontinued-agent']);
+
+function autoDW(t){const p=String((t&&t.project)||'').toLowerCase().trim();if(!p||DW_DENY.has(p))return false;return DW_EXTRA.has(p)||DW_RE.test(p)||DW_VENDOR.test(p)||DW_KW.test(p);}
+function isDW(t){if(!t)return false;if(t.segment==='dw')return true;if(t.segment==='nondw')return false;return autoDW(t);}
+
+module.exports = { isDW, autoDW, DW_DENY, DW_RE, DW_KW, DW_VENDOR, DW_EXTRA };
diff --git a/public/index.html b/public/index.html
index 2064f68..53b0993 100644
--- a/public/index.html
+++ b/public/index.html
@@ -70,6 +70,12 @@
      (blocked/open/idle/…) is pushed to the chip's right edge. margin-left:auto eats the free
      space between count and label on the full-width side-dock chip. */
   .tkchip .nm { margin-left:auto; padding-left:8px; }
+  /* Hairline before the dw chip: the five chips above it are a cut by STATE, the two below are the
+     same tickets cut by BUSINESS, so the counts read down the column but must not be summed across
+     the line. The rule follows the bar's axis — a left edge when the bar is a row, a top edge when
+     it is a column (Steve 2026-09-25 "add dw and non dw here"). */
+  body.orient-top .tkchip.seg-start { border-left:1px solid var(--line); margin-left:6px; padding-left:10px; }
+  body.orient-left .tkchip.seg-start, body.orient-right .tkchip.seg-start { border-top:1px solid var(--line); margin-top:6px; padding-top:8px; }
   /* the flexible gap that pushes #meta to the far end (right on top; bottom on side) */
   #spacer, #spacer2 { flex:1; -webkit-app-region:drag; }
   /* Individually-clickable ticket-state segments (Steve 2026-09-23): hover highlights the one under the cursor. */
@@ -249,8 +255,9 @@ function renderBar(){
   snbtn.textContent = ram.on ? '🔫 ON' : '🔫 OFF';
   snbtn.onclick = toggleSniper;
   bar.appendChild(snbtn);
-  // Ticket-board states as INDIVIDUAL vertical chips — ALWAYS all five (Steve 2026-09-25: "nav bar
-  // should be including all from orig nav bar... blocked, parked, doing, idle"). Zero = dimmed;
+  // Ticket-board states as INDIVIDUAL vertical chips — ALWAYS all of them (Steve 2026-09-25: "nav bar
+  // should be including all from orig nav bar... blocked, parked, doing, idle" + "add dw and non dw
+  // here"), the five states then the dw / non-dw cut of the same population. Zero = dimmed;
   // board unreachable / never fetched = "–" + dashed OFFLINE ring (never a fake 0, never hidden).
   // The server computes `states` (ticket-states.js); fall back to the same list client-side.
   {
@@ -262,13 +269,18 @@ function renderBar(){
       { key:'idle',    section:'stopped', label:'idle',    css:'#9ca3af' },
       { key:'doing',   section:'doing',   label:'doing',   css:'#60a5fa' },
       { key:'parked',  section:'parked',  label:'parked',  css:'#f9a8d4' },
+      { key:'dw',      section:'dw',      label:'dw',      css:'#34d399', divider:true },
+      { key:'nondw',   section:'nondw',   label:'non-dw',  css:'#c084fc' },
     ].map(t => ({ ...t, n: measured ? tickets[t.key] : null }));
     for (const t of TSTATES) {
       const off = t.n === null || t.n === undefined;
       const tc = document.createElement('div');
-      tc.className = 'chip tkchip' + (off ? ' offline' : (t.n ? '' : ' zero'));
-      tc.title = off ? `${t.label} — ticket board unreachable (not measured). Click to open the Fleet board`
-                     : `${t.n} ${t.label} — open the Fleet board`;
+      tc.className = 'chip tkchip' + (t.divider ? ' seg-start' : '') + (off ? ' offline' : (t.n ? '' : ' zero'));
+      const what = (t.key === 'dw' || t.key === 'nondw')
+        ? `active tickets, ${t.key === 'dw' ? 'Designer Wallcoverings' : 'NOT Designer Wallcoverings'}`
+        : t.label;
+      tc.title = off ? `${what} — ticket board unreachable (not measured). Click to open the Fleet board`
+                     : `${t.n} ${what} — open the Fleet board`;
       tc.innerHTML = `<span class="dot" style="background:${t.css}"></span>`
         + `<span class="cnt">${off ? '–' : t.n}</span>`
         + (showName ? `<span class="nm">${t.label}${off ? ' · offline' : ''}</span>` : '');
diff --git a/server.js b/server.js
index b0ac60e..0404305 100755
--- a/server.js
+++ b/server.js
@@ -11,7 +11,8 @@ const path = require('path');
 const jevDots = require('./jev-dots');  // re-classifies each dot COLOR via Jev ($0 builtin; paid flip gated)
 const liveFilter = require('./live-filter');  // TK-12236: hide dead parked rows (pure; unit-tested)
 const realm = require('./realm');     // DW / Non-DW folder per session (TK-12234)
-const { ticketStates } = require('./ticket-states');  // always-render ticket-state row (blocked/open/idle/doing/parked)
+const { ticketStates } = require('./ticket-states');  // always-render ticket-state row (blocked/open/idle/doing/parked/dw/non-dw)
+const { isDW } = require('./dw-segment');             // board-parity DW segmentation for the dw / non-dw chips
 
 const ALLCOLORDOTS = `${process.env.HOME}/.claude/skills/allcolordots/allcolordots.sh`;
 const ROUTER = `${process.env.HOME}/.claude/skills/dot-screen-router/router.sh`;
@@ -263,7 +264,7 @@ 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, ok: false, blocked: 0, open: 0, idle: 0, doing: 0, parked: 0, latest: null };
+let ticketsSnapshot = { updated: 0, ok: false, blocked: 0, open: 0, idle: 0, doing: 0, parked: 0, dw: 0, nondw: 0, latest: null };
 let refreshing = false;
 // Board ticket-status map for the parked filter; null until first good fetch. Older than the
 // max age = NOT-MEASURED (shown with an "unknown" marker), never trusted as current.
@@ -288,7 +289,9 @@ async function getTickets() {
   if (!boardData || !boardData.trim()) return null;
   let j;
   try { j = JSON.parse(boardData); } catch (e) { return null; }
-  const tickets = { blocked: 0, open: 0, idle: 0, doing: 0, parked: 0, latest: null };
+  // dw / nondw partition the SAME active population the five state chips count, so
+  // dw + nondw == blocked + open + idle + doing + parked exactly (asserted in test).
+  const tickets = { blocked: 0, open: 0, idle: 0, doing: 0, parked: 0, dw: 0, nondw: 0, latest: null };
   const statusMap = liveFilter.ticketStatusMap(j);
   if (statusMap) ticketStatus = { map: statusMap, at: Date.now() };
   if (Array.isArray(j)) {
@@ -304,6 +307,7 @@ async function getTickets() {
       else if (status === 'open') tickets.open++;
       else if (status === 'doing') tickets.doing++;
       else if (status === 'stopped') tickets.idle++;
+      if (isDW(row)) tickets.dw++; else tickets.nondw++;
       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 };
     }
@@ -365,7 +369,7 @@ const server = http.createServer(async (req, res) => {
       req.on('end', async () => {
         let section = 'all';
         try { section = JSON.parse(raw).section || 'all'; } catch {}
-        const OK = ['all', 'blocked', 'open', 'stopped', 'doing', 'parked', 'agents', 'skills', 'nondw', 'done'];
+        const OK = ['all', 'blocked', 'open', 'stopped', 'doing', 'parked', 'agents', 'skills', 'dw', 'nondw', 'done'];
         if (!OK.includes(section)) section = 'all';
         const url2 = `http://127.0.0.1:9794/?section=${section}&layout=grid`;
         const { err } = await run('open', [url2]);   // execFile — args are NOT shell-parsed, so no injection
diff --git a/test/dw-segment.test.js b/test/dw-segment.test.js
new file mode 100644
index 0000000..86a59ad
--- /dev/null
+++ b/test/dw-segment.test.js
@@ -0,0 +1,57 @@
+'use strict';
+// The bar's dw / non-dw counts must mean EXACTLY what the Fleet board means by those words —
+// click the "dw 121" chip and the board has to show 121 tickets. dw-segment.js copies the board's
+// five classifier constants verbatim because board.html is not requireable; this test re-reads
+// board.html and fails the moment either side is edited without the other. Without it the copy
+// rots silently and the chip becomes a number nobody can reconcile (a false-green of its own:
+// a wrong count looks exactly like a right one).
+const fs = require('node:fs');
+const os = require('node:os');
+const path = require('node:path');
+const test = require('node:test');
+const assert = require('node:assert');
+const seg = require('../dw-segment.js');
+
+const BOARD = path.join(os.homedir(), 'Projects', 'ticket-system', 'board.html');
+
+test('classifier constants stay byte-identical to the board (anti-drift)', (t) => {
+  if (!fs.existsSync(BOARD)) return t.skip(`board.html not present at ${BOARD} — NOT MEASURED`);
+  const src = fs.readFileSync(BOARD, 'utf8');
+  // Pull the board's own declaration for each constant and compare to ours, rendered the same way.
+  const ours = {
+    DW_DENY:   `new Set(${JSON.stringify([...seg.DW_DENY])})`,
+    DW_RE:     String(seg.DW_RE),
+    DW_KW:     String(seg.DW_KW),
+    DW_VENDOR: String(seg.DW_VENDOR),
+    DW_EXTRA:  `new Set(${JSON.stringify([...seg.DW_EXTRA])})`,
+  };
+  for (const name of Object.keys(ours)) {
+    const m = src.match(new RegExp(`^const ${name}=(.*?);`, 'm'));
+    assert.ok(m, `${name} not found in board.html — the board was restructured; re-sync dw-segment.js by hand`);
+    const theirs = m[1].replace(/'/g, '"').replace(/,\s*$/, '');
+    assert.strictEqual(theirs.replace(/\s+/g, ''), ours[name].replace(/\s+/g, ''),
+      `${name} DRIFTED between board.html and dw-segment.js — the bar's dw/non-dw counts no longer match the board`);
+  }
+});
+
+test('hand-set segment overrides the auto guess, both directions', () => {
+  assert.strictEqual(seg.isDW({ project: 'dw-infra' }), true);
+  assert.strictEqual(seg.isDW({ project: 'dw-infra', segment: 'nondw' }), false);
+  assert.strictEqual(seg.isDW({ project: 'butlr' }), false);
+  assert.strictEqual(seg.isDW({ project: 'butlr', segment: 'dw' }), true);
+});
+
+test('deny-list beats the wallpaper keyword; empty project is NOT DW', () => {
+  assert.strictEqual(seg.isDW({ project: 'wallpapersback' }), false);  // separate business
+  assert.strictEqual(seg.isDW({ project: 'abramsego' }), false);
+  assert.strictEqual(seg.isDW({ project: '' }), false);
+  assert.strictEqual(seg.isDW({}), false);
+  assert.strictEqual(seg.isDW(null), false);
+});
+
+// NEGATIVE TEST (CLAUDE.md TK-11431 amendment 3): prove the drift guard actually goes RED.
+test('drift guard reddens on an injected divergence', () => {
+  const mutated = { ...seg, DW_KW: /wallcover/i };  // one char short of the board's /wallcover|wallpaper/i
+  assert.notStrictEqual(String(mutated.DW_KW), String(seg.DW_KW),
+    'injected fault must differ from the real constant, else the guard proves nothing');
+});
diff --git a/test/ticket-states.test.js b/test/ticket-states.test.js
index a21b3a8..ba50015 100644
--- a/test/ticket-states.test.js
+++ b/test/ticket-states.test.js
@@ -1,25 +1,28 @@
 'use strict';
-// ticket-states: the bar must ALWAYS carry all five ticket states (Steve 2026-09-25), and an
-// unreachable board must read as NOT-MEASURED — never as hidden, never as a fake 0. Zero deps.
+// ticket-states: the bar must ALWAYS carry all five ticket states plus the dw / non-dw pair
+// (Steve 2026-09-25 x2), and an unreachable board must read as NOT-MEASURED — never as hidden,
+// never as a fake 0. Zero deps.
 const test = require('node:test');
 const assert = require('node:assert');
 const { ticketStates, TICKET_STATES } = require('../ticket-states');
 
-const KEYS = ['blocked', 'open', 'idle', 'doing', 'parked'];
+const KEYS = ['blocked', 'open', 'idle', 'doing', 'parked', 'dw', 'nondw'];
 
-test('healthy board: all five states present with measured counts, zeros kept', () => {
-  const s = ticketStates({ updated: Date.now(), ok: true, blocked: 78, open: 33, idle: 0, doing: 20, parked: 5 });
+test('healthy board: every state + the dw/non-dw pair, with measured counts, zeros kept', () => {
+  const s = ticketStates({ updated: Date.now(), ok: true, blocked: 78, open: 33, idle: 0, doing: 20, parked: 5, dw: 115, nondw: 21 });
   assert.deepEqual(s.map(x => x.key), KEYS);
-  assert.deepEqual(s.map(x => x.n), [78, 33, 0, 20, 5]);
+  assert.deepEqual(s.map(x => x.n), [78, 33, 0, 20, 5, 115, 21]);
+  assert.equal(s.find(x => x.key === 'dw').section, 'dw');
+  assert.equal(s.find(x => x.key === 'nondw').section, 'nondw', 'non-dw drills into the board Non-DW section');
   const idle = s.find(x => x.key === 'idle');
   assert.equal(idle.zero, true, 'a zero state is KEPT (dimmed), not dropped');
   assert.equal(s.find(x => x.key === 'idle').section, 'stopped', 'idle opens the board stopped section');
 });
 
 // NEGATIVE (fault-injection): the exact 2026-09-25 failure — ticket-board down after a reboot.
-test('NEGATIVE: never-fetched board -> five chips, every one NOT-measured (null, not 0)', () => {
-  const s = ticketStates({ updated: 0, ok: false, blocked: 0, open: 0, idle: 0, doing: 0, parked: 0 });
-  assert.equal(s.length, 5, 'row must not disappear when the board is down');
+test('NEGATIVE: never-fetched board -> every chip NOT-measured (null, not 0), dw/non-dw included', () => {
+  const s = ticketStates({ updated: 0, ok: false, blocked: 0, open: 0, idle: 0, doing: 0, parked: 0, dw: 0, nondw: 0 });
+  assert.equal(s.length, KEYS.length, 'row must not disappear when the board is down');
   for (const x of s) {
     assert.strictEqual(x.n, null, `${x.key} must be null (unmeasured), got ${x.n}`);
     assert.equal(x.measured, false);
@@ -27,11 +30,21 @@ test('NEGATIVE: never-fetched board -> five chips, every one NOT-measured (null,
 });
 
 test('NEGATIVE: board went down after a good fetch -> stale counts are NOT presented as measured', () => {
-  const s = ticketStates({ updated: Date.now() - 60000, ok: false, blocked: 78, open: 33, idle: 4, doing: 20, parked: 5 });
+  const s = ticketStates({ updated: Date.now() - 60000, ok: false, blocked: 78, open: 33, idle: 4, doing: 20, parked: 5, dw: 115, nondw: 21 });
   assert.ok(s.every(x => x.n === null), 'ok:false must blank the counts to "–"');
 });
 
-test('missing/garbage snapshot never throws and still yields all five', () => {
+// The dw/non-dw pair is a SECOND cut of the same population, so it must reconcile with the first.
+// If it ever does not, one of the two is counting a different set and the row is lying.
+test('dw + non-dw reconciles with the five state counts', () => {
+  const snap = { updated: Date.now(), ok: true, blocked: 78, open: 33, idle: 4, doing: 20, parked: 5, dw: 115, nondw: 25 };
+  const s = ticketStates(snap);
+  const byState = ['blocked', 'open', 'idle', 'doing', 'parked'].reduce((a, k) => a + snap[k], 0);
+  const bySeg = s.filter(x => x.key === 'dw' || x.key === 'nondw').reduce((a, x) => a + x.n, 0);
+  assert.equal(bySeg, byState, 'dw + non-dw must partition exactly the population the states count');
+});
+
+test('missing/garbage snapshot never throws and still yields every chip', () => {
   for (const snap of [undefined, null, {}, { updated: 1, ok: true, blocked: 'x' }]) {
     const s = ticketStates(snap);
     assert.equal(s.length, TICKET_STATES.length);
diff --git a/ticket-states.js b/ticket-states.js
index 92c4984..5e026f6 100644
--- a/ticket-states.js
+++ b/ticket-states.js
@@ -14,6 +14,12 @@ const TICKET_STATES = [
   { key: 'idle',    section: 'stopped', label: 'idle',    css: '#9ca3af' },
   { key: 'doing',   section: 'doing',   label: 'doing',   css: '#60a5fa' },
   { key: 'parked',  section: 'parked',  label: 'parked',  css: '#f9a8d4' },
+  // Steve 2026-09-25 "add dw and non dw here": the same active population, cut the OTHER way —
+  // by business, not by state. dw + non-dw always equals the five state counts above, so the two
+  // halves of the row cross-check each other at a glance. Segmentation is the board's own
+  // (dw-segment.js), and each chip drills into the matching board section.
+  { key: 'dw',      section: 'dw',      label: 'dw',      css: '#34d399', divider: true },
+  { key: 'nondw',   section: 'nondw',   label: 'non-dw',  css: '#c084fc' },
 ];
 
 // snap = the server's tickets snapshot ({updated, ok, blocked, open, ...}).

← 7010761 dotbar: start-bar comment now says Jev paid is ARMED (Steve  ·  back to Desktop Dotbar  ·  dotbar: hide dead sessions from ALL panels (colour groups + d2b3a95 →