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: