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
+18
View File
@@ -334,6 +334,24 @@ test("a reader paused between the witness and the file while a writer finishes:
assert.deepEqual(r.err, ["rev 1 visible, not confirmed durable"]);
});
test("rows: every row as show prints it, in list order; writes nothing; same notes and refusals as list", async (t) => {
const { repo, m } = await ready(t);
const files = () => [read(repo.queuePath), read(repo.viewPath), read(join(repo.gitDir, "mosaic-queue.head"))];
const before = files();
const r = m.store.rows(o(repo));
assert.equal(r.code, 0);
assert.deepEqual(r.err, []);
assert.deepEqual(r.rows.map((row) => `${row.id}\t${row.state}\t${row.owner}\t${row.piece}`), m.store.list(o(repo)).out);
for (const row of r.rows) assert.deepEqual(row, JSON.parse(m.store.show(o(repo), row.id).out[0]));
assert.deepEqual(files(), before);
const hook = (n) => { if (n === "reader-between") m.store.mutate(o(repo), note(9, "one", "note-9-000001")); };
const tail = m.store.rows(o(repo, { hook }));
assert.deepEqual(tail.err, ["rev 1 visible, not confirmed durable"]);
assert.equal(tail.rows.find((row) => row.id === 9).note, "one");
writeFileSync(repo.queuePath, read(repo.queuePath).replace('"note": "one"', '"note": "two"'));
throwsCode(() => m.store.rows(o(repo)), 2, /do not equal the replay/);
});
test("file-then-witness order forced by a hook: the locked recheck prevents a false report", async (t) => {
const { repo, m } = await ready(t);
const hook = (n) => { if (n === "reader-between") m.store.mutate(o(repo), note(9, "one", "note-9-000001")); };