Files
app/frontend/src/components/Health/LiveIndicator.tsx
T
stroblme 6c8a101d24 Rename Installation to Instance
Follows the portal: the noun is "instance" everywhere the app says it —
UI strings, CLI output, error details, docs and comments. The wire keys
(`instance_id`, `instance_token`) and the hub route this calls move with it.

An existing cloud.json is adopted rather than refused: without the key
alias the dataclass fails to parse, which the caller swallows and reads as
"never enrolled" instead of "reconnect".

`instance_key` on a node type becomes `target_key`. It means the outside
thing a node points at, which is a different sense of the word, and keeping
both would put two meanings of "instance" in one codebase.
2026-08-31 10:12:01 +02:00

74 lines
2.6 KiB
TypeScript

import { Activity } from "lucide-react"
import { AnimatePresence, motion } from "motion/react"
import { useEffect, useState, useSyncExternalStore } from "react"
import { useLiveConnection } from "@/components/Flow/liveStore"
import { connectionStore } from "@/lib/connectionStore"
import { fadeIn } from "@/lib/motion"
/**
* How long the socket may be down before it is worth saying so.
*
* The first reconnect attempt is a second away, so a dropped frame is back
* before this elapses and nothing appears. Long enough to cover the opening
* handshake too: a page that has just loaded is connecting, not disconnected.
*/
const GRACE = 3000
/**
* Says when live values have stopped arriving.
*
* Everything else on Home is polled over HTTP, so a dead socket leaves the page
* looking perfectly current while nothing on it moves any more — the neurons
* stop pulsing and the values stop changing, with nothing to say why. Nothing
* at all while the socket is up: a page that works needs no chip saying so.
*
* Quiet too while the offline notification is up, since an instance that cannot
* be reached has no socket either and one explanation of that is enough. Named
* in words rather than coloured, and deliberately not terracotta: the brain
* graph above it already owns that accent for a flow that cannot run.
*/
export function LiveIndicator() {
const connected = useLiveConnection()
const { offline } = useSyncExternalStore(
connectionStore.subscribe,
connectionStore.snapshot,
connectionStore.snapshot,
)
const [down, setDown] = useState(false)
useEffect(() => {
if (connected) {
setDown(false)
return
}
const timer = setTimeout(() => setDown(true), GRACE)
return () => clearTimeout(timer)
}, [connected])
return (
<AnimatePresence>
{down && !offline ? (
<motion.div
variants={fadeIn}
initial="hidden"
animate="visible"
exit="hidden"
role="status"
aria-live="polite"
data-testid="live-indicator"
// Wraps rather than widens: three phrases in a row do not fit a
// phone. See DESIGN-GUIDELINES.md → Responsive.
className="flex flex-wrap items-center gap-x-2 gap-y-1 rounded-lg border border-border px-4 py-2 text-sm shadow-e1"
>
<Activity className="size-4 shrink-0 text-muted-foreground" />
<span className="font-medium">Live updates disconnected</span>
<span className="text-muted-foreground">
values and activity are frozen — reconnecting…
</span>
</motion.div>
) : null}
</AnimatePresence>
)
}