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:
Thomasandajnart authored and GitHub committed 2026-06-22 15:20:12 +02:00
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>
);
}