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
This commit is contained in:
5 files changed
+28
-7
No files matched your search
@@ -154,7 +154,6 @@ export const beszelRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
systemId: z.string(),
|
||||
includeDocker: z.boolean().default(true),
|
||||
}),
|
||||
)
|
||||
.subscription(({ ctx, input }) => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -350,12 +350,13 @@ export class BeszelMockService {
|
||||
};
|
||||
onMessage({ stats: record, containerStats: null });
|
||||
};
|
||||
if (signal.aborted) return;
|
||||
await new Promise<void>((resolve) => {
|
||||
const interval = setInterval(emit, 1000);
|
||||
signal.addEventListener("abort", () => {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
});
|
||||
}, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<Center h="100%">
|
||||
<Stack align="center" gap="sm" p="md">
|
||||
<IconPlugConnectedX size={40} />
|
||||
<Text size="sm" c="dimmed" ta="center">
|
||||
{t("error.internalServerError")}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollArea
|
||||
h="100%"
|
||||
|
||||
@@ -14,14 +14,17 @@ interface LiveStatsData {
|
||||
|
||||
export const useLiveStats = (integrationIds: string[], systemId: string, enabled: boolean) => {
|
||||
const [data, setData] = useState<LiveStatsData | null>(null);
|
||||
const [error, setError] = useState<Error | null>(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 };
|
||||
};
|
||||
Reference in new issue
Block a user