dashboard: subscribe to /dashboard/stream for live updates (#406 step 3)

Pre-step-2, the SSE subscription on /index.html was conditionally
created by the broker-terminal IIFE that only ran on /flow.html
(via the `if (!flow || !window.HiveTerminal) return;` guard) — so
the dashboard only updated on cold load + after async-form submits.
The split in step 2 made the gap more obvious; close it.

Bare `EventSource` (the dashboard doesn't render broker rows, so
none of the HiveTerminal infrastructure is wanted here). Each
event's `kind` looks up against a `MUTATION_HANDLERS` table that
fans out to the existing `applyXxx` handlers. Unknown kinds (broker
`sent` / `delivered`, anything the backend adds later) silently
no-op. On (re)connect we kick `refreshState()` to recover events
lost during the disconnect window — same pattern as flow.js's
`onStreamOpen`. EventSource handles auto-reconnect.

#408 will give /index.html its own stream that omits broker
traffic; for now both pages subscribe to `/dashboard/stream` and
filter client-side.

Closes the #406 architectural roadmap:
- step 1 (#410): common.js extraction ✓
- step 2 (#412): split into app.js + flow.js entry points ✓
- step 3 (this PR): /index.html re-acquires its SSE subscription
This commit is contained in:
iris 2026-05-25 02:25:24 +02:00 committed by Mara
commit 1819bee43c

View file

@ -1828,6 +1828,63 @@ window.marked = marked;
NOTIF.bind();
Panel.bind();
// ─── live updates: dashboard event stream (#406 step 3) ────────────────
// The dashboard subscribes to /dashboard/stream for live mutation
// events so the SW4RM / Y3R C4LL / SYST3M panes update without an
// operator action triggering a refreshState. Pre-step-2 this wiring
// lived inside the broker-terminal IIFE which only fired on /flow.html
// — meaning /index.html only updated on cold load + after async-form
// submits.
//
// Bare `EventSource` (no terminal infrastructure needed — the
// dashboard doesn't render broker rows). Each event's `kind` is
// looked up against `MUTATION_HANDLERS`; unknown kinds (broker
// `sent` / `delivered`, anything new the backend adds) silently
// no-op. On (re)connect we kick a refreshState() to recover events
// lost during the disconnect window (same pattern as flow.js's
// onStreamOpen).
//
// `#408` will give /index.html its own stream that omits the
// broker traffic the dashboard never uses; for now both pages
// subscribe to `/dashboard/stream` and filter client-side.
const MUTATION_HANDLERS = {
approval_added: applyApprovalAdded,
approval_resolved: applyApprovalResolved,
question_added: applyQuestionAdded,
question_resolved: applyQuestionResolved,
transient_set: applyTransientSet,
transient_cleared: applyTransientCleared,
container_state_changed: applyContainerStateChanged,
container_removed: applyContainerRemoved,
tombstones_changed: applyTombstonesChanged,
meta_inputs_changed: applyMetaInputsChanged,
meta_update_running: applyMetaUpdateRunning,
rebuild_queue_changed: applyRebuildQueueChanged,
};
(function bindDashboardStream() {
const es = new EventSource('/dashboard/stream');
es.onmessage = (e) => {
let ev;
try { ev = JSON.parse(e.data); } catch { return; }
const h = MUTATION_HANDLERS[ev.kind];
if (!h) return; // broker rows + future kinds — dashboard doesn't care
try { h(ev); }
catch (err) { console.error('dashboard SSE handler', ev.kind, err); }
};
es.onopen = () => {
// Re-sync to recover events that fired during a disconnect
// window (issue #163). Initial connect also fires onopen — the
// first refreshState() above and this one race, but refreshState
// is idempotent so the second call just overwrites with the
// freshest snapshot. Cheap on a quiescent server, fine to repeat.
refreshState();
};
es.onerror = () => {
// EventSource auto-reconnects; nothing to do beyond logging.
console.debug('dashboard SSE error, will retry');
};
})();
// ─── tab routing (#369) ────────────────────────────────────────────────
// Hash-based: `#swarm` / `#call` / `#system` activate the matching
// pane on the dashboard. Empty hash defaults to SW4RM. FL0W is NOT