From 59054b1f57425de261669fe5f805e6bb44e3998e Mon Sep 17 00:00:00 2001 From: iris Date: Tue, 9 Jun 2026 11:33:28 +0200 Subject: [PATCH] refactor(frontend): extract the agent roster into dashboard/state.js (#1451) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First step of splitting the 4.3k-line tabs.js monolith into per-tab modules. The live agent roster (containersState) is the one piece of genuinely cross-domain state — read by the container tree, the capabilities + tool-group matrices, the schedule target-chips, the container-load poll, the selection bar and the operator inbox. Move it (and its snapshot-sync) into a new dashboard/state.js that each domain module imports, so the upcoming per-tab extractions have a single source of truth to depend on instead of a shared IIFE closure. Minimal by design — only the roster moves; the container row cache and the apply-handlers (which also re-render) stay with the container domain. Behaviour-preserving: the imported Map is mutated in place exactly as before. esbuild inlines state.js into the existing tabs.js bundle, so the static output is unchanged. Build green. --- frontend/packages/dashboard/src/state.js | 25 ++++++++++++++++++++++++ frontend/packages/dashboard/src/tabs.js | 22 +++++++++------------ 2 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 frontend/packages/dashboard/src/state.js diff --git a/frontend/packages/dashboard/src/state.js b/frontend/packages/dashboard/src/state.js new file mode 100644 index 00000000..b23e256f --- /dev/null +++ b/frontend/packages/dashboard/src/state.js @@ -0,0 +1,25 @@ +// Shared dashboard state — the one piece of cross-domain state in the +// dashboard SPA: the live agent roster. +// +// The roster is read by nearly every tab (the container tree, the +// capabilities + tool-group matrices, the schedule target-chips, the +// container-load poll, the selection bar, and the operator inbox), so +// it lives here as a single source of truth that each domain module +// imports rather than threading through call signatures. Everything +// else in the dashboard is domain-local and lives with its own module. +// +// `containersState` is keyed by `ContainerView.name` so a lifecycle +// form's POST → 200 → matching SSE event can flip a single row without +// a full snapshot refetch. Mutated in place (`set` / `delete` / `clear`) +// by the container apply-handlers; consumers read it by reference, so +// the imported binding always reflects the latest roster. + +export const containersState = new Map(); + +// Replace the whole roster from a fresh `/api/state` snapshot. Called by +// the entry's `refreshState` on cold load and after async-form submits; +// live single-row updates go through the container apply-handlers. +export function syncContainersFromSnapshot(s) { + containersState.clear(); + for (const c of s.containers || []) containersState.set(c.name, c); +} diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index 2a5efde1..5fc58116 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -19,6 +19,7 @@ import { openStream, renderServerWarnings, } from './common.js'; import { createTabStrip } from '@hive/shared/tabs.js'; +import { containersState, syncContainersFromSnapshot } from './state.js'; // mdNode (in common.js) reads `window.marked` for the markdown side // panel preview path. Set it here on the dashboard entry so file @@ -149,23 +150,18 @@ window.marked = marked; } }); - // Derived container state — cold-loaded from /api/state.containers, - // then mutated live by `container_state_changed` (upsert by name) - // and `container_removed` (drop by name). The coordinator's rescan - // helper fires these after every mutation site + on a periodic poll - // in crash_watch. Keyed by ContainerView.name so the lifecycle - // forms' POST → 200 → matching event flips the row without a - // snapshot refetch. - const containersState = new Map(); + // The live agent roster (`containersState`) + its snapshot-sync now + // live in `./state.js` — it's the one piece of cross-domain state, read + // by nearly every tab. It's still mutated live by `container_state_changed` + // (upsert by name) and `container_removed` (drop by name) via the apply + // handlers just below; the imported binding reflects those in place. + // // Keyed container row cache. Maps agent name → { el:
  • , fingerprint }. // Allows renderContainers to skip rebuilding rows whose displayed state // hasn't changed — prevents full-wipe flicker + avoids redundant async - // dashboard-state fetches on every SSE event. + // dashboard-state fetches on every SSE event. Container-domain only, + // so it stays here (not in state.js). const containerRowCache = new Map(); - function syncContainersFromSnapshot(s) { - containersState.clear(); - for (const c of s.containers || []) containersState.set(c.name, c); - } function applyContainerStateChanged(ev) { if (!ev.container || !ev.container.name) return; containersState.set(ev.container.name, ev.container);