From a0fb7553a5b4376d999e7a2ee3f5a596aea5b71e Mon Sep 17 00:00:00 2001 From: ajnart Date: Wed, 15 Jul 2026 10:59:38 +0200 Subject: [PATCH] fix(widgets): restore per-widget refetch intervals lost in v1.69.0 refactor The v1.69.0 caching refactor (f19033e0d) replaced the old Redis pub/sub push mechanism with a global 30s client-side polling interval. This caused widgets that previously refreshed every 5s (e.g. health-monitoring, Dashdot, dns-hole) to only refresh every 30s. Adds a widgetRefetchIntervalOverrides map in query-cache.ts that maps tRPC path prefixes to refetch intervals matching the pre-v1.69.0 server-side cacheDuration values. The TRPC provider applies these overrides via setQueryDefaults after the global default. Also restores server-side cacheTtlMs on all request handlers to prevent duplicate upstream API calls between client polls, and adds cacheTtlMs support to createWidgetRequestHandler. Beszel handlers are left untouched as they have their own cache handling. Fixes #6268 --- .../app/[locale]/_client-providers/trpc.tsx | 4 ++ packages/api/src/query-cache.ts | 38 +++++++++++++++++++ packages/request-handler/src/anchor-notes.ts | 2 + .../src/archive-team-warrior.ts | 1 + .../request-handler/src/audiobookshelf.ts | 1 + packages/request-handler/src/calendar.ts | 1 + packages/request-handler/src/coolify.ts | 1 + packages/request-handler/src/dns-hole.ts | 1 + packages/request-handler/src/docker.ts | 1 + packages/request-handler/src/downloads.ts | 1 + .../request-handler/src/duckduckgo-bangs.ts | 1 + packages/request-handler/src/firewall.ts | 4 ++ .../request-handler/src/health-monitoring.ts | 2 + packages/request-handler/src/immich.ts | 3 ++ .../request-handler/src/indexer-manager.ts | 1 + .../src/lib/widget-request-handler.ts | 2 + packages/request-handler/src/media-release.ts | 1 + .../src/media-request-stats.ts | 1 + packages/request-handler/src/media-server.ts | 1 + .../request-handler/src/media-transcoding.ts | 1 + .../src/minecraft-server-status.ts | 1 + packages/request-handler/src/navidrome.ts | 1 + .../request-handler/src/network-controller.ts | 1 + packages/request-handler/src/notifications.ts | 1 + packages/request-handler/src/paperless-ngx.ts | 1 + packages/request-handler/src/rss-feeds.ts | 1 + .../src/smart-home-entity-state.ts | 1 + .../request-handler/src/speedtest-tracker.ts | 1 + packages/request-handler/src/stock-price.ts | 1 + packages/request-handler/src/tracearr.ts | 1 + packages/request-handler/src/umami.ts | 6 +++ packages/request-handler/src/ups.ts | 1 + packages/request-handler/src/uptime-kuma.ts | 1 + packages/request-handler/src/vpn.ts | 1 + packages/request-handler/src/weather.ts | 1 + packages/widgets/src/docker/component.tsx | 2 +- 36 files changed, 89 insertions(+), 1 deletion(-) diff --git a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx index 7d06a8951..2fd44f926 100644 --- a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx +++ b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx @@ -29,6 +29,7 @@ import { queryCacheDefaultGcTimeMs, queryCacheDefaultRefetchIntervalMs, queryCacheDefaultStaleTimeMs, + widgetRefetchIntervalOverrides, } from "@homarr/api/query-cache"; import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared"; import { env } from "@homarr/common/env"; @@ -90,6 +91,9 @@ export function TRPCReactProvider(props: PropsWithChildren) { }, }); client.setQueryDefaults([["widget"]], { refetchInterval: queryCacheDefaultRefetchIntervalMs }); + for (const [path, interval] of Object.entries(widgetRefetchIntervalOverrides)) { + client.setQueryDefaults([path.split(".")], { refetchInterval: interval }); + } return client; }); diff --git a/packages/api/src/query-cache.ts b/packages/api/src/query-cache.ts index a8cdb9641..5870be324 100644 --- a/packages/api/src/query-cache.ts +++ b/packages/api/src/query-cache.ts @@ -7,6 +7,44 @@ export const queryCacheMaxValueBytes = 1024 * 1024; export const queryCacheStoragePrefix = "homarr-widget-query"; export const queryCacheBuster = "v2-superjson"; +export const widgetRefetchIntervalOverrides: Record = { + "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; export const setActiveQueryCacheBoardId = (boardId: string | null) => { diff --git a/packages/request-handler/src/anchor-notes.ts b/packages/request-handler/src/anchor-notes.ts index 75677b8ba..df3a1d1cc 100644 --- a/packages/request-handler/src/anchor-notes.ts +++ b/packages/request-handler/src/anchor-notes.ts @@ -12,6 +12,7 @@ export const anchorNotesListRequestHandler = createIntegrationRequestHandler< const instance = await createIntegrationAsync(integration); return instance.listNotesAsync(input); }, + cacheTtlMs: 15_000, }); export const anchorNoteRequestHandler = createIntegrationRequestHandler({ @@ -19,4 +20,5 @@ export const anchorNoteRequestHandler = createIntegrationRequestHandler extractContainerImageName(container.Image); diff --git a/packages/request-handler/src/downloads.ts b/packages/request-handler/src/downloads.ts index 12106b07a..f86f0fb3e 100644 --- a/packages/request-handler/src/downloads.ts +++ b/packages/request-handler/src/downloads.ts @@ -13,4 +13,5 @@ export const downloadClientRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getClientJobsAndStatusAsync(input); }, + cacheTtlMs: 5_000, }); diff --git a/packages/request-handler/src/duckduckgo-bangs.ts b/packages/request-handler/src/duckduckgo-bangs.ts index 628e125a1..6200567f9 100644 --- a/packages/request-handler/src/duckduckgo-bangs.ts +++ b/packages/request-handler/src/duckduckgo-bangs.ts @@ -69,4 +69,5 @@ export const duckDuckGoBangsRequestHandler = createRequestHandler({ return normalized; }, + cacheTtlMs: 86_400_000, }); diff --git a/packages/request-handler/src/firewall.ts b/packages/request-handler/src/firewall.ts index ad4f25b70..83f804aa3 100644 --- a/packages/request-handler/src/firewall.ts +++ b/packages/request-handler/src/firewall.ts @@ -18,6 +18,7 @@ export const firewallCpuRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return integrationInstance.getFirewallCpuAsync(); }, + cacheTtlMs: 5_000, }); export const firewallMemoryRequestHandler = createIntegrationRequestHandler< @@ -29,6 +30,7 @@ export const firewallMemoryRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getFirewallMemoryAsync(); }, + cacheTtlMs: 15_000, }); export const firewallInterfacesRequestHandler = createIntegrationRequestHandler< @@ -40,6 +42,7 @@ export const firewallInterfacesRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getFirewallInterfacesAsync(); }, + cacheTtlMs: 30_000, }); export const firewallVersionRequestHandler = createIntegrationRequestHandler< @@ -51,4 +54,5 @@ export const firewallVersionRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getFirewallVersionAsync(); }, + cacheTtlMs: 3_600_000, }); diff --git a/packages/request-handler/src/health-monitoring.ts b/packages/request-handler/src/health-monitoring.ts index 959d3e0ed..ebba42a9b 100644 --- a/packages/request-handler/src/health-monitoring.ts +++ b/packages/request-handler/src/health-monitoring.ts @@ -13,6 +13,7 @@ export const systemInfoRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getSystemInfoAsync(); }, + cacheTtlMs: 5_000, }); export const clusterInfoRequestHandler = createIntegrationRequestHandler< @@ -24,4 +25,5 @@ export const clusterInfoRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getClusterInfoAsync(); }, + cacheTtlMs: 5_000, }); diff --git a/packages/request-handler/src/immich.ts b/packages/request-handler/src/immich.ts index 56fc89bd0..303396fd5 100644 --- a/packages/request-handler/src/immich.ts +++ b/packages/request-handler/src/immich.ts @@ -13,6 +13,7 @@ export const immichStatsRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getServerStatsAsync(); }, + cacheTtlMs: 900_000, }); export const immichAlbumsRequestHandler = createIntegrationRequestHandler< @@ -28,6 +29,7 @@ export const immichAlbumsRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getAlbumsAsync(); }, + cacheTtlMs: 900_000, }); export const immichAlbumRequestHandler = createIntegrationRequestHandler< @@ -39,4 +41,5 @@ export const immichAlbumRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getAlbumAsync(input.albumId); }, + cacheTtlMs: 900_000, }); diff --git a/packages/request-handler/src/indexer-manager.ts b/packages/request-handler/src/indexer-manager.ts index f21a61f8f..54a7cd46e 100644 --- a/packages/request-handler/src/indexer-manager.ts +++ b/packages/request-handler/src/indexer-manager.ts @@ -13,4 +13,5 @@ export const indexerManagerRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getIndexersAsync(); }, + cacheTtlMs: 300_000, }); diff --git a/packages/request-handler/src/lib/widget-request-handler.ts b/packages/request-handler/src/lib/widget-request-handler.ts index 8649791af..7fd3aba49 100644 --- a/packages/request-handler/src/lib/widget-request-handler.ts +++ b/packages/request-handler/src/lib/widget-request-handler.ts @@ -2,6 +2,7 @@ import { createRequestHandler } from "./request-handler"; interface Options> { requestAsync: (input: TInput) => Promise; + cacheTtlMs?: number; } export const createWidgetRequestHandler = >( @@ -10,5 +11,6 @@ export const createWidgetRequestHandler = createRequestHandler({ requestAsync: options.requestAsync, + cacheTtlMs: options.cacheTtlMs, }).handler(widgetOptions), }); diff --git a/packages/request-handler/src/media-release.ts b/packages/request-handler/src/media-release.ts index 02209f68f..32067a729 100644 --- a/packages/request-handler/src/media-release.ts +++ b/packages/request-handler/src/media-release.ts @@ -13,4 +13,5 @@ export const mediaReleaseRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getMediaReleasesAsync(); }, + cacheTtlMs: 300_000, }); diff --git a/packages/request-handler/src/media-request-stats.ts b/packages/request-handler/src/media-request-stats.ts index 61ff810b8..e7ef7ea9d 100644 --- a/packages/request-handler/src/media-request-stats.ts +++ b/packages/request-handler/src/media-request-stats.ts @@ -16,4 +16,5 @@ export const mediaRequestStatsRequestHandler = createIntegrationRequestHandler< users: await integrationInstance.getUsersAsync(), }; }, + cacheTtlMs: 60_000, }); diff --git a/packages/request-handler/src/media-server.ts b/packages/request-handler/src/media-server.ts index 55a7850e0..855472bab 100644 --- a/packages/request-handler/src/media-server.ts +++ b/packages/request-handler/src/media-server.ts @@ -15,4 +15,5 @@ export const mediaServerRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getCurrentSessionsAsync({ showOnlyPlaying: input.showOnlyPlaying }); }, + cacheTtlMs: 5_000, }); diff --git a/packages/request-handler/src/media-transcoding.ts b/packages/request-handler/src/media-transcoding.ts index ba6455ccd..950cbf0dc 100644 --- a/packages/request-handler/src/media-transcoding.ts +++ b/packages/request-handler/src/media-transcoding.ts @@ -17,6 +17,7 @@ export const mediaTranscodingRequestHandler = createIntegrationRequestHandler< statistics: await integrationInstance.getStatisticsAsync(), }; }, + cacheTtlMs: 300_000, }); export interface MediaTranscoding { diff --git a/packages/request-handler/src/minecraft-server-status.ts b/packages/request-handler/src/minecraft-server-status.ts index 664a0a7f3..1a357846f 100644 --- a/packages/request-handler/src/minecraft-server-status.ts +++ b/packages/request-handler/src/minecraft-server-status.ts @@ -14,6 +14,7 @@ export const minecraftServerStatusRequestHandler = createWidgetRequestHandler({ ); return responseSchema.parse(await response.json()); }, + cacheTtlMs: 300_000, }); const responseSchema = z diff --git a/packages/request-handler/src/navidrome.ts b/packages/request-handler/src/navidrome.ts index 0ad39ed9a..0773bdfc1 100644 --- a/packages/request-handler/src/navidrome.ts +++ b/packages/request-handler/src/navidrome.ts @@ -12,4 +12,5 @@ export const navidromeRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getDashboardDataAsync(); }, + cacheTtlMs: 600_000, }); diff --git a/packages/request-handler/src/network-controller.ts b/packages/request-handler/src/network-controller.ts index 123261025..319c38c3f 100644 --- a/packages/request-handler/src/network-controller.ts +++ b/packages/request-handler/src/network-controller.ts @@ -13,4 +13,5 @@ export const networkControllerRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getNetworkSummaryAsync(); }, + cacheTtlMs: 300_000, }); diff --git a/packages/request-handler/src/notifications.ts b/packages/request-handler/src/notifications.ts index 479ff81fa..013743809 100644 --- a/packages/request-handler/src/notifications.ts +++ b/packages/request-handler/src/notifications.ts @@ -13,4 +13,5 @@ export const notificationsRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getNotificationsAsync(); }, + cacheTtlMs: 300_000, }); diff --git a/packages/request-handler/src/paperless-ngx.ts b/packages/request-handler/src/paperless-ngx.ts index e6b088006..bdf2c2f44 100644 --- a/packages/request-handler/src/paperless-ngx.ts +++ b/packages/request-handler/src/paperless-ngx.ts @@ -12,4 +12,5 @@ export const paperlessNgxStatsRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getStatsAsync(); }, + cacheTtlMs: 900_000, }); diff --git a/packages/request-handler/src/rss-feeds.ts b/packages/request-handler/src/rss-feeds.ts index 0597a325f..d2c8c2bb1 100644 --- a/packages/request-handler/src/rss-feeds.ts +++ b/packages/request-handler/src/rss-feeds.ts @@ -28,6 +28,7 @@ export const rssFeedsRequestHandler = createWidgetRequestHandler({ entries: result.entries?.map((entry) => ({ ...entry, feedUrl: input.url })).slice(0, input.count) ?? [], }; }, + cacheTtlMs: 300_000, }); const attemptGetImageFromEntry = (feedUrl: string, entry: object) => { diff --git a/packages/request-handler/src/smart-home-entity-state.ts b/packages/request-handler/src/smart-home-entity-state.ts index 3c8851522..acca6742c 100644 --- a/packages/request-handler/src/smart-home-entity-state.ts +++ b/packages/request-handler/src/smart-home-entity-state.ts @@ -18,4 +18,5 @@ export const smartHomeEntityStateRequestHandler = createIntegrationRequestHandle return result.data.state; }, + cacheTtlMs: 60_000, }); diff --git a/packages/request-handler/src/speedtest-tracker.ts b/packages/request-handler/src/speedtest-tracker.ts index 3f03f7187..733adfd45 100644 --- a/packages/request-handler/src/speedtest-tracker.ts +++ b/packages/request-handler/src/speedtest-tracker.ts @@ -12,4 +12,5 @@ export const speedtestTrackerRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getDashboardDataAsync(); }, + cacheTtlMs: 600_000, }); diff --git a/packages/request-handler/src/stock-price.ts b/packages/request-handler/src/stock-price.ts index 177b223aa..b326dd479 100644 --- a/packages/request-handler/src/stock-price.ts +++ b/packages/request-handler/src/stock-price.ts @@ -39,6 +39,7 @@ export const fetchStockPriceHandler = createWidgetRequestHandler({ shortName: firstResult.meta.shortName, }; }, + cacheTtlMs: 300_000, }); const dataSchema = z diff --git a/packages/request-handler/src/tracearr.ts b/packages/request-handler/src/tracearr.ts index 641bf2922..c078c7508 100644 --- a/packages/request-handler/src/tracearr.ts +++ b/packages/request-handler/src/tracearr.ts @@ -12,4 +12,5 @@ export const tracearrRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getDashboardDataAsync(); }, + cacheTtlMs: 5_000, }); diff --git a/packages/request-handler/src/umami.ts b/packages/request-handler/src/umami.ts index 02ef71140..6245bffed 100644 --- a/packages/request-handler/src/umami.ts +++ b/packages/request-handler/src/umami.ts @@ -12,6 +12,7 @@ export const umamiRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getVisitorStatsAsync(input.websiteId, input.timeFrame, input.eventName); }, + cacheTtlMs: 300_000, }); export const umamiEventNamesRequestHandler = createIntegrationRequestHandler({ @@ -19,6 +20,7 @@ export const umamiEventNamesRequestHandler = createIntegrationRequestHandler data?.timestamp ?? new Date(), [data?.timestamp]);