Add a per-project "Hide seen" checkbox to the control board page (#1503)

Each project table now has "Hide seen" beside "Hide offline", both on by
default, with a note saying how many rows each one hides. The choice
survives the 10-second refresh. Static test pins the markup, the filter,
the persistence guard, and the change handler. Sonnet review: APPROVED.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
2026-09-12 08:55:22 -05:00
co-authored by Claude Fable 5.1
parent b70749219f
commit e90d15dd70
6 changed files with 55 additions and 7 deletions
+13
View File
@@ -1999,3 +1999,16 @@ 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.
## 2026-09-12 — Control board step 3, third refinement (#1503)
Before: seen rows still sat in the project tables with a small tag, which
was noise now that the Seen section lists them.
After: each project header has "Hide offline" and "Hide seen" checkboxes,
both on by default, and the note under the table reads
"N offline hidden · N seen hidden". The choice survives refreshes. Page-only
change, one static test (control-board 65/65, registry 69/69). Sonnet
review: APPROVED, one FYI (the default-on test would still pass without the
persistence guard), fixed by tightening the regex. Verified in Chrome against
Jason's real marks. Next: Jason keeps using it.
+1
View File
@@ -216,3 +216,4 @@ are never rewritten or removed; corrections are new entries.
- 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, 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.
- 2026-09-12 — jarvis (Claude Code, coordinator) — Control board step 3, third refinement (#1503): Jason suggested a per-project "Hide seen" checkbox beside "Hide offline"; added, on by default, with a combined hidden-count note. Page-only change plus one static test (65/65); sonnet review APPROVED (one test-rigor FYI fixed); live check in Chrome.
@@ -139,3 +139,8 @@ 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.
**2026-09-12 — "Hide seen" per project.** Jason suggested a checkbox like
"Hide offline" so seen rows stop cluttering the fleet table now that the
Seen section exists. Each project header has both boxes, on by default,
and the note under the table reads "N offline hidden · N seen hidden".
+3 -1
View File
@@ -96,7 +96,9 @@ the agent's newest message still has that exact `lastActivity` timestamp —
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.
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.
each with an "Unsee" button, so nothing marked is ever out of reach. Each
project table has "Hide offline" and "Hide seen" checkboxes (both on by
default) with a note saying how many rows each one hides.
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.
+21 -6
View File
@@ -69,6 +69,7 @@
.detail-list dd{margin:0;overflow-wrap:anywhere}
.project-group{margin-bottom:20px}
.project-group-head{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px}
.group-toggles{display:flex;flex-wrap:wrap;gap:14px}
.project-group-head label{font-size:.85rem;color:var(--muted);display:flex;align-items:center;gap:6px}
.offline-note{margin:6px 0 0;font-size:.82rem;color:var(--muted)}
.page-footer{margin-top:24px;padding-top:12px;border-top:1px solid var(--line);color:var(--muted);font-size:.85rem}
@@ -125,6 +126,7 @@
var timerId = null;
var secondsLeft = REFRESH_MS / 1000;
var hideOfflineState = {};
var hideSeenState = {};
// Open detail panels survive a refresh. Keyed by section, project and agent
// because the same agent can appear in both the waiting list and its group.
var openDetails = {};
@@ -279,16 +281,27 @@
projectsBody.innerHTML = projects.map(function (project) {
var group = byProject[project].slice().sort(function (a, b) { return a.agent.localeCompare(b.agent); });
if (!(project in hideOfflineState)) hideOfflineState[project] = true;
if (!(project in hideSeenState)) hideSeenState[project] = true;
var hideOffline = hideOfflineState[project];
var visible = group.filter(function (r) { return !(hideOffline && r.state === "offline"); });
var hiddenCount = group.length - visible.length;
var hideSeen = hideSeenState[project];
var offlineHidden = 0, seenHidden = 0;
var visible = group.filter(function (r) {
if (hideOffline && r.state === "offline") { offlineHidden += 1; return false; }
if (hideSeen && r.seen) { seenHidden += 1; return false; }
return true;
});
var rows = visible.map(function (r) { return buildRowPair(r, false); }).join("");
var note = hiddenCount > 0 ? '<p class="offline-note">' + hiddenCount + " offline hidden</p>" : "";
var noteParts = [];
if (offlineHidden > 0) noteParts.push(offlineHidden + " offline hidden");
if (seenHidden > 0) noteParts.push(seenHidden + " seen hidden");
var note = noteParts.length > 0 ? '<p class="offline-note">' + noteParts.join(" \u00b7 ") + "</p>" : "";
return (
'<div class="project-group">' +
'<div class="project-group-head"><h3>' + esc(project) + ' <span>(' + group.length + ")</span></h3>" +
'<div class="group-toggles">' +
'<label><input type="checkbox" class="hide-offline-toggle" data-project="' + esc(project) + '" ' + (hideOffline ? "checked" : "") + "> Hide offline</label>" +
"</div>" +
'<label><input type="checkbox" class="hide-seen-toggle" data-project="' + esc(project) + '" ' + (hideSeen ? "checked" : "") + "> Hide seen</label>" +
"</div></div>" +
'<div class="table-wrap"><table><thead><tr>' +
"<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 || '<tr><td colspan="4" class="empty">No agents.</td></tr>') + "</tbody></table></div>" +
@@ -297,6 +310,7 @@
}).join("");
}
function renderFooter(data) {
var counts = data.counts || {};
var parts = STATES.map(function (s) { return cap(s) + " " + (counts[s] || 0); }).join(" · ");
@@ -420,9 +434,10 @@
});
main.addEventListener("change", function (e) {
var cb = e.target.closest(".hide-offline-toggle");
var cb = e.target.closest(".hide-offline-toggle, .hide-seen-toggle");
if (!cb) return;
hideOfflineState[cb.dataset.project] = cb.checked;
var stateMap = cb.classList.contains("hide-seen-toggle") ? hideSeenState : hideOfflineState;
stateMap[cb.dataset.project] = cb.checked;
renderProjects(lastData);
});
@@ -546,3 +546,15 @@ test("page.html: has a collapsed Seen section that lists seen rows with the shar
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");
});
test("page.html: each project has a Hide seen checkbox (default on) beside Hide offline, with a hidden-count note", () => {
const html = readFileSync(join(pkgRoot, "src", "page.html"), "utf8");
const m = html.match(/function renderProjects\(data\) \{[\s\S]*?\n \}/);
assert.ok(m, "renderProjects() must exist in page.html");
const body = m[0];
assert.match(body, /if \(!\(project in hideSeenState\)\) hideSeenState\[project\] = true/, "Hide seen defaults to on and the choice survives re-render");
assert.match(body, /class="hide-seen-toggle" data-project="' \+ esc\(project\)/, "the checkbox carries the escaped project name");
assert.match(body, /hideSeen && r\.seen/, "seen rows are filtered when the box is ticked");
assert.match(body, /" seen hidden"/, "the note reports how many seen rows are hidden");
assert.match(html, /closest\("\.hide-offline-toggle, \.hide-seen-toggle"\)/, "one change handler serves both checkboxes");
});