Context
index.html re-fetches /api/agents every 5s and re-renders the whole tree from innerHTML; history.html polls every 30s. Considerable machinery exists purely to survive the re-render churn: module-level confirming/openLogs/collapsedRepos stores, the optimistic pending map with TTLs, and reconcilePending().
Problem
- Up to 5s of extra staleness stacked on top of the ~20s heartbeat interval; with the low-latency command work (tunnel poke issue) the heartbeat side gets fast, making UI polling the remaining lag.
- Full-tree
innerHTML re-render every 5s: focus/selection inside anything interactive is lost, open <details> must be tracked manually, and phones burn battery re-fetching an unchanged payload.
- Polling cost is per-open-tab; leaving the dashboard open on a wall tablet or a phone multiplies it.
Proposal
- Server-Sent Events (
GET /api/events): the hub pushes a data: frame whenever an agent's heartbeat lands, a command is queued/acked, or the offline sweep flips a host — all points already centralized in server.js. SSE beats a WebSocket here: it's plain HTTP (rides the existing basic-auth + Cloudflare tunnel with zero new handshake code — notable since the WS server framing in this codebase is hand-rolled), auto-reconnects natively via EventSource, and the hub never needs data from the browser.
- Frames can carry the same shape as
/api/agents (simplest: full snapshot, letting the client stay dumb) with the initial page load still fetching once; a retry: hint plus falling back to today's polling when EventSource errors keeps it robust.
- Client: replace
setInterval(refresh, 5000) with an EventSource handler feeding the existing render(cache). Optionally follow up with keyed/partial DOM updates to stop the full-tree rebuild — worth doing once updates become event-driven and bursty.
- Heartbeat cadence note: a Cloudflare-proxied SSE stream needs a keepalive comment every ~30s to survive idle timeouts (the hub already pings its tunnel WSs for the same reason).
Touchpoints
agent-hub/server.js — event fan-out, /api/events
agent-hub/public/index.html, history.html — EventSource client, polling fallback
Context
index.htmlre-fetches/api/agentsevery 5s and re-renders the whole tree frominnerHTML;history.htmlpolls every 30s. Considerable machinery exists purely to survive the re-render churn: module-levelconfirming/openLogs/collapsedReposstores, the optimisticpendingmap with TTLs, andreconcilePending().Problem
innerHTMLre-render every 5s: focus/selection inside anything interactive is lost, open<details>must be tracked manually, and phones burn battery re-fetching an unchanged payload.Proposal
GET /api/events): the hub pushes adata:frame whenever an agent's heartbeat lands, a command is queued/acked, or the offline sweep flips a host — all points already centralized inserver.js. SSE beats a WebSocket here: it's plain HTTP (rides the existing basic-auth + Cloudflare tunnel with zero new handshake code — notable since the WS server framing in this codebase is hand-rolled), auto-reconnects natively viaEventSource, and the hub never needs data from the browser./api/agents(simplest: full snapshot, letting the client stay dumb) with the initial page load still fetching once; aretry:hint plus falling back to today's polling whenEventSourceerrors keeps it robust.setInterval(refresh, 5000)with anEventSourcehandler feeding the existingrender(cache). Optionally follow up with keyed/partial DOM updates to stop the full-tree rebuild — worth doing once updates become event-driven and bursty.Touchpoints
agent-hub/server.js— event fan-out,/api/eventsagent-hub/public/index.html,history.html— EventSource client, polling fallback