feat(webui): read-only Queue, Business and Settings views (#1543, row 54)

The Console gains three read-only views. Queue lists every row from
rows() in packages/queue (lead decision 83: the same lock-free read as
`queue list`, writing nothing), run in a child with a timeout and an
output cap; a row page shows the full row. Business shows names, an
allowlist of vars, arbiters, authority per action and credential
metadata (service, account, role, date, never a value, file or
variable). Settings shows RELEASE, the board origin and the notifier
binding. Every route is GET only, and every string in a body or
refusal passes a redactor: the config directory and dataRoot become
<config> and <dataRoot>, other absolute, ~/ and file:// paths <path>,
and 17 to 20 digit runs <id>. A queue-read that fails to start sends a
fixed message, never node's stderr.

Four fixes to HEAD behaviour, each with a test: the bus view's refresh
timer is cleared at render, a same-page refresh keeps focus on the H1,
#conv-pick is emptied when a conversation opens, and error() returns
focus to <main> only when something had focus. Filbert's T8 tests the
failed first read.

Dewey built it in two rounds. Round 1 (dba2429e) got changes from
Filbert (27185: After rendered [object Object], ~/ and :/ paths leaked)
and Darkwing (27186: Arbiters always none, queue-read import failure
leaked a file:// path and stack). Round 2 (afb2ae0e) was approved by
Filbert (27190) and Darkwing (27191, correction 27192). Sage's gate on
d64f434f plus the candidate: 13 package node suites 0 failed (queue
149, webui 39), every scripts/test-*.sh 0 failed (task 98/0 with
Docker, 26/0 without; release 14/0), build-tokens --check current.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-10 17:11:17 -05:00
co-authored by Claude Opus 5.5
parent 0a329be00b
commit cbd79cf666
15 changed files with 1126 additions and 38 deletions
+30 -4
View File
@@ -131,6 +131,21 @@ test('shell in a browser: System default, seeded states, 400px, focus and the Ct
await b.evaluate('document.querySelector("#mode").value="system";document.querySelector("#mode").dispatchEvent(new Event("change"))');
assert.equal(await b.evaluate('document.documentElement.hasAttribute("data-mode")'), false);
// Board, a failed first read: drawn empty and saying why, not left as the loading skeleton. A reload resets window.errors, so check it first.
assert.deepEqual(await b.evaluate('window.errors'), []);
brd.unavailable(); await b.navigate(base); await wait('!document.querySelector("#error").hidden');
assert.equal(await b.evaluate('document.querySelector("#error").className'), 'banner err');
assert.match(await text('#error'), /No board data loaded\./);
assert.equal(await count('#sessions [aria-busy]'), 0, 'no skeleton after a failed first read');
assert.equal(await text('#sessions'), '');
assert.equal(await text('#waiting'), '');
assert.deepEqual(await b.evaluate('["waiting-count","seen-count","session-count"].map(id=>document.getElementById(id).textContent)'), ['–', '–', '–']);
assert.equal(await text('#status'), 'Not read: the read failed');
assert.equal(await text('#footer'), 'Board not read: the read failed');
assert.equal(await text('#fresh-board'), 'Board not read: the read failed');
assert.equal(await text('#projects'), 'Not read: the read failed.');
brd.empty(); await refresh(); await wait('document.querySelector("#error").hidden');
// Board, empty: what would appear and where it comes from.
assert.match(await text('#sessions .empty'), /No sessions match these filters\.Sessions appear here when the board scan finds/);
assert.match(await text('#waiting .empty'), /Nothing is waiting on you\./);
@@ -170,8 +185,10 @@ test('shell in a browser: System default, seeded states, 400px, focus and the Ct
for (const id of ['#waiting-count', '#seen-count', '#session-count']) assert.equal(await text(id), '–', id);
await flat(); await shot('board-refused-400');
// A failed read after the refusal shows the board again, empty: no row, card or count from
// the refused scan, and the footer says what the banner says.
// the refused scan, and the footer says what the banner says. With nothing focused, focus stays put.
await b.evaluate('document.activeElement.blur()');
brd.unavailable(); await refresh();
assert.equal(await b.evaluate('document.activeElement === document.body'), true, 'a failure with nothing focused leaves focus alone');
assert.equal(await b.evaluate('document.querySelector("#error").className'), 'banner err');
assert.match(await text('#error'), /board unavailable.*No board data loaded\./);
assert.notEqual(await b.evaluate('getComputedStyle(document.querySelector("#board-view")).display'), 'none');
@@ -196,6 +213,13 @@ test('shell in a browser: System default, seeded states, 400px, focus and the Ct
await b.evaluate('document.querySelector("table.sessions [data-history]").click()');
await wait('document.querySelector("#conv-log").textContent.includes("Fixture history line")');
assert.equal(await b.evaluate('document.querySelector("#conv-form").hidden'), false);
// Fix to HEAD behaviour: a failed catalogue read leaves no session from the last one in the picker.
assert.equal(await count('#conv-pick option'), 1);
brd.fail(); await b.evaluate('document.querySelector("table.sessions [data-history]").click()');
await wait('document.querySelector("#conv-status").textContent.startsWith("History unavailable")');
assert.equal(await count('#conv-pick option'), 0, 'no stale session in the picker');
brd.registered(); await b.evaluate('document.querySelector("table.sessions [data-history]").click()');
await wait('document.querySelector("#conv-log").textContent.includes("Fixture history line")');
brd.refuse(); await refresh();
assert.equal(await b.evaluate('document.querySelector("#conversation").hidden'), true);
assert.equal(await b.evaluate('document.body.classList.contains("has-conversation")'), false);
@@ -203,6 +227,8 @@ test('shell in a browser: System default, seeded states, 400px, focus and the Ct
assert.equal(await b.evaluate('document.querySelector("#conv-form").checkVisibility()'), false);
assert.equal(await b.evaluate('getComputedStyle(document.querySelector("#board-view")).display'), 'none');
assert.equal(await b.evaluate('document.querySelector("#board-refused").hidden'), false);
// Fix to HEAD behaviour: focus went back to History, which the refusal removed; it lands on the page, not <body>.
assert.equal(await b.evaluate('document.activeElement.id'), 'main', 'focus after a refusal closes the conversation');
brd.one(); await refresh(); await wait('document.querySelectorAll("table.sessions tbody tr").length===2');
// Keyboard on the board: down moves between rows, Enter opens, Esc closes and returns focus.
@@ -234,7 +260,7 @@ test('shell in a browser: System default, seeded states, 400px, focus and the Ct
await flat(); await shot(`bus-empty-${h1.toLowerCase()}-400`);
}
assert.equal(await b.evaluate('getComputedStyle(document.querySelector("#fresh-board")).display'), 'none');
assert.equal(await b.evaluate('document.querySelectorAll("#sections svg use").length'), 4);
assert.equal(await b.evaluate('document.querySelectorAll("#sections svg use").length'), 7);
for (const sel of ['#fresh', '#session-count']) assert.equal(await b.evaluate(`getComputedStyle(document.querySelector(${JSON.stringify(sel)})).fontVariantNumeric`), 'tabular-nums', sel);
// Before any copy the toast is an empty live region still in the accessibility tree, out of sight.
assert.deepEqual(await b.evaluate('(()=>{const t=document.querySelector("#toast"),c=getComputedStyle(t),r=t.getBoundingClientRect();return [c.display!=="none",c.visibility,t.textContent,r.width<=1&&r.height<=1]})()'), [true, 'visible', '', true]);
@@ -429,7 +455,7 @@ test('five states on Board, Inbox, Tasks, Agents and Trail', { timeout: 180000 }
}
// After a refusal the palette lists only the views: no kept decision or task.
const views = ['#/', '#/inbox', '#/tasks', '#/agents'];
const views = ['#/', '#/inbox', '#/tasks', '#/agents', '#/queue', '#/business', '#/settings'];
const palette = '[...document.querySelectorAll("#cmdk-list [data-href]")].map(e=>e.dataset.href)';
await b.evaluate('location.hash="#/inbox"'); await wait('document.querySelectorAll("#sections .count").length===1');
assert.match(await text('#sections'), /Inbox 1 1 blocking/);
@@ -452,7 +478,7 @@ test('five states on Board, Inbox, Tasks, Agents and Trail', { timeout: 180000 }
s.rows.tasks = [{ ...s.rows.tasks[0], fields: { ...s.rows.tasks[0].fields, title: '<b id="pk-t">Bold</b> task' } }, s.rows.tasks[1]];
await b.key('k', 'KeyK', 2); await wait(`${palette}.length===${views.length + 3}`);
assert.equal(await count('#cmdk-list #pk-q, #cmdk-list #pk-t, #cmdk-list img, #cmdk-list b'), 0);
assert.deepEqual(await b.evaluate('[...document.querySelectorAll("#cmdk-list [data-href] span:first-child")].slice(4).map(e=>e.textContent)'), ['<img src=x onerror="errors.push(1)" id="pk-q">Push?', '#7 <b id="pk-t">Bold</b> task', '#8 Second task']);
assert.deepEqual(await b.evaluate(`[...document.querySelectorAll("#cmdk-list [data-href] span:first-child")].slice(${views.length}).map(e=>e.textContent)`), ['<img src=x onerror="errors.push(1)" id="pk-q">Push?', '#7 <b id="pk-t">Bold</b> task', '#8 Second task']);
await b.key('Escape'); await wait('!document.querySelector("#cmdk-dialog").open');
// The board page's Inbox read: a refusal there drops the count too.
s.fail(null); await refresh(); await wait('document.querySelectorAll("#sections .count").length===1');