← 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
A dw-segment.jsM public/index.htmlM server.jsA test/dw-segment.test.jsM test/ticket-states.test.jsM ticket-states.js
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 →