diff --git a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx index 7d06a8951..6db1e26d9 100644 --- a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx +++ b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx @@ -2,6 +2,7 @@ import type { PropsWithChildren } from "react"; import { useState } from "react"; +import type { QueryKey } from "@tanstack/react-query"; import { QueryClient } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental"; @@ -33,6 +34,7 @@ import { import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared"; import { env } from "@homarr/common/env"; import { showWarningNotification } from "@homarr/notifications"; +import { widgetImports } from "@homarr/widgets"; import { createWidgetQueryPersister } from "./query-cache-persister"; @@ -90,6 +92,13 @@ export function TRPCReactProvider(props: PropsWithChildren) { }, }); client.setQueryDefaults([["widget"]], { refetchInterval: queryCacheDefaultRefetchIntervalMs }); + for (const { definition } of Object.values(widgetImports)) { + const def = definition as { refetchInterval?: number | false; queryKey?: QueryKey; kind: string }; + if (def.refetchInterval === undefined) continue; + const key = def.queryKey ?? [["widget", def.kind]]; + const interval = def.refetchInterval === false ? false : def.refetchInterval * 1000; + client.setQueryDefaults(key, { refetchInterval: interval }); + } return client; }); diff --git a/packages/widgets/src/definition.ts b/packages/widgets/src/definition.ts index aaf60269f..aa36ac06f 100644 --- a/packages/widgets/src/definition.ts +++ b/packages/widgets/src/definition.ts @@ -67,9 +67,17 @@ export const createWidgetDefinition = ({ showToggleAllButtons: factory.switch({ diff --git a/packages/widgets/src/dns-hole/summary/index.ts b/packages/widgets/src/dns-hole/summary/index.ts index fe1c8b8a1..5a614edf5 100644 --- a/packages/widgets/src/dns-hole/summary/index.ts +++ b/packages/widgets/src/dns-hole/summary/index.ts @@ -1,3 +1,4 @@ +import { widgetRefetchInterval } from "../../definition"; import { IconAd, IconServerOff } from "@tabler/icons-react"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; @@ -9,6 +10,8 @@ export const widgetKind = "dnsHoleSummary"; export const { definition, componentLoader } = createWidgetDefinition(widgetKind, { icon: IconAd, + queryKey: [["widget", "dnsHole"]], + refetchInterval: widgetRefetchInterval.fiveSeconds, createOptions() { return optionsBuilder.from((factory) => ({ usePiHoleColors: factory.switch({ diff --git a/packages/widgets/src/docker/component.tsx b/packages/widgets/src/docker/component.tsx index bbd961c1d..0d79c425d 100644 --- a/packages/widgets/src/docker/component.tsx +++ b/packages/widgets/src/docker/component.tsx @@ -188,9 +188,7 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo data, refetch, isFetching, - } = clientApi.docker.getContainers.useQuery(undefined, { - refetchInterval: 30_000, - }); + } = clientApi.docker.getContainers.useQuery(); const containers = data?.containers ?? []; const timestamp = useMemo(() => data?.timestamp ?? new Date(), [data?.timestamp]); const relativeTime = useTimeAgo(timestamp); diff --git a/packages/widgets/src/downloads/index.ts b/packages/widgets/src/downloads/index.ts index 36979083b..b72098b24 100644 --- a/packages/widgets/src/downloads/index.ts +++ b/packages/widgets/src/downloads/index.ts @@ -1,3 +1,4 @@ +import { widgetRefetchInterval } from "../definition"; import { IconDownload } from "@tabler/icons-react"; import { z } from "zod/v4"; @@ -33,6 +34,7 @@ const columnsSort = columnsList.filter((column) => export const { definition, componentLoader } = createWidgetDefinition("downloads", { icon: IconDownload, + refetchInterval: widgetRefetchInterval.fiveSeconds, createOptions() { return optionsBuilder.from( (factory) => ({ diff --git a/packages/widgets/src/firewall/index.ts b/packages/widgets/src/firewall/index.ts index 2278411de..f36fa8fec 100644 --- a/packages/widgets/src/firewall/index.ts +++ b/packages/widgets/src/firewall/index.ts @@ -1,3 +1,4 @@ +import { widgetRefetchInterval } from "../definition"; import { IconWall, IconWallOff } from "@tabler/icons-react"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; @@ -7,6 +8,7 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("firewall", { icon: IconWall, + refetchInterval: widgetRefetchInterval.fiveSeconds, createOptions() { return optionsBuilder.from(() => ({})); }, diff --git a/packages/widgets/src/health-monitoring/index.ts b/packages/widgets/src/health-monitoring/index.ts index fe31514de..cb3330b0f 100644 --- a/packages/widgets/src/health-monitoring/index.ts +++ b/packages/widgets/src/health-monitoring/index.ts @@ -1,3 +1,4 @@ +import { widgetRefetchInterval } from "../definition"; import { IconHeartRateMonitor, IconServerOff } from "@tabler/icons-react"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; @@ -8,6 +9,7 @@ import { createStorageVolumeMultiSelectOptions } from "../storage-volume-options export const { definition, componentLoader } = createWidgetDefinition("healthMonitoring", { icon: IconHeartRateMonitor, + refetchInterval: widgetRefetchInterval.fiveSeconds, createOptions() { return optionsBuilder.from( (factory) => ({ diff --git a/packages/widgets/src/immich/album-carousel/index.ts b/packages/widgets/src/immich/album-carousel/index.ts index 9f8110ffc..454d75148 100644 --- a/packages/widgets/src/immich/album-carousel/index.ts +++ b/packages/widgets/src/immich/album-carousel/index.ts @@ -1,3 +1,4 @@ +import { widgetRefetchInterval } from "../../definition"; import { IconPhoto } from "@tabler/icons-react"; import z from "zod"; @@ -8,6 +9,8 @@ import { optionsBuilder } from "../../options"; export const { definition, componentLoader } = createWidgetDefinition("immich-albumCarousel", { icon: IconPhoto, + queryKey: [["widget", "immich"]], + refetchInterval: widgetRefetchInterval.never, supportedIntegrations: ["immich"], integrationsRequired: true, createOptions() { diff --git a/packages/widgets/src/immich/server-stats/index.ts b/packages/widgets/src/immich/server-stats/index.ts index 61b07289a..2e0dbeac7 100644 --- a/packages/widgets/src/immich/server-stats/index.ts +++ b/packages/widgets/src/immich/server-stats/index.ts @@ -1,3 +1,4 @@ +import { widgetRefetchInterval } from "../../definition"; import { IconGraphFilled } from "@tabler/icons-react"; import { createWidgetDefinition } from "../../definition"; @@ -5,6 +6,8 @@ import { optionsBuilder } from "../../options"; export const { definition, componentLoader } = createWidgetDefinition("immich-serverStats", { icon: IconGraphFilled, + queryKey: [["widget", "immich"]], + refetchInterval: widgetRefetchInterval.never, supportedIntegrations: ["immich"], integrationsRequired: true, createOptions() { diff --git a/packages/widgets/src/index.tsx b/packages/widgets/src/index.tsx index 061fc4b2d..79d4c21a9 100644 --- a/packages/widgets/src/index.tsx +++ b/packages/widgets/src/index.tsx @@ -70,7 +70,9 @@ export type { WidgetContextMenuAction, WidgetContextActionProps, WidgetOptionsSettings, + WidgetRefetchInterval, } from "./definition"; +export { widgetRefetchInterval } from "./definition"; export type { WidgetComponentProps }; export type { WidgetOptionDefinition, WidgetOptionType } from "./options"; diff --git a/packages/widgets/src/indexer-manager/index.ts b/packages/widgets/src/indexer-manager/index.ts index c2160999f..aaa2cdc29 100644 --- a/packages/widgets/src/indexer-manager/index.ts +++ b/packages/widgets/src/indexer-manager/index.ts @@ -1,3 +1,4 @@ +import { widgetRefetchInterval } from "../definition"; import { IconReportSearch, IconServerOff } from "@tabler/icons-react"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; @@ -7,6 +8,7 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("indexerManager", { icon: IconReportSearch, + refetchInterval: widgetRefetchInterval.never, createOptions() { return optionsBuilder.from((factory) => ({ openIndexerSiteInNewTab: factory.switch({ diff --git a/packages/widgets/src/media-releases/index.ts b/packages/widgets/src/media-releases/index.ts index af650eef7..581ae1abf 100644 --- a/packages/widgets/src/media-releases/index.ts +++ b/packages/widgets/src/media-releases/index.ts @@ -1,3 +1,4 @@ +import { widgetRefetchInterval } from "../definition"; import { IconTicket } from "@tabler/icons-react"; import { createWidgetDefinition } from "../definition"; @@ -5,6 +6,8 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("mediaReleases", { icon: IconTicket, + queryKey: [["widget", "mediaRelease"]], + refetchInterval: widgetRefetchInterval.never, createOptions() { return optionsBuilder.from((factory) => ({ layout: factory.select({ diff --git a/packages/widgets/src/media-server/index.ts b/packages/widgets/src/media-server/index.ts index b85d09857..c9190fe23 100644 --- a/packages/widgets/src/media-server/index.ts +++ b/packages/widgets/src/media-server/index.ts @@ -1,3 +1,4 @@ +import { widgetRefetchInterval } from "../definition"; import { IconVideo } from "@tabler/icons-react"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; @@ -7,6 +8,7 @@ import { optionsBuilder } from "../options"; export const { componentLoader, definition } = createWidgetDefinition("mediaServer", { icon: IconVideo, + refetchInterval: widgetRefetchInterval.fiveSeconds, createOptions() { return optionsBuilder.from((factory) => ({ showOnlyPlaying: factory.switch({ defaultValue: true, withDescription: true }), diff --git a/packages/widgets/src/media-transcoding/index.ts b/packages/widgets/src/media-transcoding/index.ts index 2753925d8..315c2f78d 100644 --- a/packages/widgets/src/media-transcoding/index.ts +++ b/packages/widgets/src/media-transcoding/index.ts @@ -1,3 +1,4 @@ +import { widgetRefetchInterval } from "../definition"; import { IconTransform } from "@tabler/icons-react"; import { z } from "zod/v4"; @@ -11,6 +12,7 @@ export const views = ["workers", "queue", "statistics"] as const; export const { componentLoader, definition } = createWidgetDefinition("mediaTranscoding", { icon: IconTransform, + refetchInterval: widgetRefetchInterval.never, createOptions() { return optionsBuilder.from((factory) => ({ defaultView: factory.select({ diff --git a/packages/widgets/src/minecraft/server-status/index.ts b/packages/widgets/src/minecraft/server-status/index.ts index c5cd0e068..fba59cd91 100644 --- a/packages/widgets/src/minecraft/server-status/index.ts +++ b/packages/widgets/src/minecraft/server-status/index.ts @@ -1,3 +1,4 @@ +import { widgetRefetchInterval } from "../../definition"; import { IconBrandMinecraft } from "@tabler/icons-react"; import { z } from "zod/v4"; @@ -6,6 +7,8 @@ import { optionsBuilder } from "../../options"; export const { componentLoader, definition } = createWidgetDefinition("minecraftServerStatus", { icon: IconBrandMinecraft, + queryKey: [["widget", "minecraft"]], + refetchInterval: widgetRefetchInterval.never, createOptions() { return optionsBuilder.from((factory) => ({ title: factory.text({ defaultValue: "" }), diff --git a/packages/widgets/src/network-controller/network-status/index.ts b/packages/widgets/src/network-controller/network-status/index.ts index 4af2fbaf0..9dae83fd4 100644 --- a/packages/widgets/src/network-controller/network-status/index.ts +++ b/packages/widgets/src/network-controller/network-status/index.ts @@ -1,3 +1,4 @@ +import { widgetRefetchInterval } from "../../definition"; import { IconServerOff, IconTopologyFull } from "@tabler/icons-react"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; @@ -7,6 +8,8 @@ import { optionsBuilder } from "../../options"; export const { definition, componentLoader } = createWidgetDefinition("networkControllerStatus", { icon: IconTopologyFull, + queryKey: [["widget", "networkController"]], + refetchInterval: widgetRefetchInterval.never, createOptions() { return optionsBuilder.from((factory) => ({ content: factory.select({ diff --git a/packages/widgets/src/network-controller/summary/index.ts b/packages/widgets/src/network-controller/summary/index.ts index dd541024a..3e3db73cc 100644 --- a/packages/widgets/src/network-controller/summary/index.ts +++ b/packages/widgets/src/network-controller/summary/index.ts @@ -1,3 +1,4 @@ +import { widgetRefetchInterval } from "../../definition"; import { IconServerOff, IconTopologyFull } from "@tabler/icons-react"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; @@ -7,6 +8,8 @@ import { optionsBuilder } from "../../options"; export const { definition, componentLoader } = createWidgetDefinition("networkControllerSummary", { icon: IconTopologyFull, + queryKey: [["widget", "networkController"]], + refetchInterval: widgetRefetchInterval.never, createOptions() { return optionsBuilder.from(() => ({})); }, diff --git a/packages/widgets/src/notifications/index.ts b/packages/widgets/src/notifications/index.ts index 553ef5d9f..699359799 100644 --- a/packages/widgets/src/notifications/index.ts +++ b/packages/widgets/src/notifications/index.ts @@ -1,3 +1,4 @@ +import { widgetRefetchInterval } from "../definition"; import { IconMessage } from "@tabler/icons-react"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; @@ -7,6 +8,7 @@ import { optionsBuilder } from "../options"; export const { componentLoader, definition } = createWidgetDefinition("notifications", { icon: IconMessage, + refetchInterval: widgetRefetchInterval.never, createOptions() { return optionsBuilder.from((factory) => ({ hideLogos: factory.switch({ defaultValue: false }), diff --git a/packages/widgets/src/paperless-ngx/index.ts b/packages/widgets/src/paperless-ngx/index.ts index 59af7ca59..7f47de3be 100644 --- a/packages/widgets/src/paperless-ngx/index.ts +++ b/packages/widgets/src/paperless-ngx/index.ts @@ -1,3 +1,4 @@ +import { widgetRefetchInterval } from "../definition"; import { IconFileText, IconServerOff } from "@tabler/icons-react"; import { createWidgetDefinition } from "../definition"; @@ -5,6 +6,7 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("paperlessNgx", { icon: IconFileText, + refetchInterval: widgetRefetchInterval.never, supportedIntegrations: ["paperlessNgx"], integrationsRequired: true, createOptions() { diff --git a/packages/widgets/src/releases/index.ts b/packages/widgets/src/releases/index.ts index 2c3d1d3a2..09855309d 100644 --- a/packages/widgets/src/releases/index.ts +++ b/packages/widgets/src/releases/index.ts @@ -1,3 +1,4 @@ +import { widgetRefetchInterval } from "../definition"; import { IconRocket } from "@tabler/icons-react"; import { z } from "zod/v4"; @@ -14,6 +15,7 @@ const relativeDateSchema = z export const { definition, componentLoader } = createWidgetDefinition("releases", { icon: IconRocket, + refetchInterval: widgetRefetchInterval.never, createOptions() { return optionsBuilder.from((factory) => ({ newReleaseWithin: factory.text({ diff --git a/packages/widgets/src/rssFeed/index.ts b/packages/widgets/src/rssFeed/index.ts index 079c761a9..052afa998 100644 --- a/packages/widgets/src/rssFeed/index.ts +++ b/packages/widgets/src/rssFeed/index.ts @@ -1,3 +1,4 @@ +import { widgetRefetchInterval } from "../definition"; import { IconRss } from "@tabler/icons-react"; import { z } from "zod/v4"; @@ -12,6 +13,7 @@ import { optionsBuilder } from "../options"; */ export const { definition, componentLoader } = createWidgetDefinition("rssFeed", { icon: IconRss, + refetchInterval: widgetRefetchInterval.never, createOptions() { return optionsBuilder.from((factory) => ({ feedUrls: factory.multiText({ diff --git a/packages/widgets/src/speedtest-tracker/index.ts b/packages/widgets/src/speedtest-tracker/index.ts index ea3c0c5d2..08d963cd2 100644 --- a/packages/widgets/src/speedtest-tracker/index.ts +++ b/packages/widgets/src/speedtest-tracker/index.ts @@ -1,3 +1,4 @@ +import { widgetRefetchInterval } from "../definition"; import { IconServerOff, IconSpeedboat } from "@tabler/icons-react"; import { createWidgetDefinition } from "../definition"; @@ -5,6 +6,7 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("speedtestTracker", { icon: IconSpeedboat, + refetchInterval: widgetRefetchInterval.never, createOptions() { return optionsBuilder.from( (factory) => ({ diff --git a/packages/widgets/src/stocks/index.ts b/packages/widgets/src/stocks/index.ts index fa64bf3a4..46bd3d633 100644 --- a/packages/widgets/src/stocks/index.ts +++ b/packages/widgets/src/stocks/index.ts @@ -1,3 +1,4 @@ +import { widgetRefetchInterval } from "../definition"; import { IconBuildingBank } from "@tabler/icons-react"; import { createWidgetDefinition } from "../definition"; @@ -13,6 +14,7 @@ const timeIntervalOptions = stockPriceTimeFrames.interval; export const { definition, componentLoader } = createWidgetDefinition("stockPrice", { icon: IconBuildingBank, + refetchInterval: widgetRefetchInterval.never, createOptions() { return optionsBuilder.from((factory) => ({ stock: factory.text({ diff --git a/packages/widgets/src/system-disks/index.ts b/packages/widgets/src/system-disks/index.ts index 61e4a5c05..b3d508860 100644 --- a/packages/widgets/src/system-disks/index.ts +++ b/packages/widgets/src/system-disks/index.ts @@ -1,3 +1,4 @@ +import { widgetRefetchInterval } from "../definition"; import { IconServer2 } from "@tabler/icons-react"; import { createWidgetDefinition } from "../definition"; @@ -6,6 +7,8 @@ import { createStorageVolumeMultiSelectOptions } from "../storage-volume-options export const { definition, componentLoader } = createWidgetDefinition("systemDisks", { icon: IconServer2, + queryKey: [["widget", "healthMonitoring"]], + refetchInterval: widgetRefetchInterval.fiveSeconds, supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "synology"], createOptions() { return optionsBuilder.from( diff --git a/packages/widgets/src/system-resources/index.ts b/packages/widgets/src/system-resources/index.ts index 8d4837187..e722c36e5 100644 --- a/packages/widgets/src/system-resources/index.ts +++ b/packages/widgets/src/system-resources/index.ts @@ -1,3 +1,4 @@ +import { widgetRefetchInterval } from "../definition"; import { IconAlignLeft, IconEyeOff, IconGraphFilled, IconListDetails, IconPhoto } from "@tabler/icons-react"; import { objectEntries } from "@homarr/common"; @@ -14,6 +15,8 @@ const labelDisplayModeOptions = { export const { definition, componentLoader } = createWidgetDefinition("systemResources", { icon: IconGraphFilled, + queryKey: [["widget", "healthMonitoring"]], + refetchInterval: widgetRefetchInterval.fiveSeconds, supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "glances", "synology"], createOptions() { return optionsBuilder.from((factory) => ({ diff --git a/packages/widgets/src/tracearr/index.ts b/packages/widgets/src/tracearr/index.ts index 44936cb67..41c4fad02 100644 --- a/packages/widgets/src/tracearr/index.ts +++ b/packages/widgets/src/tracearr/index.ts @@ -1,3 +1,4 @@ +import { widgetRefetchInterval } from "../definition"; import { IconActivityHeartbeat, IconServerOff } from "@tabler/icons-react"; import { createWidgetDefinition } from "../definition"; @@ -5,6 +6,7 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("tracearr", { icon: IconActivityHeartbeat, + refetchInterval: widgetRefetchInterval.fiveSeconds, createOptions() { return optionsBuilder.from((factory) => ({ showStreams: factory.switch({ diff --git a/packages/widgets/src/umami/index.ts b/packages/widgets/src/umami/index.ts index c55ecd261..422ae97b6 100644 --- a/packages/widgets/src/umami/index.ts +++ b/packages/widgets/src/umami/index.ts @@ -1,3 +1,4 @@ +import { widgetRefetchInterval } from "../definition"; import { IconChartBar, IconServerOff } from "@tabler/icons-react"; import { z } from "zod/v4"; @@ -8,6 +9,7 @@ export const timeFrameValues = ["today", "24h", "7d", "30d", "month", "lastMonth export const { definition, componentLoader } = createWidgetDefinition("umami", { icon: IconChartBar, + refetchInterval: widgetRefetchInterval.never, supportedIntegrations: ["umami"], createOptions() { return optionsBuilder.from( diff --git a/packages/widgets/src/vpn/index.ts b/packages/widgets/src/vpn/index.ts index 426366a66..35918db6a 100644 --- a/packages/widgets/src/vpn/index.ts +++ b/packages/widgets/src/vpn/index.ts @@ -1,3 +1,4 @@ +import { widgetRefetchInterval } from "../definition"; import { IconShieldLock } from "@tabler/icons-react"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; @@ -7,6 +8,7 @@ import { optionsBuilder } from "../options"; export const { componentLoader, definition } = createWidgetDefinition("vpn", { icon: IconShieldLock, + refetchInterval: widgetRefetchInterval.never, createOptions() { return optionsBuilder.from(() => ({})); },