From eabe4fc650a7fe14b50be96ceac96d63d4f3cdad Mon Sep 17 00:00:00 2001 From: mayank Date: Wed, 12 Aug 2026 17:56:12 +0000 Subject: [PATCH 1/4] fix: align home stats with selected machine --- README.md | 5 +- app.tsx | 213 +++++++++++++++++++++++++++++++++++++++++++++++------- server.ts | 19 +++++ 3 files changed, 210 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 48304f7..573fdcb 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,10 @@ bb plugin install git:https://github.com/MGrin/bb-plugin-system.git@main the last hour, and top-process tables by CPU and by memory. A machine picker switches the whole dashboard between the primary bb machine and any connected enrolled machine. -**Homepage tiles** — a compact CPU / memory / disk row. +**Homepage tiles** — a compact CPU / memory / disk row for the machine selected in +the Home composer. They are off by default; toggle **Show system stats on Home** in +the top-right of the System panel to show or hide them. The saved choice applies +instantly and is remembered. **`bb system`** — the same data for agents, as text: diff --git a/app.tsx b/app.tsx index 48d122a..106e826 100644 --- a/app.tsx +++ b/app.tsx @@ -1,5 +1,5 @@ // bb-plugin-system frontend — System panel + homepage tiles. -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; import { definePluginApp, useRealtime, @@ -37,6 +37,44 @@ type Machine = { }; const PRESSURE: Record = { 1: "normal", 2: "warning", 4: "critical" }; +const SELECTED_MACHINE_KEY = "bb-plugin-system:selected-machine"; + +function selectedComposerMachine(machines: Machine[], primaryHostId: string | null) { + const composer = document.querySelector('[data-app-composer-role="primary"]'); + const machineLabel = composer + ?.querySelector('button[aria-label="Machine"]') + ?.textContent?.trim(); + if (machineLabel) { + const selected = machines.find((machine) => machineLabel === machine.name); + if (selected) return selected; + } + + const environmentLabel = composer + ?.querySelector('button[aria-label="Environment"] [data-promptbox-full-label]') + ?.textContent?.trim(); + if (environmentLabel) { + const selected = machines.find((machine) => environmentLabel.startsWith(`${machine.name} ·`)); + if (selected) return selected; + } + + // The environment chip omits the machine name for the primary host. + return machines.find((machine) => machine.id === primaryHostId) ?? null; +} + +function useComposerMachine(machines: Machine[], primaryHostId: string | null) { + const [machine, setMachine] = useState(null); + useLayoutEffect(() => { + const sync = () => { + const next = selectedComposerMachine(machines, primaryHostId); + setMachine((current) => current?.id === next?.id ? current : next); + }; + sync(); + const observer = new MutationObserver(sync); + observer.observe(document.body, { childList: true, subtree: true, characterData: true }); + return () => observer.disconnect(); + }, [machines, primaryHostId]); + return machine; +} function Meter({ frac, tone }: { frac: number; tone: "ok" | "warn" | "hot" }) { const cls = tone === "hot" ? "bg-destructive" : tone === "warn" ? "bg-primary/70" : "bg-primary"; @@ -171,6 +209,43 @@ function MachineSelect(props: { ); } +function useSelectedMachine(machines: Machine[], primaryHostId: string | null) { + const [selectedHostId, setSelectedHostId] = useState(() => { + try { + return window.localStorage.getItem(SELECTED_MACHINE_KEY); + } catch { + return null; + } + }); + useEffect(() => { + if (!machines.length) return; + setSelectedHostId((current) => + current && machines.some((machine) => machine.id === current) + ? current + : primaryHostId ?? machines[0]!.id, + ); + }, [machines, primaryHostId]); + useEffect(() => { + if (!selectedHostId) return; + try { + window.localStorage.setItem(SELECTED_MACHINE_KEY, selectedHostId); + } catch { + // Client storage may be unavailable in hardened browser contexts. The + // in-memory selection still works for the current mount. + } + }, [selectedHostId]); + useEffect(() => { + const syncSelection = (event: StorageEvent) => { + if (event.key === SELECTED_MACHINE_KEY && event.newValue) { + setSelectedHostId(event.newValue); + } + }; + window.addEventListener("storage", syncSelection); + return () => window.removeEventListener("storage", syncSelection); + }, []); + return [selectedHostId, setSelectedHostId] as const; +} + function useSystem(hostId: string | null, minutes: number, announceWatching: boolean) { const rpc = useRpc(); const [cur, setCur] = useState(null); @@ -223,24 +298,73 @@ function useSystem(hostId: string | null, minutes: number, announceWatching: boo return { cur, hist, error }; } +// The homepage-tiles visibility toggle lives in the System panel, so both +// slots (panel and homepage section) share this hook and stay in sync via +// realtime — no host settings page, no save button, instant on click. +function useHomeVisibility() { + const rpc = useRpc(); + const [showOnHomepage, setShowOnHomepage] = useState(null); + const load = useCallback(async () => { + try { + const result = await rpc.call("homeVisibility", null); + setShowOnHomepage(result.showOnHomepage); + } catch { + setShowOnHomepage(false); + } + }, [rpc]); + useEffect(() => { + void load(); + }, [load]); + useRealtime("system.home-visibility", (event) => { + const payload = event as { showOnHomepage?: boolean } | null; + if (typeof payload?.showOnHomepage === "boolean") { + setShowOnHomepage(payload.showOnHomepage); + } + }); + const setVisible = useCallback( + async (next: boolean) => { + setShowOnHomepage(next); + try { + await rpc.call("setHomeVisibility", { showOnHomepage: next }); + } catch { + void load(); + } + }, + [load, rpc], + ); + return { showOnHomepage, setShowOnHomepage: setVisible }; +} + +function ShowOnHomeToggle() { + const { showOnHomepage, setShowOnHomepage } = useHomeVisibility(); + const on = showOnHomepage === true; + return ( + + ); +} + function SystemPanel() { const { machines, primaryHostId, error: machineError } = useMachines(); - const [selectedHostId, setSelectedHostId] = useState(null); - useEffect(() => { - if (!machines.length) return; - setSelectedHostId((current) => - current && machines.some((machine) => machine.id === current) - ? current - : primaryHostId ?? machines[0]!.id, - ); - }, [machines, primaryHostId]); + const [selectedHostId, setSelectedHostId] = useSelectedMachine(machines, primaryHostId); const { cur, hist, error } = useSystem(selectedHostId, 60, true); const s = cur?.sample; const samples = hist?.samples ?? []; return (
-
+
+ {selectedHostId && machines.length > 0 ? ( ) : null} @@ -304,27 +428,64 @@ function SystemDetails({ current, samples }: { current: Current; samples: Sample ); } -function HomeTiles() { - const { primaryHostId } = useMachines(); - const { cur } = useSystem(primaryHostId, 5, false); +function VisibleHomeTiles({ hostId }: { hostId: string | null }) { + const { cur } = useSystem(hostId, 5, false); const s = cur?.sample; - if (!s) return null; return ( -
- - = 2} - /> - +
+ {!s ? ( +
+ Sampling… first data arrives shortly. +
+ ) : ( +
+ + = 2} + /> + +
+ )}
); } +function HomeTiles() { + const { showOnHomepage } = useHomeVisibility(); + const { machines, primaryHostId } = useMachines(); + const machine = useComposerMachine(machines, primaryHostId); + const rootRef = useRef(null); + // Unknown stays hidden until the persisted preference loads. Treating it as + // visible caused the disabled section to flash its loading state on Home. + const isVisible = showOnHomepage === true; + + // homepageSection has no conditional-registration API. Hide the host-owned + // heading together with our content so disabling the setting removes the + // complete section instead of leaving an orphaned "System" label behind. + useLayoutEffect(() => { + const section = rootRef.current?.closest("section"); + if (!section) return; + const heading = section.querySelector(":scope > h2"); + section.hidden = !isVisible; + section.classList.add("pt-4"); + if (heading) { + heading.textContent = machine ? `System Stats (${machine.name})` : "System Stats"; + } + return () => { + section.hidden = false; + section.classList.remove("pt-4"); + if (heading) heading.textContent = "System Stats"; + }; + }, [isVisible, machine]); + + return
{isVisible ? : null}
; +} + export default definePluginApp((app) => { app.slots.navPanel({ id: "system", title: "System", icon: "Activity", path: "system", component: SystemPanel }); - app.slots.homepageSection({ id: "system-tiles", title: "System", component: HomeTiles }); + app.slots.homepageSection({ id: "system-tiles", title: "System Stats", component: HomeTiles }); }); diff --git a/server.ts b/server.ts index ad989dd..9671fe3 100644 --- a/server.ts +++ b/server.ts @@ -87,6 +87,16 @@ export const rpcContract = defineRpcContract({ }, // The panel calls this while mounted so the sampler knows someone is watching. watching: { input: machineInput, output: z.object({ ok: z.boolean() }) }, + // Homepage-tiles visibility. Stored by the plugin itself (not the host's + // settings page) so the panel's toggle takes effect the moment it is clicked. + homeVisibility: { + input: z.null(), + output: z.object({ showOnHomepage: z.boolean() }), + }, + setHomeVisibility: { + input: z.object({ showOnHomepage: z.boolean() }).strict(), + output: z.object({ ok: z.boolean() }), + }, }); async function pressureLevel(): Promise { @@ -678,6 +688,15 @@ export default async function plugin(bb: BbPluginApi) { watchingUntil.set(hostId, Date.now() + 90_000); return { ok: true }; }, + async homeVisibility() { + const raw = await bb.storage.kv.get("showOnHomepage"); + return { showOnHomepage: raw === "1" }; + }, + async setHomeVisibility(input) { + await bb.storage.kv.set("showOnHomepage", input.showOnHomepage ? "1" : "0"); + bb.realtime.publish("system.home-visibility", { showOnHomepage: input.showOnHomepage }); + return { ok: true }; + }, }); const pct = (n: number) => `${Math.round(n * 100)}%`; From d7820f165a3455b7ef10f6bb1a073907d4495467 Mon Sep 17 00:00:00 2001 From: mayank Date: Wed, 12 Aug 2026 18:25:18 +0000 Subject: [PATCH 2/4] refactor: isolate home stats DOM integration --- app.tsx | 83 +++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 29 deletions(-) diff --git a/app.tsx b/app.tsx index 106e826..1d01703 100644 --- a/app.tsx +++ b/app.tsx @@ -38,9 +38,16 @@ type Machine = { const PRESSURE: Record = { 1: "normal", 2: "warning", 4: "critical" }; const SELECTED_MACHINE_KEY = "bb-plugin-system:selected-machine"; +const PRIMARY_COMPOSER_SELECTOR = '[data-app-composer-role="primary"]'; -function selectedComposerMachine(machines: Machine[], primaryHostId: string | null) { - const composer = document.querySelector('[data-app-composer-role="primary"]'); +// Compatibility boundary: homepageSection currently exposes the project but +// not the new-thread composer's selected host. Keep the host-DOM fallback in +// these helpers so it is easy to remove when the SDK grows that field. +function selectedComposerMachine( + composer: Element | null, + machines: Machine[], + primaryHostId: string | null, +) { const machineLabel = composer ?.querySelector('button[aria-label="Machine"]') ?.textContent?.trim(); @@ -61,21 +68,50 @@ function selectedComposerMachine(machines: Machine[], primaryHostId: string | nu return machines.find((machine) => machine.id === primaryHostId) ?? null; } -function useComposerMachine(machines: Machine[], primaryHostId: string | null) { +function useComposerMachine( + machines: Machine[], + primaryHostId: string | null, + projectId: string | null, +) { const [machine, setMachine] = useState(null); useLayoutEffect(() => { + const composer = document.querySelector(PRIMARY_COMPOSER_SELECTOR); const sync = () => { - const next = selectedComposerMachine(machines, primaryHostId); - setMachine((current) => current?.id === next?.id ? current : next); + const next = selectedComposerMachine(composer, machines, primaryHostId); + setMachine((current) => current === next ? current : next); }; sync(); + if (!composer) return; + const observer = new MutationObserver(sync); - observer.observe(document.body, { childList: true, subtree: true, characterData: true }); + observer.observe(composer, { childList: true, subtree: true, characterData: true }); return () => observer.disconnect(); - }, [machines, primaryHostId]); + }, [machines, primaryHostId, projectId]); return machine; } +function useHomepageSectionChrome( + rootRef: { current: HTMLDivElement | null }, + visible: boolean, + title: string, +) { + useLayoutEffect(() => { + const section = rootRef.current?.closest("section"); + if (!section) return; + const heading = section.querySelector(":scope > h2"); + + section.hidden = !visible; + section.classList.add("pt-4"); + if (heading) heading.textContent = title; + + return () => { + section.hidden = false; + section.classList.remove("pt-4"); + if (heading) heading.textContent = "System Stats"; + }; + }, [rootRef, title, visible]); +} + function Meter({ frac, tone }: { frac: number; tone: "ok" | "warn" | "hot" }) { const cls = tone === "hot" ? "bg-destructive" : tone === "warn" ? "bg-primary/70" : "bg-primary"; return ( @@ -344,12 +380,12 @@ function ShowOnHomeToggle() { onClick={() => setShowOnHomepage(!on)} aria-pressed={on} title={on ? "Hide the CPU / memory / disk summary from Home" : "Show the CPU / memory / disk summary on Home"} - className="flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted/40 hover:text-foreground" + className="flex cursor-pointer items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium text-muted-foreground" > {on ? "●" : "○"} - Show system stats on Home: {on ? "On" : "Off"} + Show system stats on Home: {on ? "On" : "Off"} ); } @@ -454,33 +490,22 @@ function VisibleHomeTiles({ hostId }: { hostId: string | null }) { ); } -function HomeTiles() { +function HomeTiles({ projectId }: { projectId: string | null }) { const { showOnHomepage } = useHomeVisibility(); const { machines, primaryHostId } = useMachines(); - const machine = useComposerMachine(machines, primaryHostId); + const machine = useComposerMachine(machines, primaryHostId, projectId); const rootRef = useRef(null); // Unknown stays hidden until the persisted preference loads. Treating it as // visible caused the disabled section to flash its loading state on Home. const isVisible = showOnHomepage === true; - // homepageSection has no conditional-registration API. Hide the host-owned - // heading together with our content so disabling the setting removes the - // complete section instead of leaving an orphaned "System" label behind. - useLayoutEffect(() => { - const section = rootRef.current?.closest("section"); - if (!section) return; - const heading = section.querySelector(":scope > h2"); - section.hidden = !isVisible; - section.classList.add("pt-4"); - if (heading) { - heading.textContent = machine ? `System Stats (${machine.name})` : "System Stats"; - } - return () => { - section.hidden = false; - section.classList.remove("pt-4"); - if (heading) heading.textContent = "System Stats"; - }; - }, [isVisible, machine]); + // homepageSection has no conditional-registration API, so this compatibility + // hook hides the host-owned heading together with the plugin content. + useHomepageSectionChrome( + rootRef, + isVisible, + machine ? `System Stats (${machine.name})` : "System Stats", + ); return
{isVisible ? : null}
; } From 15a7afe98256cdef971166746c6614cf7ede0382 Mon Sep 17 00:00:00 2001 From: mayank Date: Wed, 12 Aug 2026 18:35:21 +0000 Subject: [PATCH 3/4] fix: harden home stats controls --- app.tsx | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/app.tsx b/app.tsx index 1d01703..9167955 100644 --- a/app.tsx +++ b/app.tsx @@ -13,6 +13,7 @@ import { SelectTrigger, SelectValue, } from "./components/ui/select"; +import { Button } from "./components/ui/button"; import type { rpcContract } from "./server"; type Sample = { @@ -339,7 +340,9 @@ function useSystem(hostId: string | null, minutes: number, announceWatching: boo // realtime — no host settings page, no save button, instant on click. function useHomeVisibility() { const rpc = useRpc(); + const connectionState = useRealtimeConnectionState(); const [showOnHomepage, setShowOnHomepage] = useState(null); + const hasConnected = useRef(false); const load = useCallback(async () => { try { const result = await rpc.call("homeVisibility", null); @@ -351,6 +354,11 @@ function useHomeVisibility() { useEffect(() => { void load(); }, [load]); + useEffect(() => { + if (connectionState !== "connected") return; + if (hasConnected.current) void load(); + hasConnected.current = true; + }, [connectionState, load]); useRealtime("system.home-visibility", (event) => { const payload = event as { showOnHomepage?: boolean } | null; if (typeof payload?.showOnHomepage === "boolean") { @@ -375,18 +383,20 @@ function ShowOnHomeToggle() { const { showOnHomepage, setShowOnHomepage } = useHomeVisibility(); const on = showOnHomepage === true; return ( - + ); } @@ -465,11 +475,15 @@ function SystemDetails({ current, samples }: { current: Current; samples: Sample } function VisibleHomeTiles({ hostId }: { hostId: string | null }) { - const { cur } = useSystem(hostId, 5, false); + const { cur, error } = useSystem(hostId, 5, false); const s = cur?.sample; return (
- {!s ? ( + {!s && error ? ( +
+ Could not load system stats: {error} +
+ ) : !s ? (
Sampling… first data arrives shortly.
From 952570c24e81927fd2057134b4d321bad52be9c1 Mon Sep 17 00:00:00 2001 From: mayank Date: Wed, 12 Aug 2026 19:12:33 +0000 Subject: [PATCH 4/4] fix: remove DOM-based home machine detection --- README.md | 8 +-- app.tsx | 146 +++++++++++++++++++++++------------------------------- server.ts | 2 +- 3 files changed, 68 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index 573fdcb..e7a9a41 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,10 @@ bb plugin install git:https://github.com/MGrin/bb-plugin-system.git@main the last hour, and top-process tables by CPU and by memory. A machine picker switches the whole dashboard between the primary bb machine and any connected enrolled machine. -**Homepage tiles** — a compact CPU / memory / disk row for the machine selected in -the Home composer. They are off by default; toggle **Show system stats on Home** in -the top-right of the System panel to show or hide them. The saved choice applies -instantly and is remembered. +**Homepage tiles** — a compact CPU / memory / disk row for the primary bb machine. +They are shown by default; toggle **Show system stats on Home** in the top-right of +the System panel to show or hide them. The saved choice applies instantly and is +remembered. **`bb system`** — the same data for agents, as text: diff --git a/app.tsx b/app.tsx index 9167955..a755d38 100644 --- a/app.tsx +++ b/app.tsx @@ -39,78 +39,21 @@ type Machine = { const PRESSURE: Record = { 1: "normal", 2: "warning", 4: "critical" }; const SELECTED_MACHINE_KEY = "bb-plugin-system:selected-machine"; -const PRIMARY_COMPOSER_SELECTOR = '[data-app-composer-role="primary"]'; -// Compatibility boundary: homepageSection currently exposes the project but -// not the new-thread composer's selected host. Keep the host-DOM fallback in -// these helpers so it is easy to remove when the SDK grows that field. -function selectedComposerMachine( - composer: Element | null, - machines: Machine[], - primaryHostId: string | null, -) { - const machineLabel = composer - ?.querySelector('button[aria-label="Machine"]') - ?.textContent?.trim(); - if (machineLabel) { - const selected = machines.find((machine) => machineLabel === machine.name); - if (selected) return selected; - } - - const environmentLabel = composer - ?.querySelector('button[aria-label="Environment"] [data-promptbox-full-label]') - ?.textContent?.trim(); - if (environmentLabel) { - const selected = machines.find((machine) => environmentLabel.startsWith(`${machine.name} ·`)); - if (selected) return selected; - } - - // The environment chip omits the machine name for the primary host. - return machines.find((machine) => machine.id === primaryHostId) ?? null; -} - -function useComposerMachine( - machines: Machine[], - primaryHostId: string | null, - projectId: string | null, -) { - const [machine, setMachine] = useState(null); - useLayoutEffect(() => { - const composer = document.querySelector(PRIMARY_COMPOSER_SELECTOR); - const sync = () => { - const next = selectedComposerMachine(composer, machines, primaryHostId); - setMachine((current) => current === next ? current : next); - }; - sync(); - if (!composer) return; - - const observer = new MutationObserver(sync); - observer.observe(composer, { childList: true, subtree: true, characterData: true }); - return () => observer.disconnect(); - }, [machines, primaryHostId, projectId]); - return machine; -} - -function useHomepageSectionChrome( +function useHomepageSectionVisibility( rootRef: { current: HTMLDivElement | null }, visible: boolean, - title: string, ) { useLayoutEffect(() => { const section = rootRef.current?.closest("section"); if (!section) return; - const heading = section.querySelector(":scope > h2"); section.hidden = !visible; - section.classList.add("pt-4"); - if (heading) heading.textContent = title; return () => { section.hidden = false; - section.classList.remove("pt-4"); - if (heading) heading.textContent = "System Stats"; }; - }, [rootRef, title, visible]); + }, [rootRef, visible]); } function Meter({ frac, tone }: { frac: number; tone: "ok" | "warn" | "hot" }) { @@ -290,12 +233,11 @@ function useSystem(hostId: string | null, minutes: number, announceWatching: boo const [error, setError] = useState(null); const loadId = useRef(0); const load = useCallback(async () => { - if (!hostId) return; const requestId = ++loadId.current; try { const [nextCur, nextHist] = await Promise.all([ - rpc.call("current", { hostId }), - rpc.call("history", { hostId, minutes }), + rpc.call("current", hostId ? { hostId } : null), + rpc.call("history", hostId ? { hostId, minutes } : { minutes }), ]); if (requestId !== loadId.current) return; setCur(nextCur as Current); @@ -311,14 +253,13 @@ function useSystem(hostId: string | null, minutes: number, announceWatching: boo setCur(null); setHist(null); setError(null); - if (!hostId) return; void load(); const timer = announceWatching - ? setInterval(() => void rpc.call("watching", { hostId }), 60_000) + ? setInterval(() => void rpc.call("watching", hostId ? { hostId } : null), 60_000) : null; if (announceWatching) { // Tell the sampler someone is looking, so it samples at the fast cadence. - void rpc.call("watching", { hostId }); + void rpc.call("watching", hostId ? { hostId } : null); } return () => { loadId.current++; @@ -329,7 +270,7 @@ function useSystem(hostId: string | null, minutes: number, announceWatching: boo // The sampler publishes for every connected host; only the machine on // screen needs a reload. const payload = event as { hostId?: string } | null; - if (payload?.hostId && payload.hostId !== hostId) return; + if (hostId && payload?.hostId && payload.hostId !== hostId) return; void load(); }); return { cur, hist, error }; @@ -342,13 +283,22 @@ function useHomeVisibility() { const rpc = useRpc(); const connectionState = useRealtimeConnectionState(); const [showOnHomepage, setShowOnHomepage] = useState(null); + const [retryPending, setRetryPending] = useState(false); + const [error, setError] = useState(null); const hasConnected = useRef(false); - const load = useCallback(async () => { + const load = useCallback(async (isRetry = false) => { + setError(null); try { const result = await rpc.call("homeVisibility", null); setShowOnHomepage(result.showOnHomepage); - } catch { - setShowOnHomepage(false); + setRetryPending(false); + } catch (cause) { + if (isRetry) { + setRetryPending(false); + setError(cause instanceof Error ? cause.message : "Unable to load Home visibility"); + } else { + setRetryPending(true); + } } }, [rpc]); useEffect(() => { @@ -359,10 +309,20 @@ function useHomeVisibility() { if (hasConnected.current) void load(); hasConnected.current = true; }, [connectionState, load]); + useEffect(() => { + if (!retryPending || connectionState !== "connected") return; + const timer = setTimeout(() => { + setRetryPending(false); + void load(true); + }, 5_000); + return () => clearTimeout(timer); + }, [connectionState, load, retryPending]); useRealtime("system.home-visibility", (event) => { const payload = event as { showOnHomepage?: boolean } | null; if (typeof payload?.showOnHomepage === "boolean") { setShowOnHomepage(payload.showOnHomepage); + setRetryPending(false); + setError(null); } }); const setVisible = useCallback( @@ -371,16 +331,42 @@ function useHomeVisibility() { try { await rpc.call("setHomeVisibility", { showOnHomepage: next }); } catch { - void load(); + setShowOnHomepage(null); + void load(false); } }, [load, rpc], ); - return { showOnHomepage, setShowOnHomepage: setVisible }; + return { + showOnHomepage, + setShowOnHomepage: setVisible, + error, + retry: () => void load(false), + }; } function ShowOnHomeToggle() { - const { showOnHomepage, setShowOnHomepage } = useHomeVisibility(); + const { showOnHomepage, setShowOnHomepage, error, retry } = useHomeVisibility(); + if (error) { + return ( +
+ Could not load Home visibility. + +
+ ); + } + if (showOnHomepage === null) { + return Loading Home visibility…; + } const on = showOnHomepage === true; return (