Compare commits

..
Author SHA1 Message Date
ajnart e18a040686 style: auto-fix lint and format 2026-07-15 09:31:02 +00:00
ajnart 789e71096e fix(widgets): add per-widget refetchIntervalSeconds to WidgetDefinition
The v1.69.0 caching refactor replaced Redis pub/sub push with a global
30s client-side refetchInterval. This caused fast-refresh widgets like
health-monitoring (Dashdot, 5s) to only update every 30s.

Adds refetchIntervalSeconds field to WidgetDefinition. Each widget
declares its own refresh rate alongside its other config. The TRPC
provider iterates all widget definitions and calls setQueryDefaults
with the widget's queryKey (or auto-derived from widget kind).

Widgets whose kind doesn't match their tRPC path set queryKey
explicitly (e.g. dnsHoleSummary -> widget.dnsHole, systemResources ->
widget.healthMonitoring). Beszel already does this for cache sharing.

Removed inline refetchInterval from docker widget component since it's
now on the definition.

Fixes #6268
2026-07-15 11:29:24 +02:00
75 changed files with 59 additions and 90 deletions

No files matched your search

@@ -2,6 +2,7 @@
import type { PropsWithChildren } from "react"; import type { PropsWithChildren } from "react";
import { useState } from "react"; import { useState } from "react";
import type { QueryKey } from "@tanstack/react-query";
import { QueryClient } from "@tanstack/react-query"; import { QueryClient } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental"; import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental";
@@ -29,11 +30,11 @@ import {
queryCacheDefaultGcTimeMs, queryCacheDefaultGcTimeMs,
queryCacheDefaultRefetchIntervalMs, queryCacheDefaultRefetchIntervalMs,
queryCacheDefaultStaleTimeMs, queryCacheDefaultStaleTimeMs,
widgetRefetchIntervalOverrides,
} from "@homarr/api/query-cache"; } from "@homarr/api/query-cache";
import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared"; import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared";
import { env } from "@homarr/common/env"; import { env } from "@homarr/common/env";
import { showWarningNotification } from "@homarr/notifications"; import { showWarningNotification } from "@homarr/notifications";
import { widgetImports } from "@homarr/widgets";
import { createWidgetQueryPersister } from "./query-cache-persister"; import { createWidgetQueryPersister } from "./query-cache-persister";
@@ -91,8 +92,11 @@ export function TRPCReactProvider(props: PropsWithChildren) {
}, },
}); });
client.setQueryDefaults([["widget"]], { refetchInterval: queryCacheDefaultRefetchIntervalMs }); client.setQueryDefaults([["widget"]], { refetchInterval: queryCacheDefaultRefetchIntervalMs });
for (const [path, interval] of Object.entries(widgetRefetchIntervalOverrides)) { for (const { definition } of Object.values(widgetImports)) {
client.setQueryDefaults([path.split(".")], { refetchInterval: interval }); const def = definition as { refetchIntervalSeconds?: number; queryKey?: QueryKey; kind: string };
if (!def.refetchIntervalSeconds) continue;
const key = def.queryKey ?? [["widget", def.kind]];
client.setQueryDefaults(key, { refetchInterval: def.refetchIntervalSeconds * 1000 });
} }
return client; return client;
}); });
-38
View File
@@ -7,44 +7,6 @@ export const queryCacheMaxValueBytes = 1024 * 1024;
export const queryCacheStoragePrefix = "homarr-widget-query"; export const queryCacheStoragePrefix = "homarr-widget-query";
export const queryCacheBuster = "v2-superjson"; export const queryCacheBuster = "v2-superjson";
export const widgetRefetchIntervalOverrides: Record<string, number> = {
"widget.healthMonitoring": 5_000,
"widget.dnsHole": 5_000,
"widget.downloads": 5_000,
"widget.mediaServer": 5_000,
"widget.tracearr": 5_000,
"widget.firewall.getFirewallCpuStatus": 5_000,
"widget.firewall.getFirewallMemoryStatus": 15_000,
"widget.firewall.getFirewallInterfacesStatus": 30_000,
"widget.firewall.getFirewallVersionStatus": 3_600_000,
"widget.anchorNotes": 15_000,
"widget.coolify": 30_000,
"widget.umami.getActiveVisitors": 30_000,
"widget.umami": 300_000,
"widget.calendar": 60_000,
"widget.mediaRequests": 60_000,
"widget.smartHome": 60_000,
"widget.ups": 60_000,
"widget.uptimeKuma": 60_000,
"widget.weather": 60_000,
"widget.timetable": 60_000,
"widget.indexerManager": 300_000,
"widget.mediaRelease": 300_000,
"widget.mediaTranscoding": 300_000,
"widget.networkController": 300_000,
"widget.notifications": 300_000,
"widget.rssFeed": 300_000,
"widget.stockPrice": 300_000,
"widget.minecraft": 300_000,
"widget.vpn": 300_000,
"widget.archiveTeamWarrior": 300_000,
"widget.audiobookshelf": 600_000,
"widget.navidrome": 600_000,
"widget.speedtestTracker": 600_000,
"widget.immich": 900_000,
"widget.paperlessNgx": 900_000,
};
let activeQueryCacheBoardId: string | null = null; let activeQueryCacheBoardId: string | null = null;
export const setActiveQueryCacheBoardId = (boardId: string | null) => { export const setActiveQueryCacheBoardId = (boardId: string | null) => {
@@ -12,7 +12,6 @@ export const anchorNotesListRequestHandler = createIntegrationRequestHandler<
const instance = await createIntegrationAsync(integration); const instance = await createIntegrationAsync(integration);
return instance.listNotesAsync(input); return instance.listNotesAsync(input);
}, },
cacheTtlMs: 15_000,
}); });
export const anchorNoteRequestHandler = createIntegrationRequestHandler<AnchorNote, "anchor", { noteId: string }>({ export const anchorNoteRequestHandler = createIntegrationRequestHandler<AnchorNote, "anchor", { noteId: string }>({
@@ -20,5 +19,4 @@ export const anchorNoteRequestHandler = createIntegrationRequestHandler<AnchorNo
const instance = await createIntegrationAsync(integration); const instance = await createIntegrationAsync(integration);
return instance.getNoteAsync(input.noteId); return instance.getNoteAsync(input.noteId);
}, },
cacheTtlMs: 15_000,
}); });
@@ -13,5 +13,4 @@ export const archiveTeamWarriorRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getStatusAsync(); return await integrationInstance.getStatusAsync();
}, },
cacheTtlMs: 300_000,
}); });
@@ -12,5 +12,4 @@ export const audiobookshelfRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getDashboardDataAsync(); return await integrationInstance.getDashboardDataAsync();
}, },
cacheTtlMs: 600_000,
}); });
-1
View File
@@ -23,5 +23,4 @@ export const calendarMonthRequestHandler = createIntegrationRequestHandler<
input.showUnmonitored, input.showUnmonitored,
); );
}, },
cacheTtlMs: 60_000,
}); });
-1
View File
@@ -12,5 +12,4 @@ export const coolifyRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getInstanceInfoAsync(); return await integrationInstance.getInstanceInfoAsync();
}, },
cacheTtlMs: 30_000,
}); });
-1
View File
@@ -13,5 +13,4 @@ export const dnsHoleRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getSummaryAsync(); return await integrationInstance.getSummaryAsync();
}, },
cacheTtlMs: 5_000,
}); });
-1
View File
@@ -177,7 +177,6 @@ export const dockerContainersRequestHandler = createWidgetRequestHandler({
} }
return await getContainersWithStatsAsync(); return await getContainersWithStatsAsync();
}, },
cacheTtlMs: 20_000,
}); });
const extractImage = (container: ContainerInfo) => extractContainerImageName(container.Image); const extractImage = (container: ContainerInfo) => extractContainerImageName(container.Image);
@@ -13,5 +13,4 @@ export const downloadClientRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getClientJobsAndStatusAsync(input); return await integrationInstance.getClientJobsAndStatusAsync(input);
}, },
cacheTtlMs: 5_000,
}); });
@@ -69,5 +69,4 @@ export const duckDuckGoBangsRequestHandler = createRequestHandler({
return normalized; return normalized;
}, },
cacheTtlMs: 86_400_000,
}); });
-4
View File
@@ -18,7 +18,6 @@ export const firewallCpuRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return integrationInstance.getFirewallCpuAsync(); return integrationInstance.getFirewallCpuAsync();
}, },
cacheTtlMs: 5_000,
}); });
export const firewallMemoryRequestHandler = createIntegrationRequestHandler< export const firewallMemoryRequestHandler = createIntegrationRequestHandler<
@@ -30,7 +29,6 @@ export const firewallMemoryRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getFirewallMemoryAsync(); return await integrationInstance.getFirewallMemoryAsync();
}, },
cacheTtlMs: 15_000,
}); });
export const firewallInterfacesRequestHandler = createIntegrationRequestHandler< export const firewallInterfacesRequestHandler = createIntegrationRequestHandler<
@@ -42,7 +40,6 @@ export const firewallInterfacesRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getFirewallInterfacesAsync(); return await integrationInstance.getFirewallInterfacesAsync();
}, },
cacheTtlMs: 30_000,
}); });
export const firewallVersionRequestHandler = createIntegrationRequestHandler< export const firewallVersionRequestHandler = createIntegrationRequestHandler<
@@ -54,5 +51,4 @@ export const firewallVersionRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getFirewallVersionAsync(); return await integrationInstance.getFirewallVersionAsync();
}, },
cacheTtlMs: 3_600_000,
}); });
@@ -13,7 +13,6 @@ export const systemInfoRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getSystemInfoAsync(); return await integrationInstance.getSystemInfoAsync();
}, },
cacheTtlMs: 5_000,
}); });
export const clusterInfoRequestHandler = createIntegrationRequestHandler< export const clusterInfoRequestHandler = createIntegrationRequestHandler<
@@ -25,5 +24,4 @@ export const clusterInfoRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getClusterInfoAsync(); return await integrationInstance.getClusterInfoAsync();
}, },
cacheTtlMs: 5_000,
}); });
-3
View File
@@ -13,7 +13,6 @@ export const immichStatsRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getServerStatsAsync(); return await integrationInstance.getServerStatsAsync();
}, },
cacheTtlMs: 900_000,
}); });
export const immichAlbumsRequestHandler = createIntegrationRequestHandler< export const immichAlbumsRequestHandler = createIntegrationRequestHandler<
@@ -29,7 +28,6 @@ export const immichAlbumsRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getAlbumsAsync(); return await integrationInstance.getAlbumsAsync();
}, },
cacheTtlMs: 900_000,
}); });
export const immichAlbumRequestHandler = createIntegrationRequestHandler< export const immichAlbumRequestHandler = createIntegrationRequestHandler<
@@ -41,5 +39,4 @@ export const immichAlbumRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getAlbumAsync(input.albumId); return await integrationInstance.getAlbumAsync(input.albumId);
}, },
cacheTtlMs: 900_000,
}); });
@@ -13,5 +13,4 @@ export const indexerManagerRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getIndexersAsync(); return await integrationInstance.getIndexersAsync();
}, },
cacheTtlMs: 300_000,
}); });
@@ -2,7 +2,6 @@ import { createRequestHandler } from "./request-handler";
interface Options<TData, TInput extends Record<string, unknown>> { interface Options<TData, TInput extends Record<string, unknown>> {
requestAsync: (input: TInput) => Promise<TData>; requestAsync: (input: TInput) => Promise<TData>;
cacheTtlMs?: number;
} }
export const createWidgetRequestHandler = <TData, TInput extends Record<string, unknown>>( export const createWidgetRequestHandler = <TData, TInput extends Record<string, unknown>>(
@@ -11,6 +10,5 @@ export const createWidgetRequestHandler = <TData, TInput extends Record<string,
handler: (widgetOptions: TInput) => handler: (widgetOptions: TInput) =>
createRequestHandler<TData, TInput>({ createRequestHandler<TData, TInput>({
requestAsync: options.requestAsync, requestAsync: options.requestAsync,
cacheTtlMs: options.cacheTtlMs,
}).handler(widgetOptions), }).handler(widgetOptions),
}); });
@@ -13,5 +13,4 @@ export const mediaReleaseRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getMediaReleasesAsync(); return await integrationInstance.getMediaReleasesAsync();
}, },
cacheTtlMs: 300_000,
}); });
@@ -16,5 +16,4 @@ export const mediaRequestStatsRequestHandler = createIntegrationRequestHandler<
users: await integrationInstance.getUsersAsync(), users: await integrationInstance.getUsersAsync(),
}; };
}, },
cacheTtlMs: 60_000,
}); });
@@ -15,5 +15,4 @@ export const mediaServerRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getCurrentSessionsAsync({ showOnlyPlaying: input.showOnlyPlaying }); return await integrationInstance.getCurrentSessionsAsync({ showOnlyPlaying: input.showOnlyPlaying });
}, },
cacheTtlMs: 5_000,
}); });
@@ -17,7 +17,6 @@ export const mediaTranscodingRequestHandler = createIntegrationRequestHandler<
statistics: await integrationInstance.getStatisticsAsync(), statistics: await integrationInstance.getStatisticsAsync(),
}; };
}, },
cacheTtlMs: 300_000,
}); });
export interface MediaTranscoding { export interface MediaTranscoding {
@@ -14,7 +14,6 @@ export const minecraftServerStatusRequestHandler = createWidgetRequestHandler({
); );
return responseSchema.parse(await response.json()); return responseSchema.parse(await response.json());
}, },
cacheTtlMs: 300_000,
}); });
const responseSchema = z const responseSchema = z
@@ -12,5 +12,4 @@ export const navidromeRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getDashboardDataAsync(); return await integrationInstance.getDashboardDataAsync();
}, },
cacheTtlMs: 600_000,
}); });
@@ -13,5 +13,4 @@ export const networkControllerRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getNetworkSummaryAsync(); return await integrationInstance.getNetworkSummaryAsync();
}, },
cacheTtlMs: 300_000,
}); });
@@ -13,5 +13,4 @@ export const notificationsRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getNotificationsAsync(); return await integrationInstance.getNotificationsAsync();
}, },
cacheTtlMs: 300_000,
}); });
@@ -12,5 +12,4 @@ export const paperlessNgxStatsRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getStatsAsync(); return await integrationInstance.getStatsAsync();
}, },
cacheTtlMs: 900_000,
}); });
@@ -28,7 +28,6 @@ export const rssFeedsRequestHandler = createWidgetRequestHandler({
entries: result.entries?.map((entry) => ({ ...entry, feedUrl: input.url })).slice(0, input.count) ?? [], entries: result.entries?.map((entry) => ({ ...entry, feedUrl: input.url })).slice(0, input.count) ?? [],
}; };
}, },
cacheTtlMs: 300_000,
}); });
const attemptGetImageFromEntry = (feedUrl: string, entry: object) => { const attemptGetImageFromEntry = (feedUrl: string, entry: object) => {
@@ -18,5 +18,4 @@ export const smartHomeEntityStateRequestHandler = createIntegrationRequestHandle
return result.data.state; return result.data.state;
}, },
cacheTtlMs: 60_000,
}); });
@@ -12,5 +12,4 @@ export const speedtestTrackerRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getDashboardDataAsync(); return await integrationInstance.getDashboardDataAsync();
}, },
cacheTtlMs: 600_000,
}); });
@@ -39,7 +39,6 @@ export const fetchStockPriceHandler = createWidgetRequestHandler({
shortName: firstResult.meta.shortName, shortName: firstResult.meta.shortName,
}; };
}, },
cacheTtlMs: 300_000,
}); });
const dataSchema = z const dataSchema = z
-1
View File
@@ -12,5 +12,4 @@ export const tracearrRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getDashboardDataAsync(); return await integrationInstance.getDashboardDataAsync();
}, },
cacheTtlMs: 5_000,
}); });
-6
View File
@@ -12,7 +12,6 @@ export const umamiRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getVisitorStatsAsync(input.websiteId, input.timeFrame, input.eventName); return await integrationInstance.getVisitorStatsAsync(input.websiteId, input.timeFrame, input.eventName);
}, },
cacheTtlMs: 300_000,
}); });
export const umamiEventNamesRequestHandler = createIntegrationRequestHandler<string[], "umami", { websiteId: string }>({ export const umamiEventNamesRequestHandler = createIntegrationRequestHandler<string[], "umami", { websiteId: string }>({
@@ -20,7 +19,6 @@ export const umamiEventNamesRequestHandler = createIntegrationRequestHandler<str
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getEventNamesAsync(input.websiteId); return await integrationInstance.getEventNamesAsync(input.websiteId);
}, },
cacheTtlMs: 300_000,
}); });
export const umamiTopPagesRequestHandler = createIntegrationRequestHandler< export const umamiTopPagesRequestHandler = createIntegrationRequestHandler<
@@ -32,7 +30,6 @@ export const umamiTopPagesRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getTopPagesAsync(input.websiteId, input.timeFrame, input.limit); return await integrationInstance.getTopPagesAsync(input.websiteId, input.timeFrame, input.limit);
}, },
cacheTtlMs: 300_000,
}); });
export const umamiTopReferrersRequestHandler = createIntegrationRequestHandler< export const umamiTopReferrersRequestHandler = createIntegrationRequestHandler<
@@ -44,7 +41,6 @@ export const umamiTopReferrersRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getTopReferrersAsync(input.websiteId, input.timeFrame, input.limit); return await integrationInstance.getTopReferrersAsync(input.websiteId, input.timeFrame, input.limit);
}, },
cacheTtlMs: 300_000,
}); });
export const umamiMultiEventRequestHandler = createIntegrationRequestHandler< export const umamiMultiEventRequestHandler = createIntegrationRequestHandler<
@@ -56,7 +52,6 @@ export const umamiMultiEventRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getMultiEventTimeSeriesAsync(input.websiteId, input.timeFrame, input.eventNames); return await integrationInstance.getMultiEventTimeSeriesAsync(input.websiteId, input.timeFrame, input.eventNames);
}, },
cacheTtlMs: 300_000,
}); });
export const umamiActiveVisitorsRequestHandler = createIntegrationRequestHandler< export const umamiActiveVisitorsRequestHandler = createIntegrationRequestHandler<
@@ -68,5 +63,4 @@ export const umamiActiveVisitorsRequestHandler = createIntegrationRequestHandler
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getActiveVisitorsAsync(input.websiteId); return await integrationInstance.getActiveVisitorsAsync(input.websiteId);
}, },
cacheTtlMs: 30_000,
}); });
-1
View File
@@ -13,5 +13,4 @@ export const upsSummariesRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getUpsSummariesAsync(); return await integrationInstance.getUpsSummariesAsync();
}, },
cacheTtlMs: 60_000,
}); });
@@ -12,5 +12,4 @@ export const uptimeKumaRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getDashboardDataAsync(); return await integrationInstance.getDashboardDataAsync();
}, },
cacheTtlMs: 60_000,
}); });
-1
View File
@@ -25,5 +25,4 @@ export const vpnSummaryHandler = createIntegrationRequestHandler<
return null; return null;
} }
}, },
cacheTtlMs: 300_000,
}); });
-1
View File
@@ -31,7 +31,6 @@ export const weatherRequestHandler = createWidgetRequestHandler({
}), }),
} satisfies Weather; } satisfies Weather;
}, },
cacheTtlMs: 60_000,
}); });
const atLocationOutput = z.object({ const atLocationOutput = z.object({
@@ -5,6 +5,8 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("anchorNote", { export const { definition, componentLoader } = createWidgetDefinition("anchorNote", {
icon: IconNotes, icon: IconNotes,
queryKey: [["widget", "anchorNotes"]],
refetchIntervalSeconds: 15,
supportedIntegrations: ["anchor"], supportedIntegrations: ["anchor"],
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
@@ -5,6 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("archiveTeamWarrior", { export const { definition, componentLoader } = createWidgetDefinition("archiveTeamWarrior", {
icon: IconArchive, icon: IconArchive,
refetchIntervalSeconds: 300,
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
showBroadcastMessage: factory.switch({ showBroadcastMessage: factory.switch({
@@ -14,6 +14,7 @@ const hideUnlessAudiobookshelf = {
export const { definition, componentLoader } = createWidgetDefinition("audioStats", { export const { definition, componentLoader } = createWidgetDefinition("audioStats", {
icon: IconHeadphones, icon: IconHeadphones,
refetchIntervalSeconds: 600,
supportedIntegrations: ["navidrome", "audiobookshelf"], supportedIntegrations: ["navidrome", "audiobookshelf"],
integrationsRequired: true, integrationsRequired: true,
maxIntegrations: 1, maxIntegrations: 1,
+1
View File
@@ -9,6 +9,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("calendar", { export const { definition, componentLoader } = createWidgetDefinition("calendar", {
icon: IconCalendar, icon: IconCalendar,
refetchIntervalSeconds: 60,
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
releaseType: factory.multiSelect({ releaseType: factory.multiSelect({
+1
View File
@@ -5,6 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("coolify", { export const { definition, componentLoader } = createWidgetDefinition("coolify", {
icon: IconCloud, icon: IconCloud,
refetchIntervalSeconds: 30,
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
showServers: factory.switch({ showServers: factory.switch({
+1
View File
@@ -70,6 +70,7 @@ export const createWidgetDefinition = <TKind extends WidgetKind, TDefinition ext
export interface WidgetDefinition { export interface WidgetDefinition {
icon: TablerIcon; icon: TablerIcon;
queryKey?: QueryKey; queryKey?: QueryKey;
refetchIntervalSeconds?: number;
supportedIntegrations?: IntegrationKind[]; supportedIntegrations?: IntegrationKind[];
integrationsRequired?: boolean; integrationsRequired?: boolean;
maxIntegrations?: number; maxIntegrations?: number;
@@ -9,6 +9,8 @@ export const widgetKind = "dnsHoleControls";
export const { definition, componentLoader } = createWidgetDefinition(widgetKind, { export const { definition, componentLoader } = createWidgetDefinition(widgetKind, {
icon: IconDeviceGamepad, icon: IconDeviceGamepad,
queryKey: [["widget", "dnsHole"]],
refetchIntervalSeconds: 5,
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
showToggleAllButtons: factory.switch({ showToggleAllButtons: factory.switch({
@@ -9,6 +9,8 @@ export const widgetKind = "dnsHoleSummary";
export const { definition, componentLoader } = createWidgetDefinition(widgetKind, { export const { definition, componentLoader } = createWidgetDefinition(widgetKind, {
icon: IconAd, icon: IconAd,
queryKey: [["widget", "dnsHole"]],
refetchIntervalSeconds: 5,
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
usePiHoleColors: factory.switch({ usePiHoleColors: factory.switch({
+1 -3
View File
@@ -184,9 +184,7 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo
const t = useScopedI18n("docker"); const t = useScopedI18n("docker");
const isTiny = width <= 256; const isTiny = width <= 256;
const { data, refetch, isFetching } = clientApi.docker.getContainers.useQuery(undefined, { const { data, refetch, isFetching } = clientApi.docker.getContainers.useQuery(undefined, {});
refetchInterval: 20_000,
});
const containers = data?.containers ?? []; const containers = data?.containers ?? [];
const timestamp = useMemo(() => data?.timestamp ?? new Date(), [data?.timestamp]); const timestamp = useMemo(() => data?.timestamp ?? new Date(), [data?.timestamp]);
const relativeTime = useTimeAgo(timestamp); const relativeTime = useTimeAgo(timestamp);
+1
View File
@@ -26,6 +26,7 @@ const columnTranslationKeyMap = {
export const { definition, componentLoader } = createWidgetDefinition("dockerContainers", { export const { definition, componentLoader } = createWidgetDefinition("dockerContainers", {
icon: IconBrandDocker, icon: IconBrandDocker,
queryKey: [["docker", "getContainers"]], queryKey: [["docker", "getContainers"]],
refetchIntervalSeconds: 20,
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
columns: factory.multiSelect({ columns: factory.multiSelect({
+1
View File
@@ -33,6 +33,7 @@ const columnsSort = columnsList.filter((column) =>
export const { definition, componentLoader } = createWidgetDefinition("downloads", { export const { definition, componentLoader } = createWidgetDefinition("downloads", {
icon: IconDownload, icon: IconDownload,
refetchIntervalSeconds: 5,
createOptions() { createOptions() {
return optionsBuilder.from( return optionsBuilder.from(
(factory) => ({ (factory) => ({
+1
View File
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("firewall", { export const { definition, componentLoader } = createWidgetDefinition("firewall", {
icon: IconWall, icon: IconWall,
refetchIntervalSeconds: 5,
createOptions() { createOptions() {
return optionsBuilder.from(() => ({})); return optionsBuilder.from(() => ({}));
}, },
@@ -8,6 +8,7 @@ import { createStorageVolumeMultiSelectOptions } from "../storage-volume-options
export const { definition, componentLoader } = createWidgetDefinition("healthMonitoring", { export const { definition, componentLoader } = createWidgetDefinition("healthMonitoring", {
icon: IconHeartRateMonitor, icon: IconHeartRateMonitor,
refetchIntervalSeconds: 5,
createOptions() { createOptions() {
return optionsBuilder.from( return optionsBuilder.from(
(factory) => ({ (factory) => ({
@@ -8,6 +8,8 @@ import { optionsBuilder } from "../../options";
export const { definition, componentLoader } = createWidgetDefinition("immich-albumCarousel", { export const { definition, componentLoader } = createWidgetDefinition("immich-albumCarousel", {
icon: IconPhoto, icon: IconPhoto,
queryKey: [["widget", "immich"]],
refetchIntervalSeconds: 900,
supportedIntegrations: ["immich"], supportedIntegrations: ["immich"],
integrationsRequired: true, integrationsRequired: true,
createOptions() { createOptions() {
@@ -5,6 +5,8 @@ import { optionsBuilder } from "../../options";
export const { definition, componentLoader } = createWidgetDefinition("immich-serverStats", { export const { definition, componentLoader } = createWidgetDefinition("immich-serverStats", {
icon: IconGraphFilled, icon: IconGraphFilled,
queryKey: [["widget", "immich"]],
refetchIntervalSeconds: 900,
supportedIntegrations: ["immich"], supportedIntegrations: ["immich"],
integrationsRequired: true, integrationsRequired: true,
createOptions() { createOptions() {
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("indexerManager", { export const { definition, componentLoader } = createWidgetDefinition("indexerManager", {
icon: IconReportSearch, icon: IconReportSearch,
refetchIntervalSeconds: 300,
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
openIndexerSiteInNewTab: factory.switch({ openIndexerSiteInNewTab: factory.switch({
@@ -5,6 +5,8 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("mediaReleases", { export const { definition, componentLoader } = createWidgetDefinition("mediaReleases", {
icon: IconTicket, icon: IconTicket,
queryKey: [["widget", "mediaRelease"]],
refetchIntervalSeconds: 300,
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
layout: factory.select({ layout: factory.select({
@@ -8,6 +8,8 @@ import { optionsBuilder } from "../../options";
export const { componentLoader, definition } = createWidgetDefinition("mediaRequests-requestList", { export const { componentLoader, definition } = createWidgetDefinition("mediaRequests-requestList", {
icon: IconZoomQuestion, icon: IconZoomQuestion,
queryKey: [["widget", "mediaRequests"]],
refetchIntervalSeconds: 60,
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
linksTargetNewTab: factory.switch({ linksTargetNewTab: factory.switch({
@@ -7,6 +7,8 @@ import { createWidgetDefinition } from "../../definition";
export const { componentLoader, definition } = createWidgetDefinition("mediaRequests-requestStats", { export const { componentLoader, definition } = createWidgetDefinition("mediaRequests-requestStats", {
icon: IconChartBar, icon: IconChartBar,
queryKey: [["widget", "mediaRequests"]],
refetchIntervalSeconds: 60,
createOptions() { createOptions() {
return {}; return {};
}, },
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../options";
export const { componentLoader, definition } = createWidgetDefinition("mediaServer", { export const { componentLoader, definition } = createWidgetDefinition("mediaServer", {
icon: IconVideo, icon: IconVideo,
refetchIntervalSeconds: 5,
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
showOnlyPlaying: factory.switch({ defaultValue: true, withDescription: true }), showOnlyPlaying: factory.switch({ defaultValue: true, withDescription: true }),
@@ -11,6 +11,7 @@ export const views = ["workers", "queue", "statistics"] as const;
export const { componentLoader, definition } = createWidgetDefinition("mediaTranscoding", { export const { componentLoader, definition } = createWidgetDefinition("mediaTranscoding", {
icon: IconTransform, icon: IconTransform,
refetchIntervalSeconds: 300,
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
defaultView: factory.select({ defaultView: factory.select({
@@ -6,6 +6,7 @@ import { optionsBuilder } from "../../options";
export const { componentLoader, definition } = createWidgetDefinition("minecraftServerStatus", { export const { componentLoader, definition } = createWidgetDefinition("minecraftServerStatus", {
icon: IconBrandMinecraft, icon: IconBrandMinecraft,
refetchIntervalSeconds: 300,
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
title: factory.text({ defaultValue: "" }), title: factory.text({ defaultValue: "" }),
@@ -7,6 +7,8 @@ import { optionsBuilder } from "../../options";
export const { definition, componentLoader } = createWidgetDefinition("networkControllerStatus", { export const { definition, componentLoader } = createWidgetDefinition("networkControllerStatus", {
icon: IconTopologyFull, icon: IconTopologyFull,
queryKey: [["widget", "networkController"]],
refetchIntervalSeconds: 300,
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
content: factory.select({ content: factory.select({
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../../options";
export const { definition, componentLoader } = createWidgetDefinition("networkControllerSummary", { export const { definition, componentLoader } = createWidgetDefinition("networkControllerSummary", {
icon: IconTopologyFull, icon: IconTopologyFull,
refetchIntervalSeconds: 300,
createOptions() { createOptions() {
return optionsBuilder.from(() => ({})); return optionsBuilder.from(() => ({}));
}, },
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../options";
export const { componentLoader, definition } = createWidgetDefinition("notifications", { export const { componentLoader, definition } = createWidgetDefinition("notifications", {
icon: IconMessage, icon: IconMessage,
refetchIntervalSeconds: 300,
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
hideLogos: factory.switch({ defaultValue: false }), hideLogos: factory.switch({ defaultValue: false }),
@@ -5,6 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("paperlessNgx", { export const { definition, componentLoader } = createWidgetDefinition("paperlessNgx", {
icon: IconFileText, icon: IconFileText,
refetchIntervalSeconds: 900,
supportedIntegrations: ["paperlessNgx"], supportedIntegrations: ["paperlessNgx"],
integrationsRequired: true, integrationsRequired: true,
createOptions() { createOptions() {
+1
View File
@@ -14,6 +14,7 @@ const relativeDateSchema = z
export const { definition, componentLoader } = createWidgetDefinition("releases", { export const { definition, componentLoader } = createWidgetDefinition("releases", {
icon: IconRocket, icon: IconRocket,
refetchIntervalSeconds: 300,
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
newReleaseWithin: factory.text({ newReleaseWithin: factory.text({
+1
View File
@@ -12,6 +12,7 @@ import { optionsBuilder } from "../options";
*/ */
export const { definition, componentLoader } = createWidgetDefinition("rssFeed", { export const { definition, componentLoader } = createWidgetDefinition("rssFeed", {
icon: IconRss, icon: IconRss,
refetchIntervalSeconds: 300,
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
feedUrls: factory.multiText({ feedUrls: factory.multiText({
@@ -7,6 +7,8 @@ import { optionsBuilder } from "../../options";
export const { definition, componentLoader } = createWidgetDefinition("smartHome-entityState", { export const { definition, componentLoader } = createWidgetDefinition("smartHome-entityState", {
icon: IconBinaryTree, icon: IconBinaryTree,
queryKey: [["widget", "smartHome"]],
refetchIntervalSeconds: 60,
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
entityId: factory.text({ entityId: factory.text({
@@ -5,6 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("speedtestTracker", { export const { definition, componentLoader } = createWidgetDefinition("speedtestTracker", {
icon: IconSpeedboat, icon: IconSpeedboat,
refetchIntervalSeconds: 600,
createOptions() { createOptions() {
return optionsBuilder.from( return optionsBuilder.from(
(factory) => ({ (factory) => ({
+1
View File
@@ -13,6 +13,7 @@ const timeIntervalOptions = stockPriceTimeFrames.interval;
export const { definition, componentLoader } = createWidgetDefinition("stockPrice", { export const { definition, componentLoader } = createWidgetDefinition("stockPrice", {
icon: IconBuildingBank, icon: IconBuildingBank,
refetchIntervalSeconds: 300,
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
stock: factory.text({ stock: factory.text({
@@ -6,6 +6,8 @@ import { createStorageVolumeMultiSelectOptions } from "../storage-volume-options
export const { definition, componentLoader } = createWidgetDefinition("systemDisks", { export const { definition, componentLoader } = createWidgetDefinition("systemDisks", {
icon: IconServer2, icon: IconServer2,
queryKey: [["widget", "healthMonitoring"]],
refetchIntervalSeconds: 5,
supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "synology"], supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "synology"],
createOptions() { createOptions() {
return optionsBuilder.from( return optionsBuilder.from(
@@ -14,6 +14,8 @@ const labelDisplayModeOptions = {
export const { definition, componentLoader } = createWidgetDefinition("systemResources", { export const { definition, componentLoader } = createWidgetDefinition("systemResources", {
icon: IconGraphFilled, icon: IconGraphFilled,
queryKey: [["widget", "healthMonitoring"]],
refetchIntervalSeconds: 5,
supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "glances", "synology"], supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "glances", "synology"],
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
+1
View File
@@ -8,6 +8,7 @@ import { optionsBuilder } from "../options";
export const { componentLoader, definition } = createWidgetDefinition("timetable", { export const { componentLoader, definition } = createWidgetDefinition("timetable", {
icon: IconBusStop, icon: IconBusStop,
refetchIntervalSeconds: 60,
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
baseUrl: factory.text({ baseUrl: factory.text({
+1
View File
@@ -5,6 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("tracearr", { export const { definition, componentLoader } = createWidgetDefinition("tracearr", {
icon: IconActivityHeartbeat, icon: IconActivityHeartbeat,
refetchIntervalSeconds: 5,
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
showStreams: factory.switch({ showStreams: factory.switch({
+1
View File
@@ -8,6 +8,7 @@ export const timeFrameValues = ["today", "24h", "7d", "30d", "month", "lastMonth
export const { definition, componentLoader } = createWidgetDefinition("umami", { export const { definition, componentLoader } = createWidgetDefinition("umami", {
icon: IconChartBar, icon: IconChartBar,
refetchIntervalSeconds: 300,
supportedIntegrations: ["umami"], supportedIntegrations: ["umami"],
createOptions() { createOptions() {
return optionsBuilder.from( return optionsBuilder.from(
+1
View File
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("ups", { export const { definition, componentLoader } = createWidgetDefinition("ups", {
icon: IconBatteryCharging, icon: IconBatteryCharging,
refetchIntervalSeconds: 60,
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
showBattery: factory.switch({ showBattery: factory.switch({
@@ -5,6 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("uptimeKuma", { export const { definition, componentLoader } = createWidgetDefinition("uptimeKuma", {
icon: IconHeartbeat, icon: IconHeartbeat,
refetchIntervalSeconds: 60,
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
showAverageUptime: factory.switch({ showAverageUptime: factory.switch({
+1
View File
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../options";
export const { componentLoader, definition } = createWidgetDefinition("vpn", { export const { componentLoader, definition } = createWidgetDefinition("vpn", {
icon: IconShieldLock, icon: IconShieldLock,
refetchIntervalSeconds: 300,
createOptions() { createOptions() {
return optionsBuilder.from(() => ({})); return optionsBuilder.from(() => ({}));
}, },
+1
View File
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("weather", { export const { definition, componentLoader } = createWidgetDefinition("weather", {
icon: IconCloud, icon: IconCloud,
refetchIntervalSeconds: 60,
createOptions() { createOptions() {
return optionsBuilder.from( return optionsBuilder.from(
(factory) => ({ (factory) => ({