tabs.js: scrub #262/#486/#163 cookies from agent links + topology move + SSE resync (#712 batch 6)

Three small clusters of cookies removed:

- #262 (×2): agent-declared dashboard links via /api/agent/<name>/links.
  Both comment blocks describe the same architectural decision
  (agent backend = source of truth, DOM-built so link strings can't
  reach the HTML parser). The cookies just attributed the decision
  to a PR; the prose stands without them.
- #486 (×2): M0V3 topology affordances + addBulkButton perAgentBodyFor
  hook. Substantive prose moved to docs/web-ui.md::Selection bar in
  #695 already; in-code comments now reference the docs.
- #163 (×3): snapshot re-sync + SSE catchup idempotence. The cookies
  flagged 'this is why the guard exists'; replaced with the present-
  tense 'post-disconnect SSE catchup can carry duplicate rows' which
  reads as the actual reason without needing the issue context.

tabs.js: 31 → 24 #NNN refs (-7). Net +1 line (the rewrites are
sometimes slightly longer when 'issue #N' is replaced with the
substantive description; net is still ahead). 67% reduction since
milestone start.

refs #712
This commit is contained in:
iris 2026-05-31 13:05:04 +02:00 committed by Mara
commit 8134b955e2

View file

@ -579,8 +579,8 @@ window.marked = marked;
// a same-origin proxy that forwards the agent backend's own link list // a same-origin proxy that forwards the agent backend's own link list
// (stats / screen-if-gui / forge profile / agent-configs / extras). // (stats / screen-if-gui / forge profile / agent-configs / extras).
// The agent backend is the single source of truth; no hardcoded link // The agent backend is the single source of truth; no hardcoded link
// list here (issue #262). DOM-built — link strings come from the // list here. DOM-built — link strings come from the agent's process
// agent's process and must never reach the HTML parser. // and must never reach the HTML parser.
const navStrip = el('span', { class: 'nav-strip' }); const navStrip = el('span', { class: 'nav-strip' });
head.append(navStrip); head.append(navStrip);
const forgeBase = `http://${hostname}:3000`; const forgeBase = `http://${hostname}:3000`;
@ -709,9 +709,8 @@ window.marked = marked;
// The hardcoded config-repo trigger and the agent-declared // The hardcoded config-repo trigger and the agent-declared
// extras block both moved into the unified nav strip in the // extras block both moved into the unified nav strip in the
// head row above (sourced from the agent backend via // head row above (sourced from the agent backend via
// `/api/agent/{name}/links` — issue #262). Only the journald // `/api/agent/{name}/links`). Only the journald trigger stays
// trigger stays here since it opens the side panel rather // here since it opens the side panel rather than a link.
// than a link.
body.append(drill); body.append(drill);
li.append(icon, body); li.append(icon, body);
@ -806,7 +805,8 @@ window.marked = marked;
disabledTitle: why('PURG3', managerNames.map((n) => `\`${n}\` is the manager`)), disabledTitle: why('PURG3', managerNames.map((n) => `\`${n}\` is the manager`)),
}); });
// #486 — move agent(s) in the topology tree. Two affordances: // Move agent(s) in the topology tree — see
// docs/web-ui.md::Selection bar for the two affordances:
// //
// ⇡ M0V3 → ROOT promote selected agent(s) to top-level (parent=null) // ⇡ M0V3 → ROOT promote selected agent(s) to top-level (parent=null)
// ⇢ M0V3 → [sel] reparent the single selected agent under a picked // ⇢ M0V3 → [sel] reparent the single selected agent under a picked
@ -973,9 +973,8 @@ window.marked = marked;
// `opts.body` is a static object applied to every POST. // `opts.body` is a static object applied to every POST.
// - `opts.perAgentBodyFor(name)` is set: `opts.action` is the // - `opts.perAgentBodyFor(name)` is set: `opts.action` is the
// full URL (no name appended) and the per-agent body comes // full URL (no name appended) and the per-agent body comes
// from the callback. Used by /api/topology/set-parent (#486), // from the callback. Used by /api/topology/set-parent, where
// where the agent name is a body field rather than a URL // the agent name is a body field rather than a URL component.
// component.
for (const name of names) { for (const name of names) {
const body = opts.perAgentBodyFor const body = opts.perAgentBodyFor
? new URLSearchParams(opts.perAgentBodyFor(name)) ? new URLSearchParams(opts.perAgentBodyFor(name))
@ -1161,9 +1160,10 @@ window.marked = marked;
const idx = questionsState.pending.findIndex((q) => q.id === ev.id); const idx = questionsState.pending.findIndex((q) => q.id === ev.id);
const existing = idx >= 0 ? questionsState.pending[idx] : null; const existing = idx >= 0 ? questionsState.pending[idx] : null;
if (idx >= 0) questionsState.pending.splice(idx, 1); if (idx >= 0) questionsState.pending.splice(idx, 1);
// Idempotent: a snapshot re-sync (issue #163) can carry this same // Idempotent: a snapshot re-sync (post-disconnect SSE catchup) can
// answered row in `question_history` while a live event also // carry this same answered row in `question_history` while a live
// delivers it — guard the unshift so history can't double a row. // event also delivers it — guard the unshift so history can't
// double a row.
if (!questionsState.history.some((h) => h.id === ev.id)) { if (!questionsState.history.some((h) => h.id === ev.id)) {
questionsState.history.unshift({ questionsState.history.unshift({
id: ev.id, id: ev.id,
@ -1461,9 +1461,10 @@ window.marked = marked;
function applyApprovalResolved(ev) { function applyApprovalResolved(ev) {
// Drop from pending; prepend to history (newest-first), cap at 30. // Drop from pending; prepend to history (newest-first), cap at 30.
approvalsState.pending = approvalsState.pending.filter((a) => a.id !== ev.id); approvalsState.pending = approvalsState.pending.filter((a) => a.id !== ev.id);
// Idempotent: a snapshot re-sync (issue #163) can carry this same // Idempotent: a snapshot re-sync (post-disconnect SSE catchup) can
// resolved row in `approval_history` while a live event also // carry this same resolved row in `approval_history` while a live
// delivers it — guard the unshift so history can't double a row. // event also delivers it — guard the unshift so history can't
// double a row.
if (!approvalsState.history.some((h) => h.id === ev.id)) { if (!approvalsState.history.some((h) => h.id === ev.id)) {
approvalsState.history.unshift({ approvalsState.history.unshift({
id: ev.id, id: ev.id,
@ -3216,10 +3217,10 @@ window.marked = marked;
catch (err) { console.error('dashboard SSE handler', ev.kind, err); } catch (err) { console.error('dashboard SSE handler', ev.kind, err); }
}; };
es.onopen = () => { es.onopen = () => {
// Re-sync to recover events that fired during a disconnect // Re-sync to recover events that fired during the SSE disconnect
// window (issue #163). Initial connect also fires onopen — the // window. Initial connect also fires onopen — the first
// first refreshState() above and this one race, but refreshState // refreshState() above and this one race, but refreshState is
// is idempotent so the second call just overwrites with the // idempotent so the second call just overwrites with the
// freshest snapshot. Cheap on a quiescent server, fine to repeat. // freshest snapshot. Cheap on a quiescent server, fine to repeat.
refreshState(); refreshState();
}; };