Agents in `active` state without a current taskId had no SSE to attach
to, but the card still rendered "Connecting..." — implying a network
state that never resolves. Branch on taskId first: idle agents show
"Idle — no task assigned"; running-without-task shows "Starting...".
Also fix a related SSE multiplexer race: subscribers joining a channel
that had already opened never got an onOpen callback (EventSource only
fires `open` once), so they sat at isConnected=false forever whenever
another component was already streaming the same URL. Fire onOpen on a
microtask for late-joiners.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After ~3 refreshes, the dashboard would hang on "Initializing dashboard..."
with all /api/* fetches stalling. Root cause: Chrome keeps HTTP/1.1 sockets
in its keep-alive pool across page navigations even after EventSource is
garbage-collected. Once 6 (the per-origin limit) are held, every new fetch
queues indefinitely and the app can't finish booting.
Fix, layered:
1. sse-bus.ts — pagehide/beforeunload listeners close all active channels
and send a sendBeacon to /api/events/disconnect so the server forces
the socket closed (socket.destroy) rather than waiting for the browser
to notice. Uses a sessionStorage clientId to correlate.
2. api.ts — createResilientEventSource (used by planning / mission / slice
stream endpoints) registers every handle in a module-level set and
closes them all on pagehide/beforeunload. sse-bus doesn't see these
streams, so it needs its own teardown.
3. sse.ts — server-side connection bookkeeping. Tracks managed SSE
connections by clientId, supports client-triggered disconnect via
POST /api/events/disconnect, stale-timer cleanup, and supersedes
older streams when a client reconnects.
4. server.ts — exposes /api/events/disconnect and /api/events/keepalive
under a dedicated 300 req/min rate limit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-release polish. Two related changes bundled because they both land the
project on public-release footing:
Dashboard auth
- fn dashboard now gates the HTTP API + terminal/badge WebSockets behind a
bearer token by default. Token resolution order: --token flag,
FUSION_DASHBOARD_TOKEN env, FUSION_DAEMON_TOKEN env (back-compat), or an
auto-generated fn_<32 hex>. --no-auth disables. The startup banner prints
a click-to-open URL with ?token=<token> embedded.
- Auth middleware now also accepts fn_token=<token> as a query-string
fallback so EventSource and WebSocket clients (which can't set custom
headers) still authenticate.
- setupTerminalWebSocket / setupBadgeWebSocket now refuse unauthenticated
upgrades with a proper 401 + socket close.
- Frontend: new auth.ts module captures ?token= off the URL into
localStorage (key fn.authToken), strips it from the visible URL via
replaceState, and installs a window.fetch wrapper that injects
Authorization: Bearer <token> on every same-origin /api/* request.
EventSource/WebSocket URL builders (api.ts, sse-bus.ts, useTerminal,
useBadgeWebSocket) route through appendTokenQuery().
MIT license
- LICENSE file at repo root.
- license: "MIT" on root package.json and every packages/*/package.json,
plus description/bugs metadata on the CLI package.
Docs
- docs/cli-reference.md documents --token / --no-auth / FUSION_DASHBOARD_TOKEN
and the click-to-open auth flow.
- docs/getting-started.md, docs/docker.md, README.md point at the new flow
and the CLI reference section.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add /api/projects/across-nodes to merge local projects with online remote node project lists
- Expose fetchProjectsAcrossNodes and ProjectInfoWithSource, and switch useProjects to consume cross-node data
- Update ProjectOverview and ProjectCard with node badges, node count stats, and a node filter dropdown plus responsive styles
- Add server and dashboard test coverage for cross-node aggregation, filtering, and hook behavior changes
- Include a changeset for @gsxdsm/fusion minor release and preserve stale-channel SSE reconnect guards during merge
- Add a closed flag to sse-bus channel state so teardown permanently disables reconnect scheduling
- Guard useTasks onReconnect callbacks against stale effect instances when toggling views
- Add regression tests for sse-bus and useTasks to ensure closed/unmounted channels do not reconnect
- Document the leak root cause and closed-flag pattern in .fusion/memory.md