Own the live socket outside React so leaving Home cannot orphan it

The socket belonged to whichever hook instance ran its effect first. Passive
effects run children before parents, so on Home that was the brain graph rather
than the shell: navigating to a sibling route unmounted the graph, which closed
the socket, while the shell kept the reference count above zero. From there the
page was deaf for the rest of its life, with nothing left to reconnect it.

A module-level connection with a real refcount replaces it — connect on the
first subscriber, disconnect on the last — and the hook is a thin subscription
with the same signature, correct under StrictMode's mount/unmount/mount.

A 1008 now reconnects instead of returning silently: the token is read afresh
per attempt, and three consecutive rejections fall through to the caller's auth
handler so a revoked session surfaces rather than spins.

The snapshot's emit counts are read into a store of their own, apart from the
live count, so a graph that connects into a busy engine is drawn as busy without
every neuron claiming it just fired. The neuron and edge pulses now key off a
change seen while they were mounted, so returning to Home no longer replays
every emission of the session.

Home gets a live indicator for the case none of this can fix: quiet while the
socket is up, and named in words when it is down, since HTTP polling keeps the
rest of the page looking current.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HTsT1isxUjw5gtkJk8WhuA
This commit is contained in:
2026-08-20 11:58:49 +02:00
co-authored by Claude Opus 5
parent 558e1cdf18
commit bbff78766e
6 changed files with 439 additions and 166 deletions
+5
View File
@@ -9,6 +9,7 @@ import { BrainView } from "@/components/Flow/BrainView"
import { flowKeys, flowsQueryOptions } from "@/components/Flow/queries"
import { HealthActivity } from "@/components/Health/HealthActivity"
import { HealthOverview } from "@/components/Health/HealthOverview"
import { LiveIndicator } from "@/components/Health/LiveIndicator"
import { Badge } from "@/components/ui/badge"
import { Card } from "@/components/ui/card"
import { Skeleton } from "@/components/ui/skeleton"
@@ -116,6 +117,10 @@ function Dashboard() {
// Every section here is free to shrink instead. See DESIGN-GUIDELINES.md
// → Responsive.
<div className="grid gap-6 [&>*]:min-w-0">
{/* Nothing at all while the socket is up. Everything below is polled, so
a dead socket would otherwise look like an engine that went quiet. */}
<LiveIndicator />
{/* Nothing wired up yet means nothing to draw, and the band would still
hold a screenful of empty space above the flows card. Node count
rather than flow count: a flow made a minute ago has none. */}