diff --git a/BUILD-LOG.md b/BUILD-LOG.md index 93bc8e8e..b1430424 100644 --- a/BUILD-LOG.md +++ b/BUILD-LOG.md @@ -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. diff --git a/docs/SESSIONS.md b/docs/SESSIONS.md index ee50ba86..fdf5b449 100644 --- a/docs/SESSIONS.md +++ b/docs/SESSIONS.md @@ -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. diff --git a/docs/plans/2026-09-12_control-board-mvp.md b/docs/plans/2026-09-12_control-board-mvp.md index 45a03cc0..d8799ae4 100644 --- a/docs/plans/2026-09-12_control-board-mvp.md +++ b/docs/plans/2026-09-12_control-board-mvp.md @@ -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". diff --git a/packages/control-board/README.md b/packages/control-board/README.md index 7141d562..06af35fb 100644 --- a/packages/control-board/README.md +++ b/packages/control-board/README.md @@ -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. diff --git a/packages/control-board/src/page.html b/packages/control-board/src/page.html index 867665ad..e76d7f30 100644 --- a/packages/control-board/src/page.html +++ b/packages/control-board/src/page.html @@ -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 ? '

' + hiddenCount + " offline hidden

" : ""; + var noteParts = []; + if (offlineHidden > 0) noteParts.push(offlineHidden + " offline hidden"); + if (seenHidden > 0) noteParts.push(seenHidden + " seen hidden"); + var note = noteParts.length > 0 ? '

' + noteParts.join(" \u00b7 ") + "

" : ""; return ( '
' + '

' + esc(project) + ' (' + group.length + ")

" + + '
' + '" + - "
" + + '" + + "
" + '
' + "" + "" + (rows || '') + "
AgentStateAgeLast message
No agents.
" + @@ -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); }); diff --git a/packages/control-board/tests/serve.test.mjs b/packages/control-board/tests/serve.test.mjs index ad70ab7f..e996e291 100644 --- a/packages/control-board/tests/serve.test.mjs +++ b/packages/control-board/tests/serve.test.mjs @@ -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"); +});