diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 000000000..103c85307 --- /dev/null +++ b/.mise.toml @@ -0,0 +1,3 @@ +[tools] +node = "24.16.0" +pnpm = "10.33.4" diff --git a/apps/nextjs/src/app/[locale]/_client-providers/query-cache-persister.ts b/apps/nextjs/src/app/[locale]/_client-providers/query-cache-persister.ts index faeadbb56..70d8581c7 100644 --- a/apps/nextjs/src/app/[locale]/_client-providers/query-cache-persister.ts +++ b/apps/nextjs/src/app/[locale]/_client-providers/query-cache-persister.ts @@ -6,7 +6,6 @@ import { fetchApi } from "@homarr/api/client"; import { getActiveQueryCacheBoardId, isPersistableWidgetQueryKey, - queryCacheMaxAgeMs, queryCacheStoragePrefix, } from "@homarr/api/query-cache"; @@ -39,7 +38,7 @@ const queryCacheStorage = { export const createWidgetQueryPersister = () => experimental_createQueryPersister({ storage: typeof window === "undefined" ? undefined : queryCacheStorage, - maxAge: queryCacheMaxAgeMs, + maxAge: Number.POSITIVE_INFINITY, prefix: queryCacheStoragePrefix, refetchOnRestore: true, serialize: (query) => stringify(query), diff --git a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx index 7601dac6d..76cf90695 100644 --- a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx +++ b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx @@ -19,6 +19,7 @@ import type { SuperJSONResult } from "superjson"; import type { AppRouter } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; +import { queryCacheDefaultStaleTimeMs } from "@homarr/api/query-cache"; import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared"; import { env } from "@homarr/common/env"; @@ -58,7 +59,7 @@ export function TRPCReactProvider(props: PropsWithChildren) { defaultOptions: { queries: { persister: queryPersister.persisterFn, - staleTime: 5 * 1000, + staleTime: queryCacheDefaultStaleTimeMs, }, }, }), diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx index 73235701e..27ed4b4fa 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx @@ -9,7 +9,6 @@ import type { DehydratedState } from "@tanstack/react-query"; import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; import { parse } from "superjson"; -import { queryCacheMaxAgeMs } from "@homarr/api/query-cache"; import { getQueryClient } from "@homarr/api/server"; import { IntegrationProvider } from "@homarr/auth/client"; import { auth } from "@homarr/auth/next"; @@ -127,13 +126,11 @@ interface PersistedEntry { async function QueryCacheHydration({ userId, boardId }: { userId: string; boardId: string }) { try { const entries = await getAllQueryCacheAsync(userId, boardId); - const now = Date.now(); const queries: DehydratedState["queries"] = []; for (const serialized of Object.values(entries)) { try { const persisted = parse(serialized); - if (now - persisted.state.dataUpdatedAt > queryCacheMaxAgeMs) continue; if (persisted.state.status !== "success") continue; queries.push({ diff --git a/apps/nextjs/src/components/layout/header/query-refresh-indicator.tsx b/apps/nextjs/src/components/layout/header/query-refresh-indicator.tsx deleted file mode 100644 index 6ac2e7f1e..000000000 --- a/apps/nextjs/src/components/layout/header/query-refresh-indicator.tsx +++ /dev/null @@ -1,222 +0,0 @@ -"use client"; - -import { forwardRef, useEffect, useState } from "react"; -import { Avatar, Box, Group, Indicator, Menu, ScrollArea, Text, Tooltip, UnstyledButton } from "@mantine/core"; -import { useQueryClient } from "@tanstack/react-query"; -import dayjs from "dayjs"; - -import { useEditMode } from "@homarr/boards/edit-mode"; -import type { IntegrationKind } from "@homarr/definitions"; -import { getIconUrl } from "@homarr/definitions"; - -interface IntegrationInfo { - id: string; - kind: IntegrationKind; - name: string; -} - -interface QueryStatusRow { - id: string; - procedure: string; - integrationIcons: { kind: IntegrationKind; name: string }[]; - status: "pending" | "error" | "success"; - fetchStatus: "fetching" | "paused" | "idle"; - dataUpdatedAt: number; -} - -const hiddenProcedures = new Set(["widget.app.ping"]); - -const isTrackedWidgetQuery = (queryKey: readonly unknown[]): boolean => { - const first = queryKey[0]; - if (!Array.isArray(first) || first.length < 2 || first[0] !== "widget") return false; - - const path = first.join("."); - return !hiddenProcedures.has(path); -}; - -const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); - -const parseWidgetQueryKey = (queryKey: readonly unknown[]): { procedure: string; integrationIds: string[] } => { - const first = queryKey[0] as string[]; - const procedure = first.slice(1).map(capitalize).join(" — "); - - const second = queryKey[1] as { input?: Record } | undefined; - const integrationIds = Array.isArray(second?.input?.integrationIds) ? (second.input.integrationIds as string[]) : []; - - return { procedure, integrationIds }; -}; - -const statusColors: Record = { - fetching: "orange", - success: "green", - error: "red", - pending: "gray", -}; - -const resolveRowColor = (fetchStatus: string, status: string) => { - if (fetchStatus === "fetching") return statusColors.fetching!; - return statusColors[status] ?? "gray"; -}; - -const resolveDotColor = (fetchingCount: number, errorCount: number) => { - if (fetchingCount > 0) return statusColors.fetching!; - if (errorCount > 0) return statusColors.error!; - return statusColors.success!; -}; - -interface CacheSnapshot { - rows: QueryStatusRow[]; - fetchingCount: number; - errorCount: number; -} - -const emptyCacheSnapshot: CacheSnapshot = { rows: [], fetchingCount: 0, errorCount: 0 }; - -function resolveIntegrationMap(queryClient: ReturnType): Map { - const cache = queryClient.getQueryCache(); - const integrationQuery = cache.find({ queryKey: [["integration", "all"]] }); - - const map = new Map(); - const data = integrationQuery?.state.data as IntegrationInfo[] | undefined; - if (!data) return map; - - for (const integration of data) { - map.set(integration.id, integration); - } - return map; -} - -function useQueryCacheStatus() { - const queryClient = useQueryClient(); - const [snapshot, setSnapshot] = useState(emptyCacheSnapshot); - - useEffect(() => { - const update = () => { - const cache = queryClient.getQueryCache(); - const queries = cache.findAll({ type: "active" }).filter((q) => isTrackedWidgetQuery(q.queryKey)); - const integrationMap = resolveIntegrationMap(queryClient); - - let fetching = 0; - let errors = 0; - const nextRows: QueryStatusRow[] = queries.map((q) => { - if (q.state.fetchStatus === "fetching") fetching++; - if (q.state.status === "error") errors++; - const { procedure, integrationIds } = parseWidgetQueryKey(q.queryKey); - - const integrationIcons = integrationIds - .map((id) => integrationMap.get(id)) - .filter((info): info is IntegrationInfo => info !== undefined) - .map(({ kind, name }) => ({ kind, name })); - - return { - id: q.queryHash, - procedure, - integrationIcons, - status: q.state.status, - fetchStatus: q.state.fetchStatus, - dataUpdatedAt: q.state.dataUpdatedAt, - }; - }); - - setSnapshot({ rows: nextRows, fetchingCount: fetching, errorCount: errors }); - }; - - update(); - - let rafId: number | null = null; - const unsubscribe = queryClient.getQueryCache().subscribe(() => { - if (rafId !== null) return; - rafId = requestAnimationFrame(() => { - rafId = null; - update(); - }); - }); - - return () => { - unsubscribe(); - if (rafId !== null) cancelAnimationFrame(rafId); - }; - }, [queryClient]); - - return { ...snapshot, totalCount: snapshot.rows.length }; -} - -const StatusDot = forwardRef( - ({ color, processing, ...others }, ref) => ( - - - - - - ), -); - -export const QueryRefreshIndicator = () => { - const [isEditMode] = useEditMode(); - const { rows, fetchingCount, errorCount, totalCount } = useQueryCacheStatus(); - - if (isEditMode || totalCount === 0) return null; - - const isFetching = fetchingCount > 0; - const dotColor = resolveDotColor(fetchingCount, errorCount); - - return ( - - - - - - - {rows.map((row) => ( - - ))} - - - - ); -}; - -const SmallDot = ({ color }: { color: string }) => ( - -); - -const QueryMenuItem = ({ row }: { row: QueryStatusRow }) => { - const rowColor = resolveRowColor(row.fetchStatus, row.status); - const updatedLabel = row.dataUpdatedAt > 0 ? dayjs(row.dataUpdatedAt).fromNow() : "—"; - - return ( - - - {row.integrationIcons.length > 0 && } - - } - rightSection={ - - {updatedLabel} - - } - style={{ cursor: "default" }} - > - - {row.procedure} - - - ); -}; - -const IntegrationAvatars = ({ icons }: { icons: QueryStatusRow["integrationIcons"] }) => ( - - {icons.slice(0, 3).map((icon, index) => ( - - - - ))} - -); diff --git a/apps/nextjs/src/components/layout/logo/board-logo.tsx b/apps/nextjs/src/components/layout/logo/board-logo.tsx index 86bf94eeb..ad2f1ca2e 100644 --- a/apps/nextjs/src/components/layout/logo/board-logo.tsx +++ b/apps/nextjs/src/components/layout/logo/board-logo.tsx @@ -1,10 +1,7 @@ "use client"; -import { Group } from "@mantine/core"; - import { useRequiredBoard } from "@homarr/boards/context"; -import { QueryRefreshIndicator } from "../header/query-refresh-indicator"; import { homarrLogoPath, homarrPageTitle } from "./homarr-logo"; import type { LogoWithTitleProps } from "./logo"; import { Logo, LogoWithTitle } from "./logo"; @@ -36,14 +33,11 @@ export const BoardLogoWithTitle = ({ size, hideTitleOnMobile }: CommonLogoWithTi const board = useRequiredBoard(); const imageOptions = useImageOptions(); return ( - - - - + ); }; diff --git a/packages/api/src/query-cache.ts b/packages/api/src/query-cache.ts index d6e19f618..690059723 100644 --- a/packages/api/src/query-cache.ts +++ b/packages/api/src/query-cache.ts @@ -1,6 +1,7 @@ import type { QueryKey } from "@tanstack/react-query"; -export const queryCacheMaxAgeMs = 1000 * 60 * 60 * 24; +export const queryCacheRetentionMs = 1000 * 60 * 60 * 24; +export const queryCacheDefaultStaleTimeMs = 1000 * 60 * 5; export const queryCacheMaxValueBytes = 1024 * 1024; export const queryCacheStoragePrefix = "homarr-widget-query"; diff --git a/packages/api/src/router/query-cache.ts b/packages/api/src/router/query-cache.ts index d79de2711..6143501fc 100644 --- a/packages/api/src/router/query-cache.ts +++ b/packages/api/src/router/query-cache.ts @@ -4,7 +4,7 @@ import { eq } from "@homarr/db"; import { boards } from "@homarr/db/schema"; import { createQueryCacheChannel } from "@homarr/redis"; -import { queryCacheMaxAgeMs, queryCacheMaxValueBytes } from "../query-cache"; +import { queryCacheMaxValueBytes, queryCacheRetentionMs } from "../query-cache"; import { createTRPCRouter, publicProcedure } from "../trpc"; import { throwIfActionForbiddenAsync } from "./board/board-access"; @@ -22,7 +22,7 @@ const createChannel = (input: z.infer, userId: string userId, boardId: input.boardId, key: input.key, - ttlMs: queryCacheMaxAgeMs, + ttlMs: queryCacheRetentionMs, maxValueBytes: queryCacheMaxValueBytes, }); diff --git a/packages/api/src/shared.ts b/packages/api/src/shared.ts index 4ef9b7a4f..e05700f22 100644 --- a/packages/api/src/shared.ts +++ b/packages/api/src/shared.ts @@ -1,5 +1,7 @@ import { defaultShouldDehydrateQuery, QueryClient } from "@tanstack/react-query"; +import { queryCacheDefaultStaleTimeMs } from "./query-cache"; + /** * Creates a headers callback for a given source * It will set the x-trpc-source header and cookies if needed @@ -58,7 +60,7 @@ export const makeQueryClient = () => { return new QueryClient({ defaultOptions: { queries: { - staleTime: 5 * 1000, + staleTime: queryCacheDefaultStaleTimeMs, }, dehydrate: { shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === "pending", diff --git a/packages/api/src/test/query-cache.spec.ts b/packages/api/src/test/query-cache.spec.ts index aba9fbf42..c765af031 100644 --- a/packages/api/src/test/query-cache.spec.ts +++ b/packages/api/src/test/query-cache.spec.ts @@ -1,11 +1,17 @@ import { afterEach, describe, expect, test, vi } from "vitest"; +import { QueryClient } from "@tanstack/react-query"; import type { Session } from "@homarr/auth"; import { createId } from "@homarr/common"; import type { Database } from "@homarr/db"; import { boards, users } from "@homarr/db/schema"; import { createDb } from "@homarr/db/test"; -import { isPersistableWidgetQueryKey, queryCacheMaxAgeMs, queryCacheMaxValueBytes } from "@homarr/api/query-cache"; +import { + isPersistableWidgetQueryKey, + queryCacheDefaultStaleTimeMs, + queryCacheMaxValueBytes, + queryCacheRetentionMs, +} from "@homarr/api/query-cache"; import { queryCacheRouter } from "../router/query-cache"; @@ -141,7 +147,7 @@ describe("queryCacheRouter", () => { userId: "anonymous", boardId: "board-1", key: "query-key", - ttlMs: queryCacheMaxAgeMs, + ttlMs: queryCacheRetentionMs, maxValueBytes: queryCacheMaxValueBytes, }); }); @@ -161,7 +167,7 @@ describe("queryCacheRouter", () => { expect(redisMock.calls.at(-1)).toMatchObject({ userId: "user-1", boardId: "board-1", - ttlMs: queryCacheMaxAgeMs, + ttlMs: queryCacheRetentionMs, maxValueBytes: queryCacheMaxValueBytes, }); }); @@ -180,8 +186,39 @@ describe("queryCacheRouter", () => { await caller.setItem({ boardId: "board-1", key: "query-key", value: "cached-value" }); await expect(caller.getItem({ boardId: "board-1", key: "query-key" })).resolves.toBe("cached-value"); - await vi.advanceTimersByTimeAsync(queryCacheMaxAgeMs + 1); + await vi.advanceTimersByTimeAsync(queryCacheRetentionMs + 1); await expect(caller.getItem({ boardId: "board-1", key: "query-key" })).resolves.toBeNull(); }); }); + +describe("query cache stale handling", () => { + test("uses TanStack Query dataUpdatedAt and staleTime to decide when cached data refetches", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-01-01T00:10:00.000Z")); + + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + staleTime: queryCacheDefaultStaleTimeMs, + }, + }, + }); + const queryKey = [["widget", "example"], { input: { integrationIds: ["integration-1"] }, type: "query" }]; + const queryFn = vi.fn().mockResolvedValue("fresh"); + + queryClient.setQueryData(queryKey, "cached", { + updatedAt: Date.now() - queryCacheDefaultStaleTimeMs - 1, + }); + + await expect( + queryClient.fetchQuery({ + queryKey, + queryFn, + }), + ).resolves.toBe("fresh"); + + expect(queryFn).toHaveBeenCalledOnce(); + }); +}); diff --git a/packages/widgets/src/anchor-note/component.tsx b/packages/widgets/src/anchor-note/component.tsx index 6a2718505..99585b838 100644 --- a/packages/widgets/src/anchor-note/component.tsx +++ b/packages/widgets/src/anchor-note/component.tsx @@ -151,7 +151,7 @@ const AnchorNoteWidgetContent = ({ options, integrationId, noteId }: AnchorNoteW noteId, }, { - refetchOnMount: false, + staleTime: 15 * 1000, refetchOnWindowFocus: false, refetchOnReconnect: false, retry: false, diff --git a/packages/widgets/src/app/component.tsx b/packages/widgets/src/app/component.tsx index 05a306db3..054a2da6b 100644 --- a/packages/widgets/src/app/component.tsx +++ b/packages/widgets/src/app/component.tsx @@ -28,7 +28,6 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget id: options.appId, }, { - refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false, retry: false, diff --git a/packages/widgets/src/calendar/component.tsx b/packages/widgets/src/calendar/component.tsx index b3c3160ea..ac3d8b37b 100644 --- a/packages/widgets/src/calendar/component.tsx +++ b/packages/widgets/src/calendar/component.tsx @@ -40,7 +40,7 @@ const FetchCalendar = ({ month, setMonth, isEditMode, integrationIds, options }: showUnmonitored: options.showUnmonitored, }; const { data } = clientApi.widget.calendar.findAllEvents.useQuery(input, { - refetchOnMount: false, + staleTime: 60 * 1000, refetchOnWindowFocus: false, refetchOnReconnect: false, retry: false, diff --git a/packages/widgets/src/coolify/component.tsx b/packages/widgets/src/coolify/component.tsx index 3dde4bcfb..91b37ecd4 100644 --- a/packages/widgets/src/coolify/component.tsx +++ b/packages/widgets/src/coolify/component.tsx @@ -31,7 +31,10 @@ interface CoolifyContentProps { } function CoolifyContent({ integrationIds, options, width }: CoolifyContentProps) { - const { data: instancesData = [] } = clientApi.widget.coolify.getInstancesInfo.useQuery({ integrationIds }); + const { data: instancesData = [] } = clientApi.widget.coolify.getInstancesInfo.useQuery( + { integrationIds }, + { staleTime: 30 * 1000 }, + ); const utils = clientApi.useUtils(); clientApi.widget.coolify.subscribeInstancesInfo.useSubscription( diff --git a/packages/widgets/src/dns-hole/controls/component.tsx b/packages/widgets/src/dns-hole/controls/component.tsx index 460e86d04..afe397cfd 100644 --- a/packages/widgets/src/dns-hole/controls/component.tsx +++ b/packages/widgets/src/dns-hole/controls/component.tsx @@ -56,7 +56,7 @@ export default function DnsHoleControlsWidget({ integrationIds, }, { - refetchOnMount: false, + staleTime: 5 * 1000, refetchOnWindowFocus: false, refetchOnReconnect: false, retry: false, diff --git a/packages/widgets/src/dns-hole/summary/component.tsx b/packages/widgets/src/dns-hole/summary/component.tsx index a72960d03..4a4c2879c 100644 --- a/packages/widgets/src/dns-hole/summary/component.tsx +++ b/packages/widgets/src/dns-hole/summary/component.tsx @@ -25,7 +25,7 @@ export default function DnsHoleSummaryWidget({ options, integrationIds }: Widget integrationIds, }, { - refetchOnMount: false, + staleTime: 5 * 1000, refetchOnWindowFocus: false, refetchOnReconnect: false, retry: false, diff --git a/packages/widgets/src/docker/component.tsx b/packages/widgets/src/docker/component.tsx index 7e6b8ef44..677a5c533 100644 --- a/packages/widgets/src/docker/component.tsx +++ b/packages/widgets/src/docker/component.tsx @@ -204,7 +204,9 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo const isTiny = width <= 256; const utils = clientApi.useUtils(); - const { data } = clientApi.docker.getContainers.useQuery(); + const { data } = clientApi.docker.getContainers.useQuery(undefined, { + staleTime: 20 * 1000, + }); const containers = data?.containers ?? []; const timestamp = useMemo(() => data?.timestamp ?? new Date(), [data?.timestamp]); const relativeTime = useTimeAgo(timestamp); diff --git a/packages/widgets/src/downloads/component.tsx b/packages/widgets/src/downloads/component.tsx index e84544f92..bee31eb20 100644 --- a/packages/widgets/src/downloads/component.tsx +++ b/packages/widgets/src/downloads/component.tsx @@ -93,7 +93,7 @@ export default function DownloadClientsWidget({ limitPerIntegration: options.limitPerIntegration, }, { - refetchOnMount: false, + staleTime: 5 * 1000, refetchOnWindowFocus: false, refetchOnReconnect: false, retry: false, diff --git a/packages/widgets/src/firewall/component.tsx b/packages/widgets/src/firewall/component.tsx index 80f1812eb..7318cd3d3 100644 --- a/packages/widgets/src/firewall/component.tsx +++ b/packages/widgets/src/firewall/component.tsx @@ -189,7 +189,7 @@ export const useUpdatingCpuStatus = (integrationIds: string[]) => { integrationIds, }, { - refetchOnMount: false, + staleTime: 5 * 1000, refetchOnWindowFocus: false, refetchOnReconnect: false, retry: false, @@ -230,7 +230,7 @@ export const useUpdatingMemoryStatus = (integrationIds: string[]) => { integrationIds, }, { - refetchOnMount: false, + staleTime: 15 * 1000, refetchOnWindowFocus: false, refetchOnReconnect: false, retry: false, @@ -271,7 +271,7 @@ export const useUpdatingVersionStatus = (integrationIds: string[]) => { integrationIds, }, { - refetchOnMount: false, + staleTime: 60 * 60 * 1000, refetchOnWindowFocus: false, refetchOnReconnect: false, retry: false, @@ -311,7 +311,7 @@ export const useUpdatingInterfacesStatus = (integrationIds: string[]) => { integrationIds, }, { - refetchOnMount: false, + staleTime: 30 * 1000, refetchOnWindowFocus: false, refetchOnReconnect: false, retry: false, diff --git a/packages/widgets/src/health-monitoring/cluster/cluster-health.tsx b/packages/widgets/src/health-monitoring/cluster/cluster-health.tsx index d8fa10614..daa0b40ec 100644 --- a/packages/widgets/src/health-monitoring/cluster/cluster-health.tsx +++ b/packages/widgets/src/health-monitoring/cluster/cluster-health.tsx @@ -40,7 +40,7 @@ export const ClusterHealthMonitoring = ({ integrationId, }, { - refetchOnMount: false, + staleTime: 5 * 1000, refetchOnWindowFocus: false, refetchOnReconnect: false, retry: false, diff --git a/packages/widgets/src/health-monitoring/system-health.tsx b/packages/widgets/src/health-monitoring/system-health.tsx index 9c5260ff0..1f0dd02e5 100644 --- a/packages/widgets/src/health-monitoring/system-health.tsx +++ b/packages/widgets/src/health-monitoring/system-health.tsx @@ -57,7 +57,7 @@ export const SystemHealthMonitoring = ({ integrationIds, }, { - refetchOnMount: false, + staleTime: 5 * 1000, refetchOnWindowFocus: false, refetchOnReconnect: false, retry: false, diff --git a/packages/widgets/src/immich/album-carousel/component.tsx b/packages/widgets/src/immich/album-carousel/component.tsx index 9c157bfaa..94795991c 100644 --- a/packages/widgets/src/immich/album-carousel/component.tsx +++ b/packages/widgets/src/immich/album-carousel/component.tsx @@ -23,7 +23,7 @@ export default function ImmichAlbumCarouselWidget({ integrationId: integrationIds[0] ?? "", albumId: options.albumId ?? "", }, - { enabled: Boolean(options.albumId) }, + { enabled: Boolean(options.albumId), staleTime: 15 * 60 * 1000 }, ); if (!options.albumId) { diff --git a/packages/widgets/src/immich/server-stats/component.tsx b/packages/widgets/src/immich/server-stats/component.tsx index 919b0caf0..e3e1596ea 100644 --- a/packages/widgets/src/immich/server-stats/component.tsx +++ b/packages/widgets/src/immich/server-stats/component.tsx @@ -17,9 +17,12 @@ export default function ImmichServerStatsWidget({ options, }: WidgetComponentProps<"immich-serverStats">) { const t = useI18n(); - const { data: stats } = clientApi.widget.immich.getServerStats.useQuery({ - integrationId: integrationIds[0] ?? "", - }); + const { data: stats } = clientApi.widget.immich.getServerStats.useQuery( + { + integrationId: integrationIds[0] ?? "", + }, + { staleTime: 15 * 60 * 1000 }, + ); if (!stats) return ; diff --git a/packages/widgets/src/indexer-manager/component.tsx b/packages/widgets/src/indexer-manager/component.tsx index ce0e89403..51ddb1805 100644 --- a/packages/widgets/src/indexer-manager/component.tsx +++ b/packages/widgets/src/indexer-manager/component.tsx @@ -21,7 +21,7 @@ export default function IndexerManagerWidget({ const { data: indexersData = [] } = clientApi.widget.indexerManager.getIndexersStatus.useQuery( { integrationIds }, { - refetchOnMount: false, + staleTime: 5 * 60 * 1000, refetchOnWindowFocus: false, refetchOnReconnect: false, retry: false, diff --git a/packages/widgets/src/media-releases/component.tsx b/packages/widgets/src/media-releases/component.tsx index 26fe367a9..61fc9216d 100644 --- a/packages/widgets/src/media-releases/component.tsx +++ b/packages/widgets/src/media-releases/component.tsx @@ -18,9 +18,12 @@ import { OverflowBadge } from "@homarr/ui"; import type { WidgetComponentProps } from "../definition"; export default function MediaReleasesWidget({ options, integrationIds }: WidgetComponentProps<"mediaReleases">) { - const { data: releases = [] } = clientApi.widget.mediaRelease.getMediaReleases.useQuery({ - integrationIds, - }); + const { data: releases = [] } = clientApi.widget.mediaRelease.getMediaReleases.useQuery( + { + integrationIds, + }, + { staleTime: 5 * 60 * 1000 }, + ); return ( diff --git a/packages/widgets/src/media-requests/list/component.tsx b/packages/widgets/src/media-requests/list/component.tsx index 4c60b1dcf..5c990a709 100644 --- a/packages/widgets/src/media-requests/list/component.tsx +++ b/packages/widgets/src/media-requests/list/component.tsx @@ -25,7 +25,7 @@ export default function MediaServerWidget({ integrationIds, }, { - refetchOnMount: false, + staleTime: 60 * 1000, refetchOnWindowFocus: false, refetchOnReconnect: false, }, diff --git a/packages/widgets/src/media-requests/stats/component.tsx b/packages/widgets/src/media-requests/stats/component.tsx index 5a6f425b3..8824fed6a 100644 --- a/packages/widgets/src/media-requests/stats/component.tsx +++ b/packages/widgets/src/media-requests/stats/component.tsx @@ -38,7 +38,7 @@ export default function MediaServerWidget({ integrationIds, }, { - refetchOnMount: false, + staleTime: 60 * 1000, refetchOnWindowFocus: false, refetchOnReconnect: false, }, diff --git a/packages/widgets/src/media-server/component.tsx b/packages/widgets/src/media-server/component.tsx index 138728e57..efa2c2415 100644 --- a/packages/widgets/src/media-server/component.tsx +++ b/packages/widgets/src/media-server/component.tsx @@ -29,7 +29,7 @@ export default function MediaServerWidget({ showOnlyPlaying: options.showOnlyPlaying, }, { - refetchOnMount: false, + staleTime: 5 * 1000, refetchOnWindowFocus: false, refetchOnReconnect: false, }, diff --git a/packages/widgets/src/media-transcoding/component.tsx b/packages/widgets/src/media-transcoding/component.tsx index 494cfd968..8b0ba6591 100644 --- a/packages/widgets/src/media-transcoding/component.tsx +++ b/packages/widgets/src/media-transcoding/component.tsx @@ -39,7 +39,7 @@ export default function MediaTranscodingWidget({ page: queuePage, }; const { data: transcodingData } = clientApi.widget.mediaTranscoding.getDataAsync.useQuery(input, { - refetchOnMount: false, + staleTime: 5 * 60 * 1000, refetchOnWindowFocus: false, refetchOnReconnect: false, }); diff --git a/packages/widgets/src/minecraft/server-status/component.tsx b/packages/widgets/src/minecraft/server-status/component.tsx index c57cd5314..ddf4d6e4b 100644 --- a/packages/widgets/src/minecraft/server-status/component.tsx +++ b/packages/widgets/src/minecraft/server-status/component.tsx @@ -11,7 +11,9 @@ import { WidgetEmptyState } from "../../common/empty-state"; import type { WidgetComponentProps } from "../../definition"; export default function MinecraftServerStatusWidget({ options }: WidgetComponentProps<"minecraftServerStatus">) { - const { data: result } = clientApi.widget.minecraft.getServerStatus.useQuery(options); + const { data: result } = clientApi.widget.minecraft.getServerStatus.useQuery(options, { + staleTime: 5 * 60 * 1000, + }); const utils = clientApi.useUtils(); clientApi.widget.minecraft.subscribeServerStatus.useSubscription(options, { onData(data) { diff --git a/packages/widgets/src/network-controller/network-status/component.tsx b/packages/widgets/src/network-controller/network-status/component.tsx index e0ce57dfb..846d1128f 100644 --- a/packages/widgets/src/network-controller/network-status/component.tsx +++ b/packages/widgets/src/network-controller/network-status/component.tsx @@ -26,7 +26,7 @@ export default function NetworkControllerNetworkStatusWidget({ integrationIds, }, { - refetchOnMount: false, + staleTime: 5 * 60 * 1000, refetchOnWindowFocus: false, refetchOnReconnect: false, retry: false, diff --git a/packages/widgets/src/network-controller/summary/component.tsx b/packages/widgets/src/network-controller/summary/component.tsx index 040b53eca..21a4b6a93 100644 --- a/packages/widgets/src/network-controller/summary/component.tsx +++ b/packages/widgets/src/network-controller/summary/component.tsx @@ -25,7 +25,7 @@ export default function NetworkControllerSummaryWidget({ integrationIds, }, { - refetchOnMount: false, + staleTime: 5 * 60 * 1000, refetchOnWindowFocus: false, refetchOnReconnect: false, retry: false, diff --git a/packages/widgets/src/notifications/component.tsx b/packages/widgets/src/notifications/component.tsx index 4334836b9..47aa829be 100644 --- a/packages/widgets/src/notifications/component.tsx +++ b/packages/widgets/src/notifications/component.tsx @@ -18,7 +18,7 @@ export default function NotificationsWidget({ options, integrationIds }: WidgetC integrationIds, }, { - refetchOnMount: false, + staleTime: 5 * 60 * 1000, refetchOnWindowFocus: false, refetchOnReconnect: false, retry: false, diff --git a/packages/widgets/src/rssFeed/component.tsx b/packages/widgets/src/rssFeed/component.tsx index 49cf807ea..a2cf07ebb 100644 --- a/packages/widgets/src/rssFeed/component.tsx +++ b/packages/widgets/src/rssFeed/component.tsx @@ -13,7 +13,7 @@ import classes from "./component.module.scss"; const useLiveFeedEntries = (input: RouterInputs["widget"]["rssFeed"]["getFeeds"]) => { const { data: feedEntries = [] } = clientApi.widget.rssFeed.getFeeds.useQuery(input, { - refetchOnMount: false, + staleTime: 5 * 60 * 1000, refetchOnWindowFocus: false, refetchOnReconnect: false, retry: false, diff --git a/packages/widgets/src/smart-home/entity-state/component.tsx b/packages/widgets/src/smart-home/entity-state/component.tsx index f796f3261..2ad45b7a3 100644 --- a/packages/widgets/src/smart-home/entity-state/component.tsx +++ b/packages/widgets/src/smart-home/entity-state/component.tsx @@ -23,7 +23,9 @@ export default function SmartHomeEntityStateWidget({ entityId: options.entityId, integrationId, }; - const { data: entityState } = clientApi.widget.smartHome.entityState.useQuery(input); + const { data: entityState } = clientApi.widget.smartHome.entityState.useQuery(input, { + staleTime: 60 * 1000, + }); const utils = clientApi.useUtils(); diff --git a/packages/widgets/src/speedtest-tracker/component.tsx b/packages/widgets/src/speedtest-tracker/component.tsx index b55143084..dcde145a4 100644 --- a/packages/widgets/src/speedtest-tracker/component.tsx +++ b/packages/widgets/src/speedtest-tracker/component.tsx @@ -20,7 +20,10 @@ export default function SpeedtestTrackerWidget({ isEditMode, }: WidgetComponentProps<"speedtestTracker">) { const t = useScopedI18n("widget.speedtestTracker"); - const { data: dashboardData = [] } = clientApi.widget.speedtestTracker.getDashboard.useQuery({ integrationIds }); + const { data: dashboardData = [] } = clientApi.widget.speedtestTracker.getDashboard.useQuery( + { integrationIds }, + { staleTime: 10 * 60 * 1000 }, + ); const utils = clientApi.useUtils(); clientApi.widget.speedtestTracker.subscribeToDashboard.useSubscription( diff --git a/packages/widgets/src/stocks/component.tsx b/packages/widgets/src/stocks/component.tsx index 918851ce8..99078b4d2 100644 --- a/packages/widgets/src/stocks/component.tsx +++ b/packages/widgets/src/stocks/component.tsx @@ -25,7 +25,9 @@ function calculateChangePercentage(currentPrice: number, previousClose: number) export default function StockPriceWidget({ options, width, height }: WidgetComponentProps<"stockPrice">) { const t = useScopedI18n("widget.stockPrice"); const theme = useMantineTheme(); - const { data: result } = clientApi.widget.stockPrice.getPriceHistory.useQuery(options); + const { data: result } = clientApi.widget.stockPrice.getPriceHistory.useQuery(options, { + staleTime: 5 * 60 * 1000, + }); if (!result) return ; const { data } = result; diff --git a/packages/widgets/src/system-disks/component.tsx b/packages/widgets/src/system-disks/component.tsx index 36b20e275..5c6271c09 100644 --- a/packages/widgets/src/system-disks/component.tsx +++ b/packages/widgets/src/system-disks/component.tsx @@ -33,7 +33,9 @@ const getDisplayText = (item: { used: string; available: string; percentage: num export default function SystemResources({ integrationIds, options }: WidgetComponentProps<"systemDisks">) { const queryInput = { integrationIds }; - const { data = [] } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery(queryInput); + const { data = [] } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery(queryInput, { + staleTime: 5 * 1000, + }); const utils = clientApi.useUtils(); const board = useRequiredBoard(); diff --git a/packages/widgets/src/system-resources/component.tsx b/packages/widgets/src/system-resources/component.tsx index d2c5380ad..d9e5a8604 100644 --- a/packages/widgets/src/system-resources/component.tsx +++ b/packages/widgets/src/system-resources/component.tsx @@ -18,9 +18,14 @@ const MAX_QUEUE_SIZE = 15; export default function SystemResources({ integrationIds, options }: WidgetComponentProps<"systemResources">) { const { ref, width } = useElementSize(); - const { data = [] } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery({ - integrationIds, - }); + const { data = [] } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery( + { + integrationIds, + }, + { + staleTime: 5 * 1000, + }, + ); const memoryCapacityInBytes = (data[0]?.healthInfo.memAvailableInBytes ?? 0) + (data[0]?.healthInfo.memUsedInBytes ?? 0); diff --git a/packages/widgets/src/timetable/component.tsx b/packages/widgets/src/timetable/component.tsx index 2f1ad982b..539d7e6c7 100644 --- a/packages/widgets/src/timetable/component.tsx +++ b/packages/widgets/src/timetable/component.tsx @@ -26,11 +26,16 @@ interface TimetableWidgetInnerProps { } const TimetableWidgetInner = ({ station, integrationId }: TimetableWidgetInnerProps) => { - const { data: timetable } = clientApi.widget.timetable.getTimetable.useQuery({ - integrationId, - stationId: station.value, - limit: 10, - }); + const { data: timetable } = clientApi.widget.timetable.getTimetable.useQuery( + { + integrationId, + stationId: station.value, + limit: 10, + }, + { + staleTime: 60 * 1000, + }, + ); const t = useScopedI18n("widget.timetable"); return ( diff --git a/packages/widgets/src/tracearr/component.tsx b/packages/widgets/src/tracearr/component.tsx index 4a54779ee..0ff5e9132 100644 --- a/packages/widgets/src/tracearr/component.tsx +++ b/packages/widgets/src/tracearr/component.tsx @@ -35,7 +35,10 @@ interface TracearrContentProps { function TracearrContent({ integrationIds, options, width, isEditMode }: TracearrContentProps) { const t = useScopedI18n("widget.tracearr"); - const { data: dashboardData = [] } = clientApi.widget.tracearr.getDashboard.useQuery({ integrationIds }); + const { data: dashboardData = [] } = clientApi.widget.tracearr.getDashboard.useQuery( + { integrationIds }, + { staleTime: 5 * 1000 }, + ); const utils = clientApi.useUtils(); clientApi.widget.tracearr.subscribeToDashboard.useSubscription( diff --git a/packages/widgets/src/umami/umami-content.tsx b/packages/widgets/src/umami/umami-content.tsx index e6dd90a8f..2f8dea22c 100644 --- a/packages/widgets/src/umami/umami-content.tsx +++ b/packages/widgets/src/umami/umami-content.tsx @@ -40,15 +40,20 @@ export function UmamiContent({ const { colorScheme } = useMantineColorScheme(); const tickColor = colorScheme === "dark" ? "#c1c2c5" : "#495057"; - const { data: results = [] } = clientApi.widget.umami.getVisitorStats.useQuery({ - integrationIds, - websiteId, - timeFrame, - eventName, - }); + const { data: results = [] } = clientApi.widget.umami.getVisitorStats.useQuery( + { + integrationIds, + websiteId, + timeFrame, + eventName, + }, + { staleTime: 5 * 60 * 1000 }, + ); const activeVisitorsInput = { integrationId: integrationIds[0] ?? "", websiteId }; - const { data: activeVisitors = 0 } = clientApi.widget.umami.getActiveVisitors.useQuery(activeVisitorsInput); + const { data: activeVisitors = 0 } = clientApi.widget.umami.getActiveVisitors.useQuery(activeVisitorsInput, { + staleTime: 30 * 1000, + }); const utils = clientApi.useUtils(); clientApi.widget.umami.subscribeActiveVisitors.useSubscription(activeVisitorsInput, { onData(count) { @@ -58,7 +63,7 @@ export function UmamiContent({ const { data: multiEventSeries } = clientApi.widget.umami.getMultiEventTimeSeries.useQuery( { integrationId: integrationIds[0] ?? "", websiteId, timeFrame, eventNames: [...eventNames].toSorted() }, - { enabled: viewMode === "events" && eventNames.length > 0 }, + { enabled: viewMode === "events" && eventNames.length > 0, staleTime: 5 * 60 * 1000 }, ); const multiEventTotal = multiEventSeries diff --git a/packages/widgets/src/umami/umami-events-content.tsx b/packages/widgets/src/umami/umami-events-content.tsx index 6dfac617c..0351e906c 100644 --- a/packages/widgets/src/umami/umami-events-content.tsx +++ b/packages/widgets/src/umami/umami-events-content.tsx @@ -28,12 +28,15 @@ export function UmamiEventsContent({ const { colorScheme } = useMantineColorScheme(); const tickColor = colorScheme === "dark" ? "#c1c2c5" : "#495057"; - const { data: series = [] } = clientApi.widget.umami.getMultiEventTimeSeries.useQuery({ - integrationId: integrationIds[0] ?? "", - websiteId, - timeFrame, - eventNames: [...eventNames].toSorted(), - }); + const { data: series = [] } = clientApi.widget.umami.getMultiEventTimeSeries.useQuery( + { + integrationId: integrationIds[0] ?? "", + websiteId, + timeFrame, + eventNames: [...eventNames].toSorted(), + }, + { staleTime: 5 * 60 * 1000 }, + ); if (eventNames.length === 0) { return ( diff --git a/packages/widgets/src/umami/umami-top-list.tsx b/packages/widgets/src/umami/umami-top-list.tsx index 699df9b74..125115f0c 100644 --- a/packages/widgets/src/umami/umami-top-list.tsx +++ b/packages/widgets/src/umami/umami-top-list.tsx @@ -15,23 +15,29 @@ interface UmamiTopListProps { export function UmamiTopPagesContent({ integrationIds, websiteId, timeFrame, limit }: UmamiTopListProps) { const t = useScopedI18n("widget.umami"); - const { data = [] } = clientApi.widget.umami.getTopPages.useQuery({ - integrationId: integrationIds[0] ?? "", - websiteId, - timeFrame, - limit, - }); + const { data = [] } = clientApi.widget.umami.getTopPages.useQuery( + { + integrationId: integrationIds[0] ?? "", + websiteId, + timeFrame, + limit, + }, + { staleTime: 5 * 60 * 1000 }, + ); return ; } export function UmamiTopReferrersContent({ integrationIds, websiteId, timeFrame, limit }: UmamiTopListProps) { const t = useScopedI18n("widget.umami"); - const { data = [] } = clientApi.widget.umami.getTopReferrers.useQuery({ - integrationId: integrationIds[0] ?? "", - websiteId, - timeFrame, - limit, - }); + const { data = [] } = clientApi.widget.umami.getTopReferrers.useQuery( + { + integrationId: integrationIds[0] ?? "", + websiteId, + timeFrame, + limit, + }, + { staleTime: 5 * 60 * 1000 }, + ); return (