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
42 changed files with 47 additions and 67 deletions

No files matched your search

@@ -93,11 +93,10 @@ 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 def = definition as { refetchIntervalSeconds?: number; queryKey?: QueryKey; kind: string };
if (!def.refetchIntervalSeconds) continue;
const key = def.queryKey ?? [["widget", def.kind]];
const interval = def.refetchInterval === false ? false : def.refetchInterval * 1000;
client.setQueryDefaults(key, { refetchInterval: interval });
client.setQueryDefaults(key, { refetchInterval: def.refetchIntervalSeconds * 1000 });
}
return client;
});
@@ -5,6 +5,8 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("anchorNote", {
icon: IconNotes,
queryKey: [["widget", "anchorNotes"]],
refetchIntervalSeconds: 15,
supportedIntegrations: ["anchor"],
createOptions() {
return optionsBuilder.from((factory) => ({
@@ -5,6 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("archiveTeamWarrior", {
icon: IconArchive,
refetchIntervalSeconds: 300,
createOptions() {
return optionsBuilder.from((factory) => ({
showBroadcastMessage: factory.switch({
@@ -14,6 +14,7 @@ const hideUnlessAudiobookshelf = {
export const { definition, componentLoader } = createWidgetDefinition("audioStats", {
icon: IconHeadphones,
refetchIntervalSeconds: 600,
supportedIntegrations: ["navidrome", "audiobookshelf"],
integrationsRequired: true,
maxIntegrations: 1,
+1
View File
@@ -9,6 +9,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("calendar", {
icon: IconCalendar,
refetchIntervalSeconds: 60,
createOptions() {
return optionsBuilder.from((factory) => ({
releaseType: factory.multiSelect({
+1
View File
@@ -5,6 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("coolify", {
icon: IconCloud,
refetchIntervalSeconds: 30,
createOptions() {
return optionsBuilder.from((factory) => ({
showServers: factory.switch({
+1 -8
View File
@@ -67,17 +67,10 @@ export const createWidgetDefinition = <TKind extends WidgetKind, TDefinition ext
withDynamicImport: createWithDynamicImport(kind, definition),
});
export const widgetRefetchInterval = {
fiveSeconds: 5,
never: false,
} as const;
export type WidgetRefetchInterval = (typeof widgetRefetchInterval)[keyof typeof widgetRefetchInterval];
export interface WidgetDefinition {
icon: TablerIcon;
queryKey?: QueryKey;
refetchInterval?: WidgetRefetchInterval;
refetchIntervalSeconds?: number;
supportedIntegrations?: IntegrationKind[];
integrationsRequired?: boolean;
maxIntegrations?: number;
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../../definition";
import { IconDeviceGamepad, IconServerOff } from "@tabler/icons-react";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
@@ -11,7 +10,7 @@ export const widgetKind = "dnsHoleControls";
export const { definition, componentLoader } = createWidgetDefinition(widgetKind, {
icon: IconDeviceGamepad,
queryKey: [["widget", "dnsHole"]],
refetchInterval: widgetRefetchInterval.fiveSeconds,
refetchIntervalSeconds: 5,
createOptions() {
return optionsBuilder.from((factory) => ({
showToggleAllButtons: factory.switch({
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../../definition";
import { IconAd, IconServerOff } from "@tabler/icons-react";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
@@ -11,7 +10,7 @@ export const widgetKind = "dnsHoleSummary";
export const { definition, componentLoader } = createWidgetDefinition(widgetKind, {
icon: IconAd,
queryKey: [["widget", "dnsHole"]],
refetchInterval: widgetRefetchInterval.fiveSeconds,
refetchIntervalSeconds: 5,
createOptions() {
return optionsBuilder.from((factory) => ({
usePiHoleColors: factory.switch({
+1 -1
View File
@@ -184,7 +184,7 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo
const t = useScopedI18n("docker");
const isTiny = width <= 256;
const { data, refetch, isFetching } = clientApi.docker.getContainers.useQuery();
const { data, refetch, isFetching } = clientApi.docker.getContainers.useQuery(undefined, {});
const containers = data?.containers ?? [];
const timestamp = useMemo(() => data?.timestamp ?? new Date(), [data?.timestamp]);
const relativeTime = useTimeAgo(timestamp);
+1
View File
@@ -26,6 +26,7 @@ const columnTranslationKeyMap = {
export const { definition, componentLoader } = createWidgetDefinition("dockerContainers", {
icon: IconBrandDocker,
queryKey: [["docker", "getContainers"]],
refetchIntervalSeconds: 20,
createOptions() {
return optionsBuilder.from((factory) => ({
columns: factory.multiSelect({
+1 -2
View File
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../definition";
import { IconDownload } from "@tabler/icons-react";
import { z } from "zod/v4";
@@ -34,7 +33,7 @@ const columnsSort = columnsList.filter((column) =>
export const { definition, componentLoader } = createWidgetDefinition("downloads", {
icon: IconDownload,
refetchInterval: widgetRefetchInterval.fiveSeconds,
refetchIntervalSeconds: 5,
createOptions() {
return optionsBuilder.from(
(factory) => ({
+1 -2
View File
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../definition";
import { IconWall, IconWallOff } from "@tabler/icons-react";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
@@ -8,7 +7,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("firewall", {
icon: IconWall,
refetchInterval: widgetRefetchInterval.fiveSeconds,
refetchIntervalSeconds: 5,
createOptions() {
return optionsBuilder.from(() => ({}));
},
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../definition";
import { IconHeartRateMonitor, IconServerOff } from "@tabler/icons-react";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
@@ -9,7 +8,7 @@ import { createStorageVolumeMultiSelectOptions } from "../storage-volume-options
export const { definition, componentLoader } = createWidgetDefinition("healthMonitoring", {
icon: IconHeartRateMonitor,
refetchInterval: widgetRefetchInterval.fiveSeconds,
refetchIntervalSeconds: 5,
createOptions() {
return optionsBuilder.from(
(factory) => ({
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../../definition";
import { IconPhoto } from "@tabler/icons-react";
import z from "zod";
@@ -10,7 +9,7 @@ import { optionsBuilder } from "../../options";
export const { definition, componentLoader } = createWidgetDefinition("immich-albumCarousel", {
icon: IconPhoto,
queryKey: [["widget", "immich"]],
refetchInterval: widgetRefetchInterval.never,
refetchIntervalSeconds: 900,
supportedIntegrations: ["immich"],
integrationsRequired: true,
createOptions() {
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../../definition";
import { IconGraphFilled } from "@tabler/icons-react";
import { createWidgetDefinition } from "../../definition";
@@ -7,7 +6,7 @@ import { optionsBuilder } from "../../options";
export const { definition, componentLoader } = createWidgetDefinition("immich-serverStats", {
icon: IconGraphFilled,
queryKey: [["widget", "immich"]],
refetchInterval: widgetRefetchInterval.never,
refetchIntervalSeconds: 900,
supportedIntegrations: ["immich"],
integrationsRequired: true,
createOptions() {
-2
View File
@@ -70,9 +70,7 @@ export type {
WidgetContextMenuAction,
WidgetContextActionProps,
WidgetOptionsSettings,
WidgetRefetchInterval,
} from "./definition";
export { widgetRefetchInterval } from "./definition";
export type { WidgetComponentProps };
export type { WidgetOptionDefinition, WidgetOptionType } from "./options";
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../definition";
import { IconReportSearch, IconServerOff } from "@tabler/icons-react";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
@@ -8,7 +7,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("indexerManager", {
icon: IconReportSearch,
refetchInterval: widgetRefetchInterval.never,
refetchIntervalSeconds: 300,
createOptions() {
return optionsBuilder.from((factory) => ({
openIndexerSiteInNewTab: factory.switch({
+1 -2
View File
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../definition";
import { IconTicket } from "@tabler/icons-react";
import { createWidgetDefinition } from "../definition";
@@ -7,7 +6,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("mediaReleases", {
icon: IconTicket,
queryKey: [["widget", "mediaRelease"]],
refetchInterval: widgetRefetchInterval.never,
refetchIntervalSeconds: 300,
createOptions() {
return optionsBuilder.from((factory) => ({
layout: factory.select({
@@ -8,6 +8,8 @@ import { optionsBuilder } from "../../options";
export const { componentLoader, definition } = createWidgetDefinition("mediaRequests-requestList", {
icon: IconZoomQuestion,
queryKey: [["widget", "mediaRequests"]],
refetchIntervalSeconds: 60,
createOptions() {
return optionsBuilder.from((factory) => ({
linksTargetNewTab: factory.switch({
@@ -7,6 +7,8 @@ import { createWidgetDefinition } from "../../definition";
export const { componentLoader, definition } = createWidgetDefinition("mediaRequests-requestStats", {
icon: IconChartBar,
queryKey: [["widget", "mediaRequests"]],
refetchIntervalSeconds: 60,
createOptions() {
return {};
},
+1 -2
View File
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../definition";
import { IconVideo } from "@tabler/icons-react";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
@@ -8,7 +7,7 @@ import { optionsBuilder } from "../options";
export const { componentLoader, definition } = createWidgetDefinition("mediaServer", {
icon: IconVideo,
refetchInterval: widgetRefetchInterval.fiveSeconds,
refetchIntervalSeconds: 5,
createOptions() {
return optionsBuilder.from((factory) => ({
showOnlyPlaying: factory.switch({ defaultValue: true, withDescription: true }),
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../definition";
import { IconTransform } from "@tabler/icons-react";
import { z } from "zod/v4";
@@ -12,7 +11,7 @@ export const views = ["workers", "queue", "statistics"] as const;
export const { componentLoader, definition } = createWidgetDefinition("mediaTranscoding", {
icon: IconTransform,
refetchInterval: widgetRefetchInterval.never,
refetchIntervalSeconds: 300,
createOptions() {
return optionsBuilder.from((factory) => ({
defaultView: factory.select({
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../../definition";
import { IconBrandMinecraft } from "@tabler/icons-react";
import { z } from "zod/v4";
@@ -7,8 +6,7 @@ import { optionsBuilder } from "../../options";
export const { componentLoader, definition } = createWidgetDefinition("minecraftServerStatus", {
icon: IconBrandMinecraft,
queryKey: [["widget", "minecraft"]],
refetchInterval: widgetRefetchInterval.never,
refetchIntervalSeconds: 300,
createOptions() {
return optionsBuilder.from((factory) => ({
title: factory.text({ defaultValue: "" }),
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../../definition";
import { IconServerOff, IconTopologyFull } from "@tabler/icons-react";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
@@ -9,7 +8,7 @@ import { optionsBuilder } from "../../options";
export const { definition, componentLoader } = createWidgetDefinition("networkControllerStatus", {
icon: IconTopologyFull,
queryKey: [["widget", "networkController"]],
refetchInterval: widgetRefetchInterval.never,
refetchIntervalSeconds: 300,
createOptions() {
return optionsBuilder.from((factory) => ({
content: factory.select({
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../../definition";
import { IconServerOff, IconTopologyFull } from "@tabler/icons-react";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
@@ -8,8 +7,7 @@ import { optionsBuilder } from "../../options";
export const { definition, componentLoader } = createWidgetDefinition("networkControllerSummary", {
icon: IconTopologyFull,
queryKey: [["widget", "networkController"]],
refetchInterval: widgetRefetchInterval.never,
refetchIntervalSeconds: 300,
createOptions() {
return optionsBuilder.from(() => ({}));
},
+1 -2
View File
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../definition";
import { IconMessage } from "@tabler/icons-react";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
@@ -8,7 +7,7 @@ import { optionsBuilder } from "../options";
export const { componentLoader, definition } = createWidgetDefinition("notifications", {
icon: IconMessage,
refetchInterval: widgetRefetchInterval.never,
refetchIntervalSeconds: 300,
createOptions() {
return optionsBuilder.from((factory) => ({
hideLogos: factory.switch({ defaultValue: false }),
+1 -2
View File
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../definition";
import { IconFileText, IconServerOff } from "@tabler/icons-react";
import { createWidgetDefinition } from "../definition";
@@ -6,7 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("paperlessNgx", {
icon: IconFileText,
refetchInterval: widgetRefetchInterval.never,
refetchIntervalSeconds: 900,
supportedIntegrations: ["paperlessNgx"],
integrationsRequired: true,
createOptions() {
+1 -2
View File
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../definition";
import { IconRocket } from "@tabler/icons-react";
import { z } from "zod/v4";
@@ -15,7 +14,7 @@ const relativeDateSchema = z
export const { definition, componentLoader } = createWidgetDefinition("releases", {
icon: IconRocket,
refetchInterval: widgetRefetchInterval.never,
refetchIntervalSeconds: 300,
createOptions() {
return optionsBuilder.from((factory) => ({
newReleaseWithin: factory.text({
+1 -2
View File
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../definition";
import { IconRss } from "@tabler/icons-react";
import { z } from "zod/v4";
@@ -13,7 +12,7 @@ import { optionsBuilder } from "../options";
*/
export const { definition, componentLoader } = createWidgetDefinition("rssFeed", {
icon: IconRss,
refetchInterval: widgetRefetchInterval.never,
refetchIntervalSeconds: 300,
createOptions() {
return optionsBuilder.from((factory) => ({
feedUrls: factory.multiText({
@@ -7,6 +7,8 @@ import { optionsBuilder } from "../../options";
export const { definition, componentLoader } = createWidgetDefinition("smartHome-entityState", {
icon: IconBinaryTree,
queryKey: [["widget", "smartHome"]],
refetchIntervalSeconds: 60,
createOptions() {
return optionsBuilder.from((factory) => ({
entityId: factory.text({
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../definition";
import { IconServerOff, IconSpeedboat } from "@tabler/icons-react";
import { createWidgetDefinition } from "../definition";
@@ -6,7 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("speedtestTracker", {
icon: IconSpeedboat,
refetchInterval: widgetRefetchInterval.never,
refetchIntervalSeconds: 600,
createOptions() {
return optionsBuilder.from(
(factory) => ({
+1 -2
View File
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../definition";
import { IconBuildingBank } from "@tabler/icons-react";
import { createWidgetDefinition } from "../definition";
@@ -14,7 +13,7 @@ const timeIntervalOptions = stockPriceTimeFrames.interval;
export const { definition, componentLoader } = createWidgetDefinition("stockPrice", {
icon: IconBuildingBank,
refetchInterval: widgetRefetchInterval.never,
refetchIntervalSeconds: 300,
createOptions() {
return optionsBuilder.from((factory) => ({
stock: factory.text({
+1 -2
View File
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../definition";
import { IconServer2 } from "@tabler/icons-react";
import { createWidgetDefinition } from "../definition";
@@ -8,7 +7,7 @@ import { createStorageVolumeMultiSelectOptions } from "../storage-volume-options
export const { definition, componentLoader } = createWidgetDefinition("systemDisks", {
icon: IconServer2,
queryKey: [["widget", "healthMonitoring"]],
refetchInterval: widgetRefetchInterval.fiveSeconds,
refetchIntervalSeconds: 5,
supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "synology"],
createOptions() {
return optionsBuilder.from(
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../definition";
import { IconAlignLeft, IconEyeOff, IconGraphFilled, IconListDetails, IconPhoto } from "@tabler/icons-react";
import { objectEntries } from "@homarr/common";
@@ -16,7 +15,7 @@ const labelDisplayModeOptions = {
export const { definition, componentLoader } = createWidgetDefinition("systemResources", {
icon: IconGraphFilled,
queryKey: [["widget", "healthMonitoring"]],
refetchInterval: widgetRefetchInterval.fiveSeconds,
refetchIntervalSeconds: 5,
supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "glances", "synology"],
createOptions() {
return optionsBuilder.from((factory) => ({
+1
View File
@@ -8,6 +8,7 @@ import { optionsBuilder } from "../options";
export const { componentLoader, definition } = createWidgetDefinition("timetable", {
icon: IconBusStop,
refetchIntervalSeconds: 60,
createOptions() {
return optionsBuilder.from((factory) => ({
baseUrl: factory.text({
+1 -2
View File
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../definition";
import { IconActivityHeartbeat, IconServerOff } from "@tabler/icons-react";
import { createWidgetDefinition } from "../definition";
@@ -6,7 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("tracearr", {
icon: IconActivityHeartbeat,
refetchInterval: widgetRefetchInterval.fiveSeconds,
refetchIntervalSeconds: 5,
createOptions() {
return optionsBuilder.from((factory) => ({
showStreams: factory.switch({
+1 -2
View File
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../definition";
import { IconChartBar, IconServerOff } from "@tabler/icons-react";
import { z } from "zod/v4";
@@ -9,7 +8,7 @@ export const timeFrameValues = ["today", "24h", "7d", "30d", "month", "lastMonth
export const { definition, componentLoader } = createWidgetDefinition("umami", {
icon: IconChartBar,
refetchInterval: widgetRefetchInterval.never,
refetchIntervalSeconds: 300,
supportedIntegrations: ["umami"],
createOptions() {
return optionsBuilder.from(
+1
View File
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("ups", {
icon: IconBatteryCharging,
refetchIntervalSeconds: 60,
createOptions() {
return optionsBuilder.from((factory) => ({
showBattery: factory.switch({
@@ -5,6 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("uptimeKuma", {
icon: IconHeartbeat,
refetchIntervalSeconds: 60,
createOptions() {
return optionsBuilder.from((factory) => ({
showAverageUptime: factory.switch({
+1 -2
View File
@@ -1,4 +1,3 @@
import { widgetRefetchInterval } from "../definition";
import { IconShieldLock } from "@tabler/icons-react";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
@@ -8,7 +7,7 @@ import { optionsBuilder } from "../options";
export const { componentLoader, definition } = createWidgetDefinition("vpn", {
icon: IconShieldLock,
refetchInterval: widgetRefetchInterval.never,
refetchIntervalSeconds: 300,
createOptions() {
return optionsBuilder.from(() => ({}));
},
+1
View File
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("weather", {
icon: IconCloud,
refetchIntervalSeconds: 60,
createOptions() {
return optionsBuilder.from(
(factory) => ({