From a68eee24ebc2f156541d12a6a8436ef333bc8307 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Mon, 15 Jun 2026 15:49:01 +0200 Subject: [PATCH] fix(beszel): improve live subscription error handling and cleanup - Surface subscription errors in useLiveStats via onError + error state - Show error UI in live mode when SSE connection fails - Abort SSE reader on subscribeTopic failure instead of silent log - Guard mock realtime against pre-aborted signal to prevent interval leak - Remove dead includeDocker from subscription input schema --- packages/api/src/router/widgets/beszel.ts | 1 - .../src/beszel/beszel-integration.ts | 3 ++- packages/integrations/src/mock/data/beszel.ts | 3 ++- .../src/beszel-system-stats/component.tsx | 17 +++++++++++++++-- .../src/beszel/_shared/use-live-stats.ts | 11 +++++++++-- 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/api/src/router/widgets/beszel.ts b/packages/api/src/router/widgets/beszel.ts index fb1e58b5c..423c750a1 100644 --- a/packages/api/src/router/widgets/beszel.ts +++ b/packages/api/src/router/widgets/beszel.ts @@ -154,7 +154,6 @@ export const beszelRouter = createTRPCRouter({ .input( z.object({ systemId: z.string(), - includeDocker: z.boolean().default(true), }), ) .subscription(({ ctx, input }) => { diff --git a/packages/integrations/src/beszel/beszel-integration.ts b/packages/integrations/src/beszel/beszel-integration.ts index 9066b9cad..64887d2cc 100644 --- a/packages/integrations/src/beszel/beszel-integration.ts +++ b/packages/integrations/src/beszel/beszel-integration.ts @@ -283,7 +283,8 @@ export class BeszelIntegration extends Integration { if (!clientId && typeof parsed.clientId === "string") { clientId = parsed.clientId; this.subscribeTopic(clientId, systemId, session.token).catch((err) => { - logger.warn("Failed to subscribe to rt_metrics topic", { error: err }); + logger.warn("Failed to subscribe to rt_metrics topic, aborting SSE", { error: err }); + reader.cancel().catch(() => {}); }); return; } diff --git a/packages/integrations/src/mock/data/beszel.ts b/packages/integrations/src/mock/data/beszel.ts index e099f00be..d088510e4 100644 --- a/packages/integrations/src/mock/data/beszel.ts +++ b/packages/integrations/src/mock/data/beszel.ts @@ -350,12 +350,13 @@ export class BeszelMockService { }; onMessage({ stats: record, containerStats: null }); }; + if (signal.aborted) return; await new Promise((resolve) => { const interval = setInterval(emit, 1000); signal.addEventListener("abort", () => { clearInterval(interval); resolve(); - }); + }, { once: true }); }); } diff --git a/packages/widgets/src/beszel-system-stats/component.tsx b/packages/widgets/src/beszel-system-stats/component.tsx index 7e457182e..0f5e0df59 100644 --- a/packages/widgets/src/beszel-system-stats/component.tsx +++ b/packages/widgets/src/beszel-system-stats/component.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo } from "react"; import { ActionIcon, Center, Menu, ScrollArea, Select, SimpleGrid, Stack, Text } from "@mantine/core"; -import { IconServer, IconQuestionMark } from "@tabler/icons-react"; +import { IconPlugConnectedX, IconServer, IconQuestionMark } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; import { useScopedI18n } from "@homarr/translation/client"; @@ -77,7 +77,7 @@ export default function BeszelSystemStatsWidget({ { staleTime: 30 * 1000, enabled: !isLive && systemReady && selectedSystem !== "" }, ); - const liveData = useLiveStats(integrationIds, selectedSystem, isLive && systemReady); + const { data: liveData, error: liveError } = useLiveStats(integrationIds, selectedSystem, isLive && systemReady); const activeStats = isLive ? liveData : statsResult; @@ -142,6 +142,19 @@ export default function BeszelSystemStatsWidget({ ); } + if (isLive && liveError && !liveData) { + return ( +
+ + + + {t("error.internalServerError")} + + +
+ ); + } + return ( { const [data, setData] = useState(null); + const [error, setError] = useState(null); const integrationKey = integrationIds.join(","); useEffect(() => { setData(null); + setError(null); }, [systemId, enabled, integrationKey]); const onData = useCallback( (incoming: { stats: BeszelSystemStatsRecord; containerStats: BeszelContainerStatsRecord | null }) => { + setError(null); setData((prev) => { const systemStats = [...(prev?.systemStats ?? []), incoming.stats].slice(-MAX_BUFFER); const containerStats = incoming.containerStats @@ -33,10 +36,14 @@ export const useLiveStats = (integrationIds: string[], systemId: string, enabled [], ); + const onError = useCallback((err: unknown) => { + setError(err instanceof Error ? err : new Error(String(err))); + }, []); + clientApi.widget.beszel.subscribeSystemStats.useSubscription( { integrationIds, systemId }, - { enabled: enabled && systemId !== "", onData }, + { enabled: enabled && systemId !== "", onData, onError }, ); - return data; + return { data, error }; };