From 781378dc7667538d7e2825b31e440d6b80c91c67 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Wed, 1 Jul 2026 15:36:00 +0200 Subject: [PATCH] fix: address PR review findings - WidgetCacheAge reads from query cache on every tick (reactive) - Guard empty timestamps array (no -Infinity edge case) - Docker widget: add explicit refetchInterval (not under [["widget"]]) - system-resources: split integrationIds reset into separate effect - Remove unused React import in vpn widget --- .../board/items/widget-context-menu.tsx | 30 +++++++++---------- packages/widgets/src/docker/component.tsx | 4 ++- .../src/system-resources/component.tsx | 18 +++++------ packages/widgets/src/vpn/component.tsx | 1 - 4 files changed, 27 insertions(+), 26 deletions(-) diff --git a/apps/nextjs/src/components/board/items/widget-context-menu.tsx b/apps/nextjs/src/components/board/items/widget-context-menu.tsx index 46fe0fe4a..d7f4bdc06 100644 --- a/apps/nextjs/src/components/board/items/widget-context-menu.tsx +++ b/apps/nextjs/src/components/board/items/widget-context-menu.tsx @@ -4,6 +4,7 @@ import type { MutableRefObject, ReactNode } from "react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Group, Menu, Switch, Text } from "@mantine/core"; import { IconCopy, IconLayoutKanban, IconRefresh, IconSettings, IconTrash } from "@tabler/icons-react"; +import type { QueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query"; import { clientApi } from "@homarr/api/client"; @@ -52,12 +53,6 @@ export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetCont void queryClient.invalidateQueries({ queryKey: [["widget"]] }); }, [queryClient]); - const dataUpdatedAt = useMemo(() => { - const queries = queryClient.getQueryCache().findAll({ queryKey: [["widget"]] }); - if (queries.length === 0) return null; - return Math.max(...queries.map((q) => q.state.dataUpdatedAt).filter(Boolean)); - }, [queryClient]); - const options = useMemo( () => reduceWidgetOptionsWithDefaultValues(item.kind, settings, item.options) as Record, [item.kind, settings, item.options], @@ -230,11 +225,9 @@ export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetCont } onClick={handleRefetch}> {tMenu("refresh")} - {dataUpdatedAt && ( - - - - )} + + + { +const WidgetCacheAge = ({ queryClient }: { queryClient: QueryClient }) => { const [, setTick] = useState(0); useEffect(() => { const id = setInterval(() => setTick((n) => n + 1), 1000); return () => clearInterval(id); }, []); - const seconds = Math.floor((Date.now() - timestamp) / 1000); + + const timestamps = queryClient + .getQueryCache() + .findAll({ queryKey: [["widget"]] }) + .map((q) => q.state.dataUpdatedAt) + .filter(Boolean); + if (timestamps.length === 0) return null; + + const seconds = Math.floor((Date.now() - Math.max(...timestamps)) / 1000); if (seconds < 5) return "just now"; if (seconds < 60) return `${seconds}s ago`; - const minutes = Math.floor(seconds / 60); - return `${minutes}m ago`; + return `${Math.floor(seconds / 60)}m ago`; }; diff --git a/packages/widgets/src/docker/component.tsx b/packages/widgets/src/docker/component.tsx index 57875cd65..42db221a2 100644 --- a/packages/widgets/src/docker/component.tsx +++ b/packages/widgets/src/docker/component.tsx @@ -184,7 +184,9 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo const t = useScopedI18n("docker"); const isTiny = width <= 256; - const { data } = clientApi.docker.getContainers.useQuery(undefined); + const { data } = clientApi.docker.getContainers.useQuery(undefined, { + refetchInterval: 30_000, + }); const containers = data?.containers ?? []; const timestamp = useMemo(() => data?.timestamp ?? new Date(), [data?.timestamp]); const relativeTime = useTimeAgo(timestamp); diff --git a/packages/widgets/src/system-resources/component.tsx b/packages/widgets/src/system-resources/component.tsx index f4e80255f..1d3854c89 100644 --- a/packages/widgets/src/system-resources/component.tsx +++ b/packages/widgets/src/system-resources/component.tsx @@ -43,21 +43,21 @@ export default function SystemResources({ integrationIds, options }: WidgetCompo { cpu: number; memory: number; gpu: number; network: { up: number; down: number } | null }[] >(() => (data[0] ? [toChartItem(data[0].healthInfo)] : [])); - const lastUpdatedAt = useRef(dataUpdatedAt); const prevIntegrationIds = useRef(integrationIds); + useEffect(() => { + if (prevIntegrationIds.current === integrationIds) return; + prevIntegrationIds.current = integrationIds; + setItems([]); + }, [integrationIds]); + + const lastUpdatedAt = useRef(dataUpdatedAt); useEffect(() => { if (dataUpdatedAt === lastUpdatedAt.current) return; lastUpdatedAt.current = dataUpdatedAt; const firstItem = data[0]; if (!firstItem) return; - const idsChanged = prevIntegrationIds.current !== integrationIds; - prevIntegrationIds.current = integrationIds; - setItems((prev) => - idsChanged - ? [toChartItem(firstItem.healthInfo)] - : [...prev, toChartItem(firstItem.healthInfo)].slice(-MAX_QUEUE_SIZE), - ); - }, [dataUpdatedAt, data, integrationIds]); + setItems((prev) => [...prev, toChartItem(firstItem.healthInfo)].slice(-MAX_QUEUE_SIZE)); + }, [dataUpdatedAt, data]); const showNetwork = items.length === 0 || (items.every((item) => item.network !== null) && options.visibleCharts.includes("network")); diff --git a/packages/widgets/src/vpn/component.tsx b/packages/widgets/src/vpn/component.tsx index 20ea3f5ed..79ed5d695 100644 --- a/packages/widgets/src/vpn/component.tsx +++ b/packages/widgets/src/vpn/component.tsx @@ -1,6 +1,5 @@ "use client"; -import React from "react"; import { Flex, ScrollArea, Stack } from "@mantine/core"; import { clientApi } from "@homarr/api/client";