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.
74 lines
2.6 KiB
TypeScript
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>
|
|
)
|
|
}
|