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,5 +1,6 @@
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { createIntegrationAsync } from "@homarr/integrations";
|
||||
import type {
|
||||
BeszelAlert,
|
||||
@@ -13,6 +14,8 @@ import type {
|
||||
|
||||
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
||||
|
||||
const logger = createLogger({ module: "beszelRequestHandler" });
|
||||
|
||||
export type { BeszelSystemRow } from "@homarr/integrations/types";
|
||||
|
||||
/**
|
||||
@@ -56,6 +59,7 @@ export const beszelSystemsRequestHandler = createCachedIntegrationRequestHandler
|
||||
Record<string, never>
|
||||
>({
|
||||
async requestAsync(integration) {
|
||||
const start = performance.now();
|
||||
const instance = await createIntegrationAsync(integration);
|
||||
const systems = await instance.getSystemsAsync();
|
||||
const enriched = await Promise.all(
|
||||
@@ -64,9 +68,16 @@ export const beszelSystemsRequestHandler = createCachedIntegrationRequestHandler
|
||||
return mapToSystemRow(system, details);
|
||||
}),
|
||||
);
|
||||
logger.debug("beszelSystems fetch completed", {
|
||||
integrationId: integration.id,
|
||||
durationMs: Math.round(performance.now() - start),
|
||||
count: enriched.length,
|
||||
});
|
||||
return enriched;
|
||||
},
|
||||
cacheDuration: dayjs.duration(5, "seconds"),
|
||||
fallbackToStaleOnError: true,
|
||||
retry: { attempts: 2, delayMs: 500 },
|
||||
queryKey: "beszelSystems",
|
||||
});
|
||||
|
||||
@@ -81,12 +92,21 @@ export const beszelAlertsRequestHandler = createCachedIntegrationRequestHandler<
|
||||
{ includeHistory: boolean; maxHistoryItems: number }
|
||||
>({
|
||||
async requestAsync(integration, input) {
|
||||
const start = performance.now();
|
||||
const instance = await createIntegrationAsync(integration);
|
||||
const alerts = await instance.getAlertsAsync();
|
||||
const history = input.includeHistory ? await instance.getAlertHistoryAsync(undefined, input.maxHistoryItems) : [];
|
||||
logger.debug("beszelAlerts fetch completed", {
|
||||
integrationId: integration.id,
|
||||
durationMs: Math.round(performance.now() - start),
|
||||
alertsCount: alerts.length,
|
||||
historyCount: history.length,
|
||||
});
|
||||
return { alerts, history };
|
||||
},
|
||||
cacheDuration: dayjs.duration(15, "seconds"),
|
||||
fallbackToStaleOnError: true,
|
||||
retry: { attempts: 2, delayMs: 500 },
|
||||
queryKey: "beszelAlerts",
|
||||
});
|
||||
|
||||
@@ -110,15 +130,26 @@ export const beszelStatsRequestHandler = createCachedIntegrationRequestHandler<
|
||||
{ systemId: string; timePeriod: string; includeDocker: boolean }
|
||||
>({
|
||||
async requestAsync(integration, input) {
|
||||
const start = performance.now();
|
||||
const config = timePeriodConfig[input.timePeriod] ?? { type: "1m", perPage: 60, cacheSeconds: 60 };
|
||||
const instance = await createIntegrationAsync(integration);
|
||||
const systemStats = await instance.getSystemStatsAsync(input.systemId, config.type, config.perPage);
|
||||
const containerStats = input.includeDocker
|
||||
? await instance.getContainerStatsAsync(input.systemId, config.type, config.perPage).catch(() => [])
|
||||
: [];
|
||||
logger.debug("beszelStats fetch completed", {
|
||||
integrationId: integration.id,
|
||||
systemId: input.systemId,
|
||||
timePeriod: input.timePeriod,
|
||||
durationMs: Math.round(performance.now() - start),
|
||||
systemStatsCount: systemStats.length,
|
||||
containerStatsCount: containerStats.length,
|
||||
});
|
||||
return { systemStats, containerStats };
|
||||
},
|
||||
cacheDuration: dayjs.duration(60, "seconds"),
|
||||
fallbackToStaleOnError: true,
|
||||
retry: { attempts: 2, delayMs: 500 },
|
||||
queryKey: "beszelStats",
|
||||
cacheDurationForInput(input) {
|
||||
const config = timePeriodConfig[input.timePeriod];
|
||||
|
||||
Reference in new issue
Block a user