fix(beszel): harden cache layer and prevent blank widgets (#6052)
* fix(beszel): harden cache layer and prevent blank widgets - Add debug logging across Beszel integration, request handler, and tRPC router (auth, fetch, SSE, per-procedure entry/exit) gated behind LOG_LEVEL=debug - cached-request-handler: retry with backoff, isValid predicate to skip caching invalid data, in-flight dedup (single-flight per channel) to avoid thundering herd after cache invalidation - redis channel: set-then-publish so storage is always consistent even when pubsub publish fails - Beszel handlers: opt into fallbackToStaleOnError and retry so brief upstream hiccups don't throw away good cached data - beszel-system-stats: add refetchInterval matching each time period's cache TTL so historical charts refresh in the background - tRPC router: switch Promise.all -> Promise.allSettled for getSystems and getAlerts; getSystemStats returns an error object instead of throwing. Each result carries an optional error field so one failed integration no longer nukes the whole widget - Widgets: add explicit isPending loader, empty state for grid and stats, and a shared per-integration error indicator (tooltip with failed integration names). Alerts and table now only show their empty state after loading completes - Translation: add empty.noSystems key to beszelSystemGrid and beszelSystemStats Addresses #6030 * style: auto-fix lint and format --------- Co-authored-by: ajnart <ajnart@users.noreply.github.com>
This commit is contained in:
1 parent
577950d20e
commit
49c266374d
12 files changed
+730
-323
No files matched your search
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { MantineSize } from "@mantine/core";
|
||||
import { Badge, Box, Card, Group, Progress, Text, Stack } from "@mantine/core";
|
||||
import { Badge, Box, Card, Center, Group, Loader, Progress, Text, Stack } from "@mantine/core";
|
||||
import {
|
||||
Activity,
|
||||
Battery,
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Thermometer,
|
||||
Wifi,
|
||||
} from "lucide-react";
|
||||
import { IconServerOff } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
@@ -28,6 +29,7 @@ import type { BeszelSystemRow } from "../beszel/_shared/types";
|
||||
import { statusColorMap, thresholdColor } from "../beszel/_shared/colors";
|
||||
import { formatByteRate, formatLoadAvg, formatPercent, formatTemp, formatUptime } from "../beszel/_shared/format";
|
||||
import { useBeszelFilteredSystems, useBeszelSystemsSubscription } from "../beszel/_shared/hooks";
|
||||
import { BeszelIntegrationErrorIndicator } from "../beszel/_shared/error-indicator";
|
||||
import { BeszelSystemStatsModal } from "../beszel/_shared/system-stats-modal";
|
||||
|
||||
interface SizeConfig {
|
||||
@@ -363,7 +365,11 @@ export default function BeszelSystemGridWidget({
|
||||
const board = useRequiredBoard();
|
||||
const { openModal } = useModalAction(BeszelSystemStatsModal);
|
||||
|
||||
const { data: results = [], error: systemsError } = clientApi.widget.beszel.getSystems.useQuery(
|
||||
const {
|
||||
data: results = [],
|
||||
error: systemsError,
|
||||
isPending,
|
||||
} = clientApi.widget.beszel.getSystems.useQuery(
|
||||
{ integrationIds },
|
||||
{ staleTime: 5_000, refetchInterval: 5_000, retry: false },
|
||||
);
|
||||
@@ -373,6 +379,30 @@ export default function BeszelSystemGridWidget({
|
||||
|
||||
if (systemsError) throw systemsError;
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<Center h="100%">
|
||||
<Loader size="sm" />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
if (filteredSystems.length === 0) {
|
||||
return (
|
||||
<Box h="100%" pos="relative" style={{ pointerEvents: isEditMode ? "none" : undefined }}>
|
||||
<BeszelIntegrationErrorIndicator results={results} />
|
||||
<Center h="100%">
|
||||
<Stack align="center" gap="xs">
|
||||
<IconServerOff size={28} opacity={0.5} />
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("empty.noSystems")}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const cols = getColCount(width, filteredSystems.length);
|
||||
const rows = Math.ceil(filteredSystems.length / cols) || 1;
|
||||
const rawCellHeight = height / rows;
|
||||
@@ -383,35 +413,37 @@ export default function BeszelSystemGridWidget({
|
||||
const maxMetrics = getMaxVisibleMetrics(effectiveCellHeight, size);
|
||||
|
||||
return (
|
||||
<Box
|
||||
h="100%"
|
||||
style={{
|
||||
pointerEvents: isEditMode ? "none" : undefined,
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${cols}, 1fr)`,
|
||||
gridTemplateRows: scrollEnabled ? `repeat(${rows}, ${MIN_CELL_HEIGHT}px)` : `repeat(${rows}, 1fr)`,
|
||||
gap: size.gap,
|
||||
overflow: scrollEnabled ? "auto" : "hidden",
|
||||
}}
|
||||
>
|
||||
{filteredSystems.map((system) => {
|
||||
const integrationId = system._key.split(":")[0] ?? "";
|
||||
const handleClick = isEditMode
|
||||
? undefined
|
||||
: () => openModal({ integrationId, systemId: system.id }, { title: system.name });
|
||||
return (
|
||||
<SystemCard
|
||||
key={system._key}
|
||||
system={system}
|
||||
options={options}
|
||||
t={t}
|
||||
size={size}
|
||||
maxMetrics={maxMetrics}
|
||||
itemRadius={board.itemRadius}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Box h="100%" pos="relative" style={{ pointerEvents: isEditMode ? "none" : undefined }}>
|
||||
<BeszelIntegrationErrorIndicator results={results} />
|
||||
<Box
|
||||
h="100%"
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${cols}, 1fr)`,
|
||||
gridTemplateRows: scrollEnabled ? `repeat(${rows}, ${MIN_CELL_HEIGHT}px)` : `repeat(${rows}, 1fr)`,
|
||||
gap: size.gap,
|
||||
overflow: scrollEnabled ? "auto" : "hidden",
|
||||
}}
|
||||
>
|
||||
{filteredSystems.map((system) => {
|
||||
const integrationId = system._key.split(":")[0] ?? "";
|
||||
const handleClick = isEditMode
|
||||
? undefined
|
||||
: () => openModal({ integrationId, systemId: system.id }, { title: system.name });
|
||||
return (
|
||||
<SystemCard
|
||||
key={system._key}
|
||||
system={system}
|
||||
options={options}
|
||||
t={t}
|
||||
size={size}
|
||||
maxMetrics={maxMetrics}
|
||||
itemRadius={board.itemRadius}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user