Compare commits

..
Author SHA1 Message Date
ajnart f290328dd5 style: auto-fix lint and format 2026-06-22 15:01:59 +00:00
Thomas Camlong 065b3de1db fix(beszel): leverage TanStack Query for stale-while-revalidate caching
Server-side Redis cache reduced to 1s dedup window. TanStack Query
(staleTime 10s, gcTime 48h, refetchInterval 10s) now controls freshness.
SSR hydration from persister provides instant page loads with old data,
background refetches hit Beszel directly every 10s.

Fixes issue where widgets showed stale/errored data indefinitely due to
long Redis cache durations preventing refetches.
2026-06-22 17:00:29 +02:00
Thomas ea9dd716f0 Merge pull request #6053 from homarr-labs/beszel-1w-30d-data-view
fix(beszel): use 480m interval for 1w/30d stats views
2026-06-22 15:44:43 +02:00
Thomas Camlong 4383da38ef fix(beszel): use 480m interval for 1w/30d stats views
Beszel only stores stats at 1m, 10m, 20m, 120m, 480m intervals.
The 1440m type doesn't exist, so queries for 1w/30d returned empty
results. Use 480m (8h) with appropriate perPage (21 for 1w, 90 for 30d).
2026-06-22 15:44:14 +02:00
homarr-crowdin[bot] 58cb65cb3a chore(lang): updated translations from crowdin (#6029) 2026-06-22 15:23:27 +02:00
Matt Van Horn 89380f81dd feat: ship source maps for readable production stack traces (#6031) 2026-06-22 15:22:54 +02:00
Thomas e01571b221 fix(icon-picker): keep URL visible in edit mode, add SVG-first ordering (#6051) 2026-06-22 15:22:25 +02:00
Thomasandajnart 49c266374d 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>
2026-06-22 15:20:12 +02:00
16 changed files with 892 additions and 839 deletions

No files matched your search

+14
View File
@@ -20,6 +20,17 @@ ARG DISABLE_REDIS_LOGS='true'
RUN corepack enable pnpm && pnpm build
# The standalone output tracing omits server chunk source maps (.next/server/**/*.map), which is
# where production Node stack frames point. Copy the .map files into the standalone tree (preserving
# directory structure) so the existing standalone COPY ships them, keeping traces readable.
# See https://github.com/homarr-labs/homarr/issues/5891
RUN cd apps/nextjs/.next/server && \
find . -name '*.map' | while IFS= read -r map; do \
dest="../standalone/apps/nextjs/.next/server/$map"; \
mkdir -p "$(dirname "$dest")"; \
cp "$map" "$dest"; \
done
FROM base AS runner
WORKDIR /app
@@ -64,6 +75,9 @@ ENV DB_DRIVER='better-sqlite3'
ENV AUTH_PROVIDERS='credentials'
ENV REDIS_IS_EXTERNAL='false'
ENV NODE_ENV='production'
# Tell Node to symbolicate stack traces using the shipped .map files so production logs
# show original source locations instead of minified frames, see https://github.com/homarr-labs/homarr/issues/5891
ENV NODE_OPTIONS='--enable-source-maps'
ENTRYPOINT [ "/app/entrypoint.sh" ]
CMD ["sh", "run.sh"]
+4
View File
@@ -31,6 +31,8 @@ const nextConfig: NextConfig = {
},
output: "standalone",
reactStrictMode: true,
// Ship browser source maps so production client stack traces are readable, see https://github.com/homarr-labs/homarr/issues/5891
productionBrowserSourceMaps: true,
// react compiler breaks mantine-react-table, so disabled for now
//reactCompiler: true,
/** We already do typechecking as separate tasks in CI */
@@ -41,6 +43,8 @@ const nextConfig: NextConfig = {
*/
serverExternalPackages: ["dockerode", "isomorphic-dompurify", "jsdom", "better-sqlite3"],
experimental: {
// Ship server source maps so production Node stack traces in logs reference original source, see https://github.com/homarr-labs/homarr/issues/5891
serverSourceMaps: true,
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
turbopackFileSystemCacheForDev: true,
preloadEntriesOnStart: false,
+25 -9
View File
@@ -15,20 +15,36 @@ export const iconsRouter = createTRPCRouter({
})
.input(iconsFindSchema)
.query(async ({ ctx, input }) => {
const term = input.searchText?.toLowerCase().trim() ?? "";
const keywords = (input.searchText ?? "").split(" ").filter((keyword) => keyword.length > 0);
let whereCondition = undefined;
if (term.length > 0) {
whereCondition = and(...keywords.map((keyword) => like(icons.name, `%${keyword}%`)));
}
return {
icons: await ctx.db.query.iconRepositories.findMany({
with: {
icons: {
columns: {
id: true,
name: true,
url: true,
columns: { id: true, name: true, url: true },
where: whereCondition,
orderBy: (table, { asc, sql }) => {
// ponytail: SVG first, then alphabetical; with a search term,
// prepend a relevance tier (exact > prefix > substring).
const svgFirst = sql`CASE WHEN ${table.name} LIKE '%.svg' THEN 0 ELSE 1 END`;
const nameAsc = asc(table.name);
if (term.length === 0) {
return [svgFirst, nameAsc];
}
const tier = sql`CASE
WHEN LOWER(${table.name}) = ${term} THEN 0
WHEN LOWER(${table.name}) LIKE ${term + "%"} THEN 1
WHEN LOWER(${table.name}) LIKE ${`%${term}%`} THEN 2
ELSE 3
END`;
return [tier, svgFirst, nameAsc];
},
where:
(input.searchText?.length ?? 0) > 0
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
and(...input.searchText!.split(" ").map((keyword) => like(icons.name, `%${keyword}%`)))
: undefined,
limit: input.limitPerGroup,
},
},
@@ -1,8 +1,11 @@
"use client";
import type { FocusEventHandler } from "react";
import { startTransition } from "react";
import { startTransition, useEffect, useState } from "react";
import {
ActionIcon,
Box,
Button,
Card,
Combobox,
Flex,
@@ -18,7 +21,7 @@ import {
useCombobox,
} from "@mantine/core";
import { useDebouncedValue, useUncontrolled } from "@mantine/hooks";
import { IconUpload } from "@tabler/icons-react";
import { IconMoodEmpty, IconSearchOff, IconUpload } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
@@ -34,7 +37,6 @@ interface IconPickerProps {
error?: string | null;
onFocus?: FocusEventHandler;
onBlur?: FocusEventHandler;
label?: string;
placeholder?: string;
withAsterisk?: boolean;
@@ -50,29 +52,30 @@ export const IconPicker = ({
label,
placeholder,
}: IconPickerProps) => {
const [value, setValue] = useUncontrolled({
value: propsValue,
onChange,
});
const [search, setSearch] = useUncontrolled({
value,
onChange: (value) => {
setValue(value);
},
});
const [previewUrl, setPreviewUrl] = useUncontrolled({
value: propsValue ?? null,
});
const [value, setValue] = useUncontrolled({ value: propsValue, onChange });
const [query, setQuery] = useState("");
// ponytail: reset the typed query when the committed value changes
// externally (form reset, parent re-render) so the input reverts to the URL.
useEffect(() => {
setQuery("");
}, [value]);
const { data: session } = useSession();
const tCommon = useScopedI18n("common");
const [debouncedQuery] = useDebouncedValue(query, 100);
const [debouncedSearch] = useDebouncedValue(search, 100);
// ponytail: only search when the typed query differs from the committed
// value. On mount the input shows the existing URL (edit mode), which
// would never match the bare filenames in the icon table.
const searchText = (debouncedQuery !== (value ?? "") && debouncedQuery) || "";
// We use not useSuspenseQuery as it would cause an above Suspense boundary to trigger and so searching for something is bad UX.
const { data } = clientApi.icon.findIcons.useQuery({
searchText: debouncedSearch,
});
// ponytail: not useSuspenseQuery — an above Suspense boundary would
// trigger on every keystroke and the dropdown flash is bad UX.
const { data, isLoading, isError, refetch } = clientApi.icon.findIcons.useQuery(
{ searchText },
{ placeholderData: (prev) => prev },
);
const combobox = useCombobox({
onDropdownClose: () => combobox.resetSelectedOption(),
@@ -90,25 +93,15 @@ export const IconPicker = ({
>
<UnstyledButton
onClick={() => {
const value = item.url;
startTransition(() => {
setPreviewUrl(value);
setSearch(value);
setValue(value);
setValue(item.url);
setQuery("");
combobox.closeDropdown();
});
}}
>
<Indicator label="SVG" disabled={!item.url.endsWith(".svg")} size={16}>
<Card
p="sm"
pos="relative"
style={{
overflow: "visible",
cursor: "pointer",
}}
className={classes.iconCard}
>
<Card p="sm" pos="relative" style={{ overflow: "visible", cursor: "pointer" }} className={classes.iconCard}>
<Box w={25} h={25}>
<Image src={item.url} w={25} h={25} />
</Box>
@@ -130,6 +123,48 @@ export const IconPicker = ({
);
});
// ponytail: show the typed query if non-empty, else fall back to the URL.
const inputValue = query || value || "";
// ponytail: show the existing icon as a left-section preview.
const leftSection = shouldShowPreview(value) && <img src={value} alt="" style={{ width: 20, height: 20 }} />;
// ponytail: prop override -> real count -> loading text.
const placeholderText =
placeholder ||
(data && tCommon("iconPicker.header", { countIcons: String(data.countIcons) })) ||
tCommon("iconPicker.headerLoading");
const renderDropdown = () => {
if (isError) {
return (
<Stack align="center" gap="xs" p="md">
<IconSearchOff size={32} stroke={1.5} />
<Text size="sm">{tCommon("iconPicker.error")}</Text>
<Button size="xs" variant="default" onClick={() => refetch()}>
{tCommon("action.tryAgain")}
</Button>
</Stack>
);
}
if (isLoading) {
return Array(15)
.fill(0)
.map((_, index) => (
<Combobox.Option value={`skeleton-${index}`} key={index} disabled>
<Skeleton height={25} visible />
</Combobox.Option>
));
}
if (totalOptions > 0) {
return <Stack gap={4}>{groups}</Stack>;
}
return (
<Stack align="center" gap="xs" p="md">
<IconMoodEmpty size={32} stroke={1.5} />
<Text size="sm">{tCommon("iconPicker.noResults", { search: debouncedQuery })}</Text>
</Stack>
);
};
return (
<Combobox store={combobox} withinPortal>
<Combobox.Target>
@@ -137,16 +172,11 @@ export const IconPicker = ({
<InputBase
flex={1}
rightSection={<Combobox.Chevron />}
leftSection={
shouldShowPreview(previewUrl) ? <img src={previewUrl} alt="" style={{ width: 20, height: 20 }} /> : null
}
value={search}
leftSection={leftSection}
value={inputValue}
onChange={(event) => {
combobox.openDropdown();
combobox.updateSelectedOptionIndex();
setSearch(event.currentTarget.value);
setValue(event.currentTarget.value);
setPreviewUrl(null);
setQuery(event.currentTarget.value);
}}
onClick={() => combobox.openDropdown()}
onFocus={(event) => {
@@ -156,25 +186,21 @@ export const IconPicker = ({
onBlur={(event) => {
onBlur?.(event);
combobox.closeDropdown();
setPreviewUrl(value);
setSearch(value || "");
setQuery("");
}}
rightSectionPointerEvents="none"
withAsterisk={withAsterisk}
error={error}
label={label ?? tCommon("iconPicker.label")}
placeholder={placeholder ?? tCommon("iconPicker.header", { countIcons: String(data?.countIcons ?? 0) })}
placeholder={placeholderText}
/>
{session?.user.permissions.includes("media-upload") && (
<UploadMedia
onSuccess={(medias) => {
const first = medias.at(0);
if (!first) return;
startTransition(() => {
setValue(first.url);
setPreviewUrl(first.url);
setSearch(first.url);
});
}}
>
@@ -190,17 +216,7 @@ export const IconPicker = ({
<Combobox.Dropdown>
<Combobox.Options mah={350} style={{ overflowY: "auto" }}>
{totalOptions > 0 ? (
<Stack gap={4}>{groups}</Stack>
) : (
Array(15)
.fill(0)
.map((_, index: number) => (
<Combobox.Option value={`skeleton-${index}`} key={index} disabled>
<Skeleton height={25} visible />
</Combobox.Option>
))
)}
{renderDropdown()}
</Combobox.Options>
</Combobox.Dropdown>
</Combobox>
@@ -209,8 +225,8 @@ export const IconPicker = ({
// This regex is used to prevent loading a preview like en or /en which would trigger a language change
// See https://github.com/homarr-labs/homarr/issues/3070
const localizationPathRegex = new RegExp(`^/?(${supportedLanguages.join("|")})(/.*)?$`, "i" /* ignore casing */);
const shouldShowPreview = (value: string | null): value is string => {
const localizationPathRegex = new RegExp(`^/?(${supportedLanguages.join("|")})(/.*)?$`, "i");
const shouldShowPreview = (value: string | null | undefined): value is string => {
if (!value) return false;
return !localizationPathRegex.test(value);
};
+11 -16
View File
@@ -75,7 +75,7 @@ export const beszelSystemsRequestHandler = createCachedIntegrationRequestHandler
});
return enriched;
},
cacheDuration: dayjs.duration(5, "seconds"),
cacheDuration: dayjs.duration(1, "second"),
fallbackToStaleOnError: true,
retry: { attempts: 2, delayMs: 500 },
queryKey: "beszelSystems",
@@ -104,19 +104,19 @@ export const beszelAlertsRequestHandler = createCachedIntegrationRequestHandler<
});
return { alerts, history };
},
cacheDuration: dayjs.duration(15, "seconds"),
cacheDuration: dayjs.duration(1, "second"),
fallbackToStaleOnError: true,
retry: { attempts: 2, delayMs: 500 },
queryKey: "beszelAlerts",
});
const timePeriodConfig: Record<string, { type: string; perPage: number; cacheSeconds: number }> = {
"1m": { type: "1m", perPage: 60, cacheSeconds: 5 },
"1h": { type: "1m", perPage: 60, cacheSeconds: 60 },
"12h": { type: "10m", perPage: 72, cacheSeconds: 300 },
"24h": { type: "20m", perPage: 72, cacheSeconds: 600 },
"1w": { type: "1440m", perPage: 9, cacheSeconds: 1800 },
"30d": { type: "1440m", perPage: 32, cacheSeconds: 3600 },
const timePeriodConfig: Record<string, { type: string; perPage: number }> = {
"1m": { type: "1m", perPage: 60 },
"1h": { type: "1m", perPage: 60 },
"12h": { type: "10m", perPage: 72 },
"24h": { type: "20m", perPage: 72 },
"1w": { type: "480m", perPage: 21 },
"30d": { type: "480m", perPage: 90 },
};
export interface BeszelStatsData {
@@ -131,7 +131,7 @@ export const beszelStatsRequestHandler = createCachedIntegrationRequestHandler<
>({
async requestAsync(integration, input) {
const start = performance.now();
const config = timePeriodConfig[input.timePeriod] ?? { type: "1m", perPage: 60, cacheSeconds: 60 };
const config = timePeriodConfig[input.timePeriod] ?? { type: "1m", perPage: 60 };
const instance = await createIntegrationAsync(integration);
const systemStats = await instance.getSystemStatsAsync(input.systemId, config.type, config.perPage);
const containerStats = input.includeDocker
@@ -147,13 +147,8 @@ export const beszelStatsRequestHandler = createCachedIntegrationRequestHandler<
});
return { systemStats, containerStats };
},
cacheDuration: dayjs.duration(60, "seconds"),
cacheDuration: dayjs.duration(1, "second"),
fallbackToStaleOnError: true,
retry: { attempts: 2, delayMs: 500 },
queryKey: "beszelStats",
cacheDurationForInput(input) {
const config = timePeriodConfig[input.timePeriod];
if (!config) return undefined;
return dayjs.duration(config.cacheSeconds, "seconds");
},
});
+7 -7
View File
@@ -3374,27 +3374,27 @@
"pausedCount": "Pausiert"
},
"error": {
"internalServerError": ""
"internalServerError": "Fehler beim Abrufen der Laufzeit Daten"
}
},
"audioStats": {
"name": "Audiostatistiken",
"description": "",
"description": "Bibliotheks- und Wiedergabestatistiken von Navidrome oder Audiobookshelf",
"option": {
"showArtists": {
"label": "Künstler anzeigen",
"description": ""
"description": "Zeige die Gesamtzahl der Künstler in deiner Musikbibliothek (Navidrome)"
},
"showAlbums": {
"label": "",
"label": "Alben anzeigen",
"description": "Zeigt die Gesamtzahl der Alben in deiner Musikbibliothek an (Navidrome)"
},
"showSongs": {
"label": "Songs anzeigen",
"description": ""
"description": "Zeigt die Gesamtzahl der Titel in deiner Musikbibliothek an (Navidrome)"
},
"showNowPlaying": {
"label": "",
"label": "\"Sie hören\" anzeigen",
"description": ""
},
"showNowPlayingList": {
@@ -3720,7 +3720,7 @@
},
"chart": {
"cpu": {
"title": "CPU-Auslastung",
"title": "CPU Auslastung",
"subtitle": "",
"series": "CPU"
},
+33 -33
View File
@@ -177,7 +177,7 @@
},
"password": {
"label": "Password",
"suggestionsTitle": "",
"suggestionsTitle": "Password suggestions",
"suggestion": {
"length": "Includes a minimum of 8 characters",
"lowercase": "Includes a lower-case letter",
@@ -687,28 +687,28 @@
"customWidget": {
"page": {
"list": {
"title": "",
"noResults": "",
"loading": ""
"title": "Custom Widgets",
"noResults": "No custom widgets yet. Create your first one!",
"loading": "Loading..."
},
"create": {
"title": "",
"subtitle": ""
"title": "Create Custom Widget",
"subtitle": "Define an API URL, authentication, and how to display the response data."
},
"edit": {
"title": ""
"title": "Edit Custom Widget"
}
},
"action": {
"create": "",
"import": "",
"save": "",
"delete": "",
"deleteConfirm": "",
"export": "",
"addMapping": "",
"addColumn": "",
"edit": "",
"create": "Create",
"import": "Import",
"save": "Save",
"delete": "Delete Custom Widget",
"deleteConfirm": "Are you sure you want to delete \"{name}\"? This action cannot be undone.",
"export": "Export",
"addMapping": "Add Mapping",
"addColumn": "Add Column",
"edit": "Edit",
"removeMapping": "",
"removeColumn": "",
"addItem": "",
@@ -5599,71 +5599,71 @@
"label": "",
"children": {
"detail": {
"title": ""
"title": "Select your colour scheme"
}
}
},
"locale": {
"label": "",
"label": "Language",
"children": {
"detail": {
"title": ""
"title": "Select your preferred language"
}
}
},
"defaultSearchEngineId": {
"label": "",
"label": "Default search engine",
"children": {
"detail": {
"title": ""
"title": "Select your default search engine"
}
}
},
"openSearchInNewTab": {
"label": "",
"label": "Open search results in new tab",
"children": {
"detail": {
"title": ""
"title": "Open search results in new tab"
}
}
},
"ddgBangs": {
"label": "",
"label": "Enable DuckDuckGo bangs",
"children": {
"detail": {
"title": ""
"title": "Enable DuckDuckGo bangs"
}
}
},
"firstDayOfWeek": {
"label": "",
"label": "First day of the week",
"children": {
"detail": {
"title": ""
"title": "Select the first day of the week"
}
}
},
"homeBoardId": {
"label": "",
"label": "Home board",
"children": {
"detail": {
"title": ""
"title": "Select your home board"
}
}
},
"mobileHomeBoardId": {
"label": "",
"label": "Mobile home board",
"children": {
"detail": {
"title": ""
"title": "Select your mobile home board"
}
}
},
"pingIconsEnabled": {
"label": "",
"label": "Use icons for pings",
"children": {
"detail": {
"title": ""
"title": "Use icons for pings"
}
}
},
+4 -1
View File
@@ -1412,7 +1412,10 @@
"here": "here",
"iconPicker": {
"label": "Icon URL",
"header": "Type name or objects to filter for icons... Homarr will search through {countIcons} icons for you."
"header": "Type name or objects to filter for icons... Homarr will search through {countIcons} icons for you.",
"headerLoading": "Type to search icons…",
"noResults": "No icons match \"{search}\".",
"error": "Couldn't load icons."
},
"colorScheme": {
"options": {
+2 -2
View File
@@ -4943,8 +4943,8 @@
"search": "Cerca {count} container",
"selected": "{selectCount} di {totalCount} containers selezionati",
"footer": "Totale container {count}",
"totalCpu": "",
"totalMemory": ""
"totalCpu": "CPU Totale: {cpu}%",
"totalMemory": "Mem Totale: {memory}"
},
"field": {
"name": {
File diff suppressed because it is too large. Load diff
+12 -12
View File
@@ -687,7 +687,7 @@
"customWidget": {
"page": {
"list": {
"title": "",
"title": "Власні віджети",
"noResults": "",
"loading": ""
},
@@ -1021,7 +1021,7 @@
"mediaMonitoring": "",
"speedtest": "",
"analytics": "",
"mediaLibrary": "",
"mediaLibrary": "Медіа бібліотека",
"uptimeMonitoring": ""
},
"testConnection": {
@@ -1139,7 +1139,7 @@
"description": {
"connection": {
"hostUnreachable": "",
"networkUnreachable": "",
"networkUnreachable": "Мережа недоступна. Перевірте підключення до Інтернету або налаштування мережі.",
"refused": "",
"reset": ""
},
@@ -1328,7 +1328,7 @@
}
},
"paperlessNgx": {
"name": "",
"name": "Безпаперовий ngx",
"description": ""
}
},
@@ -1679,9 +1679,9 @@
"label": "",
"option": {
"row": "",
"row-reverse": "",
"row-reverse": "Горизонтально (зворотнє)",
"column": "",
"column-reverse": ""
"column-reverse": "Вертикальна (перевернута)"
}
},
"descriptionDisplayMode": {
@@ -1690,7 +1690,7 @@
"option": {
"normal": "",
"tooltip": "",
"hidden": ""
"hidden": "Приховати"
}
}
},
@@ -1703,7 +1703,7 @@
},
"bookmarks": {
"name": "Закладки",
"description": "",
"description": "Показує декілька посилань додатка",
"option": {
"title": {
"label": "Назва"
@@ -1747,8 +1747,8 @@
}
},
"archiveTeamWarrior": {
"name": "",
"description": "",
"name": "АрхівTeam Warrior",
"description": "Слідкуйте за станом, проєктами та активними архівними завданнями ArchiveTeam Warrior.",
"option": {
"showBroadcastMessage": {
"label": ""
@@ -1778,7 +1778,7 @@
}
},
"error": {
"internalServerError": "",
"internalServerError": "Не вдалося отримати резюме DNS Hole",
"integrationsDisconnected": "Немає даних, всі інтеграції відключені"
},
"data": {
@@ -1808,7 +1808,7 @@
}
},
"showToggleAllButtons": {
"label": ""
"label": "Показати всі кнопки"
}
},
"error": {
@@ -46,8 +46,9 @@ export default function BeszelAlertsWidget({
error: alertsError,
isPending,
} = clientApi.widget.beszel.getAlerts.useQuery(alertsInput, {
staleTime: 15_000,
refetchInterval: 15_000,
staleTime: 10_000,
gcTime: 48 * 60 * 60 * 1000,
refetchInterval: 10_000,
retry: false,
});
const utils = clientApi.useUtils();
@@ -371,7 +371,7 @@ export default function BeszelSystemGridWidget({
isPending,
} = clientApi.widget.beszel.getSystems.useQuery(
{ integrationIds },
{ staleTime: 5_000, refetchInterval: 5_000, retry: false },
{ staleTime: 10_000, gcTime: 48 * 60 * 60 * 1000, refetchInterval: 10_000, retry: false },
);
useBeszelSystemsSubscription(integrationIds, !isEditMode);
@@ -67,7 +67,10 @@ export default function BeszelSystemStatsWidget({
data: systemsResult = [],
isPending: systemsPending,
error: systemsError,
} = clientApi.widget.beszel.getSystems.useQuery({ integrationIds }, { staleTime: 30 * 1000, retry: false });
} = clientApi.widget.beszel.getSystems.useQuery(
{ integrationIds },
{ staleTime: 10_000, gcTime: 48 * 60 * 60 * 1000, retry: false },
);
const systems = useMemo(
() => systemsResult.flatMap((r) => r.systems.map((s) => ({ value: s.id, label: s.name }))),
@@ -100,8 +103,9 @@ export default function BeszelSystemStatsWidget({
includeDocker: showDocker,
},
{
staleTime: 30 * 1000,
refetchInterval: isLive ? false : 5_000,
staleTime: 10_000,
gcTime: 48 * 60 * 60 * 1000,
refetchInterval: isLive ? false : 10_000,
enabled: !isLive && systemReady && selectedSystem !== "",
retry: false,
},
@@ -76,7 +76,7 @@ export default function BeszelSystemTableWidget({
isPending,
} = clientApi.widget.beszel.getSystems.useQuery(
{ integrationIds },
{ staleTime: 5_000, refetchInterval: 5_000, retry: false },
{ staleTime: 10_000, gcTime: 48 * 60 * 60 * 1000, refetchInterval: 10_000, retry: false },
);
const size = getSizeConfig(width);
@@ -43,7 +43,7 @@ export const BeszelSystemStatsModal = createModal<BeszelSystemStatsModalProps>((
timePeriod,
includeDocker: false,
},
{ staleTime: 30_000 },
{ staleTime: 10_000, gcTime: 48 * 60 * 60 * 1000, refetchInterval: 10_000 },
);
const mappers = useMemo(