Add a collapsed "Seen" section to the control board page (#1503)

Jason asked for a way to recall rows he marked Seen. The page now lists
them under a collapsed "Seen (N)" section between "Waiting on you" and
"By project", each with Unsee. Open/closed state survives the 10-second
refresh because only the section body is re-rendered. Page-only change
plus one static test. Tests: control-board 64/64, registry 69/69.
Review APPROVED, no findings.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
2026-09-12 08:45:51 -05:00
co-authored by Claude Fable 5.1
parent 88d21defde
commit b70749219f
6 changed files with 67 additions and 0 deletions
+12
View File
@@ -1987,3 +1987,15 @@ quoted an incomplete tmux command) is fixed. Receipt:
Still open by design: "waiting" cannot tell a question from a completion Still open by design: "waiting" cannot tell a question from a completion
report; a wording hint or an agent-side "Waiting on:" convention are the report; a wording hint or an agent-side "Waiting on:" convention are the
next candidates if the Seen button is not enough. Next: Jason keeps using it. next candidates if the Seen button is not enough. Next: Jason keeps using it.
## 2026-09-12 — Control board step 3, second refinement (#1503)
Before: after marking two rows Seen, Jason could not find them again; they
were only visible as a small tag inside their project tables.
After: a collapsed "Seen (N)" section sits between "Waiting on you" and "By
project", listing every marked row with Unsee. Open/closed state survives
the refresh because only the section body is re-rendered. Page-only change;
one static test added (control-board 64/64, registry 69/69). Independent
review APPROVED with no findings. Verified in Chrome against the two marks
Jason had made (resume, filbert). Next: Jason keeps using it.
+1
View File
@@ -215,3 +215,4 @@ are never rewritten or removed; corrections are new entries.
- 2026-09-12 — dewey (Claude Code; takeover of pi session 01a08111) — WUI Phase 2 D01-D04 done, D05 delivered: five dashboard mockups (Rail, Deck, Console, Board, Stream) plus comparison at agents/dewey/work/wui/mockups/index.html; verifier 31 checks, 0 failed, 2 not verified (forced colours, screen reader); records in VERIFY.md, README.md, TASKS.md, HISTORY.md, DECISIONS.md. Stopped at Jason's design-selection gate. No v1 inspection, backend, credentials, commit or push. - 2026-09-12 — dewey (Claude Code; takeover of pi session 01a08111) — WUI Phase 2 D01-D04 done, D05 delivered: five dashboard mockups (Rail, Deck, Console, Board, Stream) plus comparison at agents/dewey/work/wui/mockups/index.html; verifier 31 checks, 0 failed, 2 not verified (forced colours, screen reader); records in VERIFY.md, README.md, TASKS.md, HISTORY.md, DECISIONS.md. Stopped at Jason's design-selection gate. No v1 inspection, backend, credentials, commit or push.
- 2026-09-12 — jarvis (Claude Code, coordinator) — Control board MVP step 2 (#1503, D-001): `packages/control-board` gains `serve` (loopback-only local server, `/api/board` re-runs the scanner) and a single-file page (waiting-on-you first, per-project groups, hide-offline, expandable rows, 10s auto-refresh with pause). Suites: control-board 33/33, registry 69/69. Static review (sonnet) found no defects; live browser check by coordinator. Receipt docs/plans/reviews/2026-09-12_control-board-step2-review.md. CURRENT next action: step 3, daily use by Jason. - 2026-09-12 — jarvis (Claude Code, coordinator) — Control board MVP step 2 (#1503, D-001): `packages/control-board` gains `serve` (loopback-only local server, `/api/board` re-runs the scanner) and a single-file page (waiting-on-you first, per-project groups, hide-offline, expandable rows, 10s auto-refresh with pause). Suites: control-board 33/33, registry 69/69. Static review (sonnet) found no defects; live browser check by coordinator. Receipt docs/plans/reviews/2026-09-12_control-board-step2-review.md. CURRENT next action: step 3, daily use by Jason.
- 2026-09-12 — jarvis (Claude Code, coordinator) — Control board step 3, first refinement (#1503): Jason reported killed pi sessions still "waiting" and most waiting rows being completion reports. Liveness now requires a tmux pane running pi; "Seen" marks (seen.json, POST /api/seen, Seen/Unsee on the page) drop read rows out of "Waiting on you" until the agent writes again. Tests 63/63 (30 new by sonnet helper), registry 69/69; sonnet review APPROVED (README command fixed). Receipt docs/plans/reviews/2026-09-12_control-board-step3-seen-marks.md. - 2026-09-12 — jarvis (Claude Code, coordinator) — Control board step 3, first refinement (#1503): Jason reported killed pi sessions still "waiting" and most waiting rows being completion reports. Liveness now requires a tmux pane running pi; "Seen" marks (seen.json, POST /api/seen, Seen/Unsee on the page) drop read rows out of "Waiting on you" until the agent writes again. Tests 63/63 (30 new by sonnet helper), registry 69/69; sonnet review APPROVED (README command fixed). Receipt docs/plans/reviews/2026-09-12_control-board-step3-seen-marks.md.
- 2026-09-12 — jarvis (Claude Code, coordinator) — Control board step 3, second refinement (#1503): Jason asked for a way to recall Seen rows; added a collapsed "Seen (N)" section with Unsee per row. Page-only change plus one static test (64/64); sonnet review APPROVED; live check in Chrome against Jason's two real marks.
@@ -133,3 +133,9 @@ node --test packages/control-board/tests/
- No changes to `packages/mosaic`. - No changes to `packages/mosaic`.
- This plan does not authorize push or merge beyond whatever the existing - This plan does not authorize push or merge beyond whatever the existing
refactor-branch plan already allows. refactor-branch plan already allows.
**2026-09-12 — a place to find what you marked Seen.** After using the
button, Jason noted the board had no way to show seen rows again. The page
now has a collapsed "Seen (N)" section between "Waiting on you" and "By
project" that lists every marked row with an Unsee button. It stays open or
closed across refreshes.
+2
View File
@@ -95,6 +95,8 @@ only when Jason clicks "Seen" or "Unsee" on the page (via `POST
the agent's newest message still has that exact `lastActivity` timestamp — the agent's newest message still has that exact `lastActivity` timestamp —
as soon as the agent writes anything new, `lastActivity` changes, the mark as soon as the agent writes anything new, `lastActivity` changes, the mark
no longer matches, and the row falls back into "Waiting on you" on its own. no longer matches, and the row falls back into "Waiting on you" on its own.
Marked rows are listed under a collapsed "Seen (N)" section on the page,
each with an "Unsee" button, so nothing marked is ever out of reach.
If `seen.json` exists but is not valid JSON (or not an object of string If `seen.json` exists but is not valid JSON (or not an object of string
values), the scan refuses rather than silently dropping every mark. values), the scan refuses rather than silently dropping every mark.
+35
View File
@@ -35,6 +35,11 @@
.error-banner{margin:12px 0;padding:10px 14px;border-radius:var(--radius);border:1px solid var(--danger); .error-banner{margin:12px 0;padding:10px 14px;border-radius:var(--radius);border:1px solid var(--danger);
background:color-mix(in srgb,var(--danger) 10%,var(--surface));color:var(--danger);font-size:.9rem} background:color-mix(in srgb,var(--danger) 10%,var(--surface));color:var(--danger);font-size:.9rem}
.empty{color:var(--muted);font-style:italic} .empty{color:var(--muted);font-style:italic}
#seenDetails summary{cursor:pointer;list-style:none;display:flex;align-items:center;gap:8px}
#seenDetails summary::-webkit-details-marker{display:none}
#seenDetails summary::before{content:"\25B8";color:var(--muted);font-size:.9em}
#seenDetails[open] summary::before{content:"\25BE"}
#seenDetails summary h2{display:inline;margin:0}
.table-wrap{overflow-x:auto;border:1px solid var(--line);border-radius:var(--radius-lg);background:var(--surface)} .table-wrap{overflow-x:auto;border:1px solid var(--line);border-radius:var(--radius-lg);background:var(--surface)}
table{width:100%;border-collapse:collapse;font-size:.88rem} table{width:100%;border-collapse:collapse;font-size:.88rem}
th,td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--line);vertical-align:top} th,td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--line);vertical-align:top}
@@ -84,6 +89,12 @@
<h2 id="waiting-h">Waiting on you</h2> <h2 id="waiting-h">Waiting on you</h2>
<div id="waitingBody"></div> <div id="waitingBody"></div>
</section> </section>
<section aria-labelledby="seen-h">
<details id="seenDetails">
<summary><h2 id="seen-h">Seen</h2></summary>
<div id="seenBody"></div>
</details>
</section>
<section aria-labelledby="projects-h"> <section aria-labelledby="projects-h">
<h2 id="projects-h">By project</h2> <h2 id="projects-h">By project</h2>
<div id="projectsBody"></div> <div id="projectsBody"></div>
@@ -102,6 +113,8 @@
var pauseBtn = document.getElementById("pauseBtn"); var pauseBtn = document.getElementById("pauseBtn");
var errorBanner = document.getElementById("errorBanner"); var errorBanner = document.getElementById("errorBanner");
var waitingBody = document.getElementById("waitingBody"); var waitingBody = document.getElementById("waitingBody");
var seenBody = document.getElementById("seenBody");
var seenHeading = document.getElementById("seen-h");
var projectsBody = document.getElementById("projectsBody"); var projectsBody = document.getElementById("projectsBody");
var footer = document.getElementById("footer"); var footer = document.getElementById("footer");
var main = document.getElementById("main"); var main = document.getElementById("main");
@@ -204,6 +217,27 @@
return main + detail; return main + detail;
} }
// Rows you marked Seen, in one place, so they can be found and put back.
// Collapsed by default; the open/closed choice survives a refresh.
function renderSeen(data) {
var list = (data.sessions || []).filter(function (r) { return r.seen; });
list.sort(function (a, b) {
var aa = a.ageSeconds === null || a.ageSeconds === undefined ? Infinity : a.ageSeconds;
var bb = b.ageSeconds === null || b.ageSeconds === undefined ? Infinity : b.ageSeconds;
return aa - bb;
});
seenHeading.textContent = "Seen (" + list.length + ")";
if (list.length === 0) {
seenBody.innerHTML = '<p class="empty">Nothing is marked seen. Rows you mark come back here, and return to "Waiting on you" on their own when the agent writes again.</p>';
return;
}
var rows = list.map(function (r) { return buildRowPair(r, true); }).join("");
seenBody.innerHTML =
'<div class="table-wrap"><table><thead><tr>' +
"<th scope=\"col\">Project</th><th scope=\"col\">Agent</th><th scope=\"col\">State</th><th scope=\"col\">Age</th><th scope=\"col\">Last message</th>" +
"</tr></thead><tbody>" + rows + "</tbody></table></div>";
}
function renderWaiting(data) { function renderWaiting(data) {
var list = (data.sessions || []).filter(function (r) { return r.waitingOnYou; }); var list = (data.sessions || []).filter(function (r) { return r.waitingOnYou; });
list.sort(function (a, b) { list.sort(function (a, b) {
@@ -273,6 +307,7 @@
function renderAll() { function renderAll() {
rowIdx = 0; rowIdx = 0;
renderWaiting(lastData); renderWaiting(lastData);
renderSeen(lastData);
renderProjects(lastData); renderProjects(lastData);
renderFooter(lastData); renderFooter(lastData);
} }
@@ -535,3 +535,14 @@ test("page.html: seenControl() escapes rec.project/agent/lastActivity, and the P
assert.match(body, /esc\(rec\.lastActivity\)/); assert.match(body, /esc\(rec\.lastActivity\)/);
assert.match(html, /headers:\s*\{\s*"content-type":\s*"application\/json"\s*\}/, "the seen POST must send content-type: application/json"); assert.match(html, /headers:\s*\{\s*"content-type":\s*"application\/json"\s*\}/, "the seen POST must send content-type: application/json");
}); });
test("page.html: has a collapsed Seen section that lists seen rows with the shared row builder", () => {
const html = readFileSync(join(pkgRoot, "src", "page.html"), "utf8");
assert.match(html, /<details id="seenDetails">(?![^>]*\sopen)/, "the Seen section must start collapsed");
assert.match(html, /<h2 id="seen-h">Seen<\/h2>/);
const m = html.match(/function renderSeen\(data\) \{[\s\S]*?\n \}/);
assert.ok(m, "renderSeen() must exist in page.html");
assert.match(m[0], /filter\(function \(r\) \{ return r\.seen; \}\)/, "the Seen section lists exactly the rows the API marks seen");
assert.match(m[0], /buildRowPair\(r, true\)/, "seen rows reuse the escaped row builder, project column included");
assert.match(html, /renderWaiting\(lastData\);\n\s*renderSeen\(lastData\);/, "renderAll must render the Seen section on every refresh");
});