From e9611e8098efdec500fd9e2a9e81df29ef89cb05 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Sat, 27 Jun 2026 11:49:15 +0200 Subject: [PATCH] chore: temporarily restore deleted files for CodeRabbit review Restoring 27 deleted files and 26 trivially-changed request handlers to bring the PR under CodeRabbit's 150-file review limit. These will be re-deleted in the next commit. --- .../invalidate-update-checker-cache.ts | 18 ++ packages/cron-jobs/src/jobs/docker.ts | 31 ++++ .../src/jobs/integrations/dns-hole.ts | 15 ++ .../src/jobs/integrations/downloads.ts | 16 ++ .../src/jobs/integrations/firewall.ts | 46 +++++ .../jobs/integrations/health-monitoring.ts | 33 ++++ .../src/jobs/integrations/home-assistant.ts | 16 ++ .../src/jobs/integrations/indexer-manager.ts | 14 ++ .../src/jobs/integrations/media-organizer.ts | 36 ++++ .../src/jobs/integrations/media-requests.ts | 24 +++ .../src/jobs/integrations/media-server.ts | 14 ++ .../jobs/integrations/media-transcoding.ts | 14 ++ .../jobs/integrations/network-controller.ts | 14 ++ .../src/jobs/integrations/notifications.ts | 14 ++ .../src/jobs/integrations/timetable.ts | 20 +++ .../src/jobs/integrations/tracearr.ts | 14 ++ .../cron-jobs/src/jobs/integrations/umami.ts | 14 ++ .../cron-jobs/src/jobs/integrations/vpn.ts | 14 ++ .../src/jobs/minecraft-server-status.ts | 25 +++ packages/cron-jobs/src/jobs/rss-feeds.ts | 39 +++++ packages/cron-jobs/src/jobs/update-checker.ts | 11 ++ packages/cron-jobs/src/jobs/weather.ts | 36 ++++ .../redis/src/lib/channels/history-channel.ts | 6 + .../src/archive-team-warrior.ts | 8 +- .../request-handler/src/audiobookshelf.ts | 8 +- packages/request-handler/src/calendar.ts | 6 +- packages/request-handler/src/coolify.ts | 8 +- packages/request-handler/src/dns-hole.ts | 8 +- packages/request-handler/src/downloads.ts | 8 +- .../request-handler/src/indexer-manager.ts | 8 +- .../lib/cached-integration-request-handler.ts | 49 ++++++ .../src/lib/cached-request-handler.ts | 160 ++++++++++++++++++ .../cached-request-integration-job-handler.ts | 115 +++++++++++++ .../src/lib/cached-widget-request-handler.ts | 36 ++++ packages/request-handler/src/media-release.ts | 8 +- .../request-handler/src/media-request-list.ts | 8 +- .../src/media-request-stats.ts | 8 +- packages/request-handler/src/media-server.ts | 8 +- .../request-handler/src/media-transcoding.ts | 8 +- .../src/minecraft-server-status.ts | 8 +- packages/request-handler/src/navidrome.ts | 8 +- .../request-handler/src/network-controller.ts | 8 +- packages/request-handler/src/notifications.ts | 8 +- packages/request-handler/src/paperless-ngx.ts | 8 +- packages/request-handler/src/releases.ts | 8 +- packages/request-handler/src/rss-feeds.ts | 8 +- .../src/smart-home-entity-state.ts | 8 +- .../request-handler/src/speedtest-tracker.ts | 8 +- packages/request-handler/src/stock-price.ts | 8 +- packages/request-handler/src/tracearr.ts | 8 +- packages/request-handler/src/ups.ts | 8 +- packages/request-handler/src/uptime-kuma.ts | 8 +- packages/request-handler/src/weather.ts | 8 +- 53 files changed, 998 insertions(+), 52 deletions(-) create mode 100644 apps/tasks/src/on-start/invalidate-update-checker-cache.ts create mode 100644 packages/cron-jobs/src/jobs/docker.ts create mode 100644 packages/cron-jobs/src/jobs/integrations/dns-hole.ts create mode 100644 packages/cron-jobs/src/jobs/integrations/downloads.ts create mode 100644 packages/cron-jobs/src/jobs/integrations/firewall.ts create mode 100644 packages/cron-jobs/src/jobs/integrations/health-monitoring.ts create mode 100644 packages/cron-jobs/src/jobs/integrations/home-assistant.ts create mode 100644 packages/cron-jobs/src/jobs/integrations/indexer-manager.ts create mode 100644 packages/cron-jobs/src/jobs/integrations/media-organizer.ts create mode 100644 packages/cron-jobs/src/jobs/integrations/media-requests.ts create mode 100644 packages/cron-jobs/src/jobs/integrations/media-server.ts create mode 100644 packages/cron-jobs/src/jobs/integrations/media-transcoding.ts create mode 100644 packages/cron-jobs/src/jobs/integrations/network-controller.ts create mode 100644 packages/cron-jobs/src/jobs/integrations/notifications.ts create mode 100644 packages/cron-jobs/src/jobs/integrations/timetable.ts create mode 100644 packages/cron-jobs/src/jobs/integrations/tracearr.ts create mode 100644 packages/cron-jobs/src/jobs/integrations/umami.ts create mode 100644 packages/cron-jobs/src/jobs/integrations/vpn.ts create mode 100644 packages/cron-jobs/src/jobs/minecraft-server-status.ts create mode 100644 packages/cron-jobs/src/jobs/rss-feeds.ts create mode 100644 packages/cron-jobs/src/jobs/update-checker.ts create mode 100644 packages/cron-jobs/src/jobs/weather.ts create mode 100644 packages/redis/src/lib/channels/history-channel.ts create mode 100644 packages/request-handler/src/lib/cached-integration-request-handler.ts create mode 100644 packages/request-handler/src/lib/cached-request-handler.ts create mode 100644 packages/request-handler/src/lib/cached-request-integration-job-handler.ts create mode 100644 packages/request-handler/src/lib/cached-widget-request-handler.ts diff --git a/apps/tasks/src/on-start/invalidate-update-checker-cache.ts b/apps/tasks/src/on-start/invalidate-update-checker-cache.ts new file mode 100644 index 000000000..470989dcf --- /dev/null +++ b/apps/tasks/src/on-start/invalidate-update-checker-cache.ts @@ -0,0 +1,18 @@ +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker"; + +const logger = createLogger({ module: "invalidateUpdateCheckerCache" }); + +/** + * Invalidates the update checker cache on startup to ensure fresh data. + * It is important as we want to avoid showing pending updates after the update to latest version. + */ +export async function invalidateUpdateCheckerCacheAsync() { + try { + const handler = updateCheckerRequestHandler.handler({}); + await handler.invalidateAsync(); + logger.debug("Update checker cache invalidated"); + } catch (error) { + logger.error(new Error("Failed to invalidate update checker cache", { cause: error })); + } +} diff --git a/packages/cron-jobs/src/jobs/docker.ts b/packages/cron-jobs/src/jobs/docker.ts new file mode 100644 index 000000000..963f395bf --- /dev/null +++ b/packages/cron-jobs/src/jobs/docker.ts @@ -0,0 +1,31 @@ +import SuperJSON from "superjson"; + +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; +import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; +import { db, eq } from "@homarr/db"; +import { items } from "@homarr/db/schema"; +import { dockerContainersRequestHandler } from "@homarr/request-handler/docker"; + +import type { WidgetComponentProps } from "../../../widgets"; +import { createCronJob } from "../lib"; + +const logger = createLogger({ module: "dockerJobs" }); + +export const dockerContainersJob = createCronJob("dockerContainers", EVERY_MINUTE).withCallback(async () => { + const dockerItems = await db.query.items.findMany({ + where: eq(items.kind, "dockerContainers"), + }); + + await Promise.allSettled( + dockerItems.map(async (item) => { + try { + const options = SuperJSON.parse["options"]>(item.options); + const innerHandler = dockerContainersRequestHandler.handler(options); + await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true }); + } catch (error) { + logger.error(new ErrorWithMetadata("Failed to update Docker container status", { item }, { cause: error })); + } + }), + ); +}); diff --git a/packages/cron-jobs/src/jobs/integrations/dns-hole.ts b/packages/cron-jobs/src/jobs/integrations/dns-hole.ts new file mode 100644 index 000000000..a78b28e08 --- /dev/null +++ b/packages/cron-jobs/src/jobs/integrations/dns-hole.ts @@ -0,0 +1,15 @@ +import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; +import { dnsHoleRequestHandler } from "@homarr/request-handler/dns-hole"; +import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; + +import { createCronJob } from "../../lib"; + +export const dnsHoleJob = createCronJob("dnsHole", EVERY_MINUTE).withCallback( + createRequestIntegrationJobHandler(dnsHoleRequestHandler.handler, { + widgetKinds: ["dnsHoleSummary", "dnsHoleControls"], + getInput: { + dnsHoleSummary: () => ({}), + dnsHoleControls: () => ({}), + }, + }), +); diff --git a/packages/cron-jobs/src/jobs/integrations/downloads.ts b/packages/cron-jobs/src/jobs/integrations/downloads.ts new file mode 100644 index 000000000..54eb476dc --- /dev/null +++ b/packages/cron-jobs/src/jobs/integrations/downloads.ts @@ -0,0 +1,16 @@ +import { EVERY_5_SECONDS } from "@homarr/cron-jobs-core/expressions"; +import { downloadClientRequestHandler } from "@homarr/request-handler/downloads"; +import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; + +import { createCronJob } from "../../lib"; + +export const downloadsJob = createCronJob("downloads", EVERY_5_SECONDS).withCallback( + createRequestIntegrationJobHandler(downloadClientRequestHandler.handler, { + widgetKinds: ["downloads"], + getInput: { + downloads: (options) => ({ + limit: options.limitPerIntegration, + }), + }, + }), +); diff --git a/packages/cron-jobs/src/jobs/integrations/firewall.ts b/packages/cron-jobs/src/jobs/integrations/firewall.ts new file mode 100644 index 000000000..50637a642 --- /dev/null +++ b/packages/cron-jobs/src/jobs/integrations/firewall.ts @@ -0,0 +1,46 @@ +import { EVERY_5_SECONDS, EVERY_30_SECONDS, EVERY_HOUR, EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; +import { + firewallCpuRequestHandler, + firewallInterfacesRequestHandler, + firewallMemoryRequestHandler, + firewallVersionRequestHandler, +} from "@homarr/request-handler/firewall"; +import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; + +import { createCronJob } from "../../lib"; + +export const firewallCpuJob = createCronJob("firewallCpu", EVERY_5_SECONDS).withCallback( + createRequestIntegrationJobHandler(firewallCpuRequestHandler.handler, { + widgetKinds: ["firewall"], + getInput: { + firewall: () => ({}), + }, + }), +); + +export const firewallMemoryJob = createCronJob("firewallMemory", EVERY_MINUTE).withCallback( + createRequestIntegrationJobHandler(firewallMemoryRequestHandler.handler, { + widgetKinds: ["firewall"], + getInput: { + firewall: () => ({}), + }, + }), +); + +export const firewallInterfacesJob = createCronJob("firewallInterfaces", EVERY_30_SECONDS).withCallback( + createRequestIntegrationJobHandler(firewallInterfacesRequestHandler.handler, { + widgetKinds: ["firewall"], + getInput: { + firewall: () => ({}), + }, + }), +); + +export const firewallVersionJob = createCronJob("firewallVersion", EVERY_HOUR).withCallback( + createRequestIntegrationJobHandler(firewallVersionRequestHandler.handler, { + widgetKinds: ["firewall"], + getInput: { + firewall: () => ({}), + }, + }), +); diff --git a/packages/cron-jobs/src/jobs/integrations/health-monitoring.ts b/packages/cron-jobs/src/jobs/integrations/health-monitoring.ts new file mode 100644 index 000000000..4d09801d2 --- /dev/null +++ b/packages/cron-jobs/src/jobs/integrations/health-monitoring.ts @@ -0,0 +1,33 @@ +import { EVERY_5_SECONDS } from "@homarr/cron-jobs-core/expressions"; +import { coolifyRequestHandler } from "@homarr/request-handler/coolify"; +import { clusterInfoRequestHandler, systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring"; +import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; + +import { createCronJob } from "../../lib"; + +export const healthMonitoringJob = createCronJob("healthMonitoring", EVERY_5_SECONDS).withCallback( + createRequestIntegrationJobHandler( + (integration, itemOptions: Record) => { + const { kind } = integration; + + if (kind === "beszel") { + return null as never; + } + if (kind !== "proxmox" && kind !== "mock" && kind !== "coolify") { + return systemInfoRequestHandler.handler({ ...integration, kind }, itemOptions); + } + if (kind === "coolify") { + return coolifyRequestHandler.handler({ ...integration, kind }, itemOptions); + } + return clusterInfoRequestHandler.handler({ ...integration, kind }, itemOptions); + }, + { + widgetKinds: ["healthMonitoring", "systemResources", "coolify"], + getInput: { + healthMonitoring: () => ({}), + systemResources: () => ({}), + coolify: () => ({}), + }, + }, + ), +); diff --git a/packages/cron-jobs/src/jobs/integrations/home-assistant.ts b/packages/cron-jobs/src/jobs/integrations/home-assistant.ts new file mode 100644 index 000000000..2723391a7 --- /dev/null +++ b/packages/cron-jobs/src/jobs/integrations/home-assistant.ts @@ -0,0 +1,16 @@ +import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; +import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; +import { smartHomeEntityStateRequestHandler } from "@homarr/request-handler/smart-home-entity-state"; + +import { createCronJob } from "../../lib"; + +export const smartHomeEntityStateJob = createCronJob("smartHomeEntityState", EVERY_MINUTE).withCallback( + createRequestIntegrationJobHandler(smartHomeEntityStateRequestHandler.handler, { + widgetKinds: ["smartHome-entityState"], + getInput: { + "smartHome-entityState": (options) => ({ + entityId: options.entityId, + }), + }, + }), +); diff --git a/packages/cron-jobs/src/jobs/integrations/indexer-manager.ts b/packages/cron-jobs/src/jobs/integrations/indexer-manager.ts new file mode 100644 index 000000000..11da75508 --- /dev/null +++ b/packages/cron-jobs/src/jobs/integrations/indexer-manager.ts @@ -0,0 +1,14 @@ +import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions"; +import { indexerManagerRequestHandler } from "@homarr/request-handler/indexer-manager"; +import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; + +import { createCronJob } from "../../lib"; + +export const indexerManagerJob = createCronJob("indexerManager", EVERY_5_MINUTES).withCallback( + createRequestIntegrationJobHandler(indexerManagerRequestHandler.handler, { + widgetKinds: ["indexerManager"], + getInput: { + indexerManager: () => ({}), + }, + }), +); diff --git a/packages/cron-jobs/src/jobs/integrations/media-organizer.ts b/packages/cron-jobs/src/jobs/integrations/media-organizer.ts new file mode 100644 index 000000000..a84696da9 --- /dev/null +++ b/packages/cron-jobs/src/jobs/integrations/media-organizer.ts @@ -0,0 +1,36 @@ +import dayjs from "dayjs"; + +import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; +import { calendarMonthRequestHandler } from "@homarr/request-handler/calendar"; +import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; + +import { createCronJob } from "../../lib"; + +export const mediaOrganizerJob = createCronJob("mediaOrganizer", EVERY_MINUTE).withCallback( + createRequestIntegrationJobHandler(calendarMonthRequestHandler.handler, { + widgetKinds: ["calendar"], + getInput: { + // Request handler will run for all specified months + calendar: (options) => { + const inputs = []; + + const startOffset = -Number(options.filterPastMonths); + const endOffset = Number(options.filterFutureMonths); + + for (let offsetMonths = startOffset; offsetMonths <= endOffset; offsetMonths++) { + const year = dayjs().subtract(offsetMonths, "months").year(); + const month = dayjs().subtract(offsetMonths, "months").month(); + + inputs.push({ + year, + month, + releaseType: options.releaseType, + showUnmonitored: options.showUnmonitored, + }); + } + + return inputs; + }, + }, + }), +); diff --git a/packages/cron-jobs/src/jobs/integrations/media-requests.ts b/packages/cron-jobs/src/jobs/integrations/media-requests.ts new file mode 100644 index 000000000..ededb6141 --- /dev/null +++ b/packages/cron-jobs/src/jobs/integrations/media-requests.ts @@ -0,0 +1,24 @@ +import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; +import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; +import { mediaRequestListRequestHandler } from "@homarr/request-handler/media-request-list"; +import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats"; + +import { createCronJob } from "../../lib"; + +export const mediaRequestStatsJob = createCronJob("mediaRequestStats", EVERY_MINUTE).withCallback( + createRequestIntegrationJobHandler(mediaRequestStatsRequestHandler.handler, { + widgetKinds: ["mediaRequests-requestStats"], + getInput: { + "mediaRequests-requestStats": () => ({}), + }, + }), +); + +export const mediaRequestListJob = createCronJob("mediaRequestList", EVERY_MINUTE).withCallback( + createRequestIntegrationJobHandler(mediaRequestListRequestHandler.handler, { + widgetKinds: ["mediaRequests-requestList"], + getInput: { + "mediaRequests-requestList": () => ({}), + }, + }), +); diff --git a/packages/cron-jobs/src/jobs/integrations/media-server.ts b/packages/cron-jobs/src/jobs/integrations/media-server.ts new file mode 100644 index 000000000..099ea53ae --- /dev/null +++ b/packages/cron-jobs/src/jobs/integrations/media-server.ts @@ -0,0 +1,14 @@ +import { EVERY_5_SECONDS } from "@homarr/cron-jobs-core/expressions"; +import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; +import { mediaServerRequestHandler } from "@homarr/request-handler/media-server"; + +import { createCronJob } from "../../lib"; + +export const mediaServerJob = createCronJob("mediaServer", EVERY_5_SECONDS).withCallback( + createRequestIntegrationJobHandler(mediaServerRequestHandler.handler, { + widgetKinds: ["mediaServer"], + getInput: { + mediaServer: ({ showOnlyPlaying }) => ({ showOnlyPlaying }), + }, + }), +); diff --git a/packages/cron-jobs/src/jobs/integrations/media-transcoding.ts b/packages/cron-jobs/src/jobs/integrations/media-transcoding.ts new file mode 100644 index 000000000..c039dc250 --- /dev/null +++ b/packages/cron-jobs/src/jobs/integrations/media-transcoding.ts @@ -0,0 +1,14 @@ +import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions"; +import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; +import { mediaTranscodingRequestHandler } from "@homarr/request-handler/media-transcoding"; + +import { createCronJob } from "../../lib"; + +export const mediaTranscodingJob = createCronJob("mediaTranscoding", EVERY_5_MINUTES).withCallback( + createRequestIntegrationJobHandler(mediaTranscodingRequestHandler.handler, { + widgetKinds: ["mediaTranscoding"], + getInput: { + mediaTranscoding: () => ({ pageOffset: 0, pageSize: 10 }), + }, + }), +); diff --git a/packages/cron-jobs/src/jobs/integrations/network-controller.ts b/packages/cron-jobs/src/jobs/integrations/network-controller.ts new file mode 100644 index 000000000..5d9354588 --- /dev/null +++ b/packages/cron-jobs/src/jobs/integrations/network-controller.ts @@ -0,0 +1,14 @@ +import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; +import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; +import { networkControllerRequestHandler } from "@homarr/request-handler/network-controller"; + +import { createCronJob } from "../../lib"; + +export const networkControllerJob = createCronJob("networkController", EVERY_MINUTE).withCallback( + createRequestIntegrationJobHandler(networkControllerRequestHandler.handler, { + widgetKinds: ["networkControllerSummary"], + getInput: { + networkControllerSummary: () => ({}), + }, + }), +); diff --git a/packages/cron-jobs/src/jobs/integrations/notifications.ts b/packages/cron-jobs/src/jobs/integrations/notifications.ts new file mode 100644 index 000000000..0f544995f --- /dev/null +++ b/packages/cron-jobs/src/jobs/integrations/notifications.ts @@ -0,0 +1,14 @@ +import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions"; +import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; +import { notificationsRequestHandler } from "@homarr/request-handler/notifications"; + +import { createCronJob } from "../../lib"; + +export const refreshNotificationsJob = createCronJob("refreshNotifications", EVERY_5_MINUTES).withCallback( + createRequestIntegrationJobHandler(notificationsRequestHandler.handler, { + widgetKinds: ["notifications"], + getInput: { + notifications: (options) => options, + }, + }), +); diff --git a/packages/cron-jobs/src/jobs/integrations/timetable.ts b/packages/cron-jobs/src/jobs/integrations/timetable.ts new file mode 100644 index 000000000..b3964dc38 --- /dev/null +++ b/packages/cron-jobs/src/jobs/integrations/timetable.ts @@ -0,0 +1,20 @@ +import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions"; +import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; +import { timetableGetTimetableRequestHandler } from "@homarr/request-handler/timetable"; + +import { createCronJob } from "../../lib"; + +export const timetableJob = createCronJob("timetable", EVERY_5_MINUTES).withCallback( + createRequestIntegrationJobHandler(timetableGetTimetableRequestHandler.handler, { + widgetKinds: ["timetable"], + getInput: { + timetable: (options) => + options.station + ? { + stationId: options.station.value, + limit: 10, + } + : [], + }, + }), +); diff --git a/packages/cron-jobs/src/jobs/integrations/tracearr.ts b/packages/cron-jobs/src/jobs/integrations/tracearr.ts new file mode 100644 index 000000000..f80130374 --- /dev/null +++ b/packages/cron-jobs/src/jobs/integrations/tracearr.ts @@ -0,0 +1,14 @@ +import { EVERY_5_SECONDS } from "@homarr/cron-jobs-core/expressions"; +import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; +import { tracearrRequestHandler } from "@homarr/request-handler/tracearr"; + +import { createCronJob } from "../../lib"; + +export const tracearrJob = createCronJob("tracearr", EVERY_5_SECONDS).withCallback( + createRequestIntegrationJobHandler(tracearrRequestHandler.handler, { + widgetKinds: ["tracearr"], + getInput: { + tracearr: () => ({}), + }, + }), +); diff --git a/packages/cron-jobs/src/jobs/integrations/umami.ts b/packages/cron-jobs/src/jobs/integrations/umami.ts new file mode 100644 index 000000000..c50a3c604 --- /dev/null +++ b/packages/cron-jobs/src/jobs/integrations/umami.ts @@ -0,0 +1,14 @@ +import { EVERY_30_SECONDS } from "@homarr/cron-jobs-core/expressions"; +import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; +import { umamiActiveVisitorsRequestHandler } from "@homarr/request-handler/umami"; + +import { createCronJob } from "../../lib"; + +export const umamiActiveVisitorsJob = createCronJob("umamiActiveVisitors", EVERY_30_SECONDS).withCallback( + createRequestIntegrationJobHandler(umamiActiveVisitorsRequestHandler.handler, { + widgetKinds: ["umami"], + getInput: { + umami: (options) => (options.websiteId ? { websiteId: options.websiteId } : []), + }, + }), +); diff --git a/packages/cron-jobs/src/jobs/integrations/vpn.ts b/packages/cron-jobs/src/jobs/integrations/vpn.ts new file mode 100644 index 000000000..b6495b359 --- /dev/null +++ b/packages/cron-jobs/src/jobs/integrations/vpn.ts @@ -0,0 +1,14 @@ +import { EVERY_30_SECONDS } from "@homarr/cron-jobs-core/expressions"; +import { vpnSummaryHandler } from "@homarr/request-handler/vpn"; +import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; + +import { createCronJob } from "../../lib"; + +export const vpnJob = createCronJob("vpn", EVERY_30_SECONDS).withCallback( + createRequestIntegrationJobHandler(vpnSummaryHandler.handler, { + widgetKinds: ["vpn"], + getInput: { + vpn: () => ({}), + }, + }), +); diff --git a/packages/cron-jobs/src/jobs/minecraft-server-status.ts b/packages/cron-jobs/src/jobs/minecraft-server-status.ts new file mode 100644 index 000000000..e6c084702 --- /dev/null +++ b/packages/cron-jobs/src/jobs/minecraft-server-status.ts @@ -0,0 +1,25 @@ +import SuperJSON from "superjson"; + +import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions"; +import { db, eq } from "@homarr/db"; +import { items } from "@homarr/db/schema"; +import { minecraftServerStatusRequestHandler } from "@homarr/request-handler/minecraft-server-status"; + +import type { WidgetComponentProps } from "../../../widgets/src"; +import { createCronJob } from "../lib"; + +export const minecraftServerStatusJob = createCronJob("minecraftServerStatus", EVERY_5_MINUTES).withCallback( + async () => { + const dbItems = await db.query.items.findMany({ + where: eq(items.kind, "minecraftServerStatus"), + }); + + await Promise.allSettled( + dbItems.map(async (item) => { + const options = SuperJSON.parse["options"]>(item.options); + const innerHandler = minecraftServerStatusRequestHandler.handler(options); + await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true }); + }), + ); + }, +); diff --git a/packages/cron-jobs/src/jobs/rss-feeds.ts b/packages/cron-jobs/src/jobs/rss-feeds.ts new file mode 100644 index 000000000..6a497a043 --- /dev/null +++ b/packages/cron-jobs/src/jobs/rss-feeds.ts @@ -0,0 +1,39 @@ +import SuperJSON from "superjson"; + +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; +import { EVERY_10_MINUTES } from "@homarr/cron-jobs-core/expressions"; +import { db, eq } from "@homarr/db"; +import { items } from "@homarr/db/schema"; +import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds"; + +// This import is done that way to avoid circular dependencies. +import type { WidgetComponentProps } from "../../../widgets"; +import { createCronJob } from "../lib"; + +const logger = createLogger({ module: "rssFeedsJobs" }); + +export const rssFeedsJob = createCronJob("rssFeeds", EVERY_10_MINUTES).withCallback(async () => { + const rssItems = await db.query.items.findMany({ + where: eq(items.kind, "rssFeed"), + }); + + const itemOptions = rssItems.map((item) => SuperJSON.parse["options"]>(item.options)); + + for (const option of itemOptions) { + const maxAmountPosts = typeof option.maximumAmountPosts === "number" ? option.maximumAmountPosts : 100; + for (const url of option.feedUrls) { + try { + const innerHandler = rssFeedsRequestHandler.handler({ + url, + count: maxAmountPosts, + }); + await innerHandler.getCachedOrUpdatedDataAsync({ + forceUpdate: true, + }); + } catch (error) { + logger.error(new ErrorWithMetadata("Failed to update RSS feed", { url }, { cause: error })); + } + } + } +}); diff --git a/packages/cron-jobs/src/jobs/update-checker.ts b/packages/cron-jobs/src/jobs/update-checker.ts new file mode 100644 index 000000000..eea68d07b --- /dev/null +++ b/packages/cron-jobs/src/jobs/update-checker.ts @@ -0,0 +1,11 @@ +import { EVERY_DAY } from "@homarr/cron-jobs-core/expressions"; +import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker"; + +import { createCronJob } from "../lib"; + +export const updateCheckerJob = createCronJob("updateChecker", EVERY_DAY, { + runOnStart: true, +}).withCallback(async () => { + const handler = updateCheckerRequestHandler.handler({}); + await handler.getCachedOrUpdatedDataAsync({}); +}); diff --git a/packages/cron-jobs/src/jobs/weather.ts b/packages/cron-jobs/src/jobs/weather.ts new file mode 100644 index 000000000..1b23ade4c --- /dev/null +++ b/packages/cron-jobs/src/jobs/weather.ts @@ -0,0 +1,36 @@ +import SuperJSON from "superjson"; + +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; +import { EVERY_10_MINUTES } from "@homarr/cron-jobs-core/expressions"; +import { db, eq } from "@homarr/db"; +import { items } from "@homarr/db/schema"; +import { weatherRequestHandler } from "@homarr/request-handler/weather"; + +import type { WidgetComponentProps } from "../../../widgets"; +import { createCronJob } from "../lib"; + +const logger = createLogger({ module: "weatherJobs" }); + +export const weatherJob = createCronJob("weather", EVERY_10_MINUTES).withCallback(async () => { + const weatherItems = await db.query.items.findMany({ + where: eq(items.kind, "weather"), + }); + + const parsedItems = weatherItems.map((item) => ({ + id: item.id, + options: SuperJSON.parse["options"]>(item.options), + })); + + for (const item of parsedItems) { + try { + const innerHandler = weatherRequestHandler.handler({ + longitude: item.options.location.longitude, + latitude: item.options.location.latitude, + }); + await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true }); + } catch (error) { + logger.error(new ErrorWithMetadata("Failed to update weather", { id: item.id }, { cause: error })); + } + } +}); diff --git a/packages/redis/src/lib/channels/history-channel.ts b/packages/redis/src/lib/channels/history-channel.ts new file mode 100644 index 000000000..541830464 --- /dev/null +++ b/packages/redis/src/lib/channels/history-channel.ts @@ -0,0 +1,6 @@ +import { createChannelEventHistory } from "../channel"; + +export const createIntegrationHistoryChannel = (integrationId: string, queryKey: string, maxElements = 32) => { + const channelName = `integration:${integrationId}:history:${queryKey}`; + return createChannelEventHistory(channelName, maxElements); +}; diff --git a/packages/request-handler/src/archive-team-warrior.ts b/packages/request-handler/src/archive-team-warrior.ts index e02391f19..4038ec9dc 100644 --- a/packages/request-handler/src/archive-team-warrior.ts +++ b/packages/request-handler/src/archive-team-warrior.ts @@ -1,10 +1,12 @@ +import dayjs from "dayjs"; + import type { IntegrationKindByCategory } from "@homarr/definitions"; import type { ArchiveTeamWarriorStatus } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations"; -import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; -export const archiveTeamWarriorRequestHandler = createIntegrationRequestHandler< +export const archiveTeamWarriorRequestHandler = createCachedIntegrationRequestHandler< ArchiveTeamWarriorStatus, IntegrationKindByCategory<"archiving">, Record @@ -13,4 +15,6 @@ export const archiveTeamWarriorRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getStatusAsync(); }, + cacheDuration: dayjs.duration(5, "minute"), + queryKey: "archiveTeamWarriorStatus", }); diff --git a/packages/request-handler/src/audiobookshelf.ts b/packages/request-handler/src/audiobookshelf.ts index 12562342c..bb4b2d83c 100644 --- a/packages/request-handler/src/audiobookshelf.ts +++ b/packages/request-handler/src/audiobookshelf.ts @@ -1,9 +1,11 @@ +import dayjs from "dayjs"; + import { createIntegrationAsync } from "@homarr/integrations"; import type { AudiobookshelfDashboardData } from "@homarr/integrations/types"; -import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; -export const audiobookshelfRequestHandler = createIntegrationRequestHandler< +export const audiobookshelfRequestHandler = createCachedIntegrationRequestHandler< AudiobookshelfDashboardData, "audiobookshelf", Record @@ -12,4 +14,6 @@ export const audiobookshelfRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getDashboardDataAsync(); }, + cacheDuration: dayjs.duration(10, "minutes"), + queryKey: "audiobookshelfDashboard", }); diff --git a/packages/request-handler/src/calendar.ts b/packages/request-handler/src/calendar.ts index a358d13e8..dd198912a 100644 --- a/packages/request-handler/src/calendar.ts +++ b/packages/request-handler/src/calendar.ts @@ -4,9 +4,9 @@ import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { CalendarEvent, RadarrReleaseType } from "@homarr/integrations/types"; -import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; -export const calendarMonthRequestHandler = createIntegrationRequestHandler< +export const calendarMonthRequestHandler = createCachedIntegrationRequestHandler< CalendarEvent[], IntegrationKindByCategory<"calendar">, { year: number; month: number; releaseType: RadarrReleaseType[]; showUnmonitored: boolean } @@ -23,4 +23,6 @@ export const calendarMonthRequestHandler = createIntegrationRequestHandler< input.showUnmonitored, ); }, + cacheDuration: dayjs.duration(1, "minute"), + queryKey: "calendarMonth", }); diff --git a/packages/request-handler/src/coolify.ts b/packages/request-handler/src/coolify.ts index 25ddfe962..d307a780f 100644 --- a/packages/request-handler/src/coolify.ts +++ b/packages/request-handler/src/coolify.ts @@ -1,9 +1,11 @@ +import dayjs from "dayjs"; + import { createIntegrationAsync } from "@homarr/integrations"; import type { CoolifyInstanceInfo } from "@homarr/integrations/types"; -import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; -export const coolifyRequestHandler = createIntegrationRequestHandler< +export const coolifyRequestHandler = createCachedIntegrationRequestHandler< CoolifyInstanceInfo, "coolify", Record @@ -12,4 +14,6 @@ export const coolifyRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getInstanceInfoAsync(); }, + cacheDuration: dayjs.duration(30, "seconds"), + queryKey: "coolifyInfo", }); diff --git a/packages/request-handler/src/dns-hole.ts b/packages/request-handler/src/dns-hole.ts index 85188c740..a8c920801 100644 --- a/packages/request-handler/src/dns-hole.ts +++ b/packages/request-handler/src/dns-hole.ts @@ -1,10 +1,12 @@ +import dayjs from "dayjs"; + import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { DnsHoleSummary } from "@homarr/integrations/types"; -import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; -export const dnsHoleRequestHandler = createIntegrationRequestHandler< +export const dnsHoleRequestHandler = createCachedIntegrationRequestHandler< DnsHoleSummary, IntegrationKindByCategory<"dnsHole">, Record @@ -13,4 +15,6 @@ export const dnsHoleRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getSummaryAsync(); }, + cacheDuration: dayjs.duration(5, "seconds"), + queryKey: "dnsHoleSummary", }); diff --git a/packages/request-handler/src/downloads.ts b/packages/request-handler/src/downloads.ts index 12106b07a..06d4e9c64 100644 --- a/packages/request-handler/src/downloads.ts +++ b/packages/request-handler/src/downloads.ts @@ -1,10 +1,12 @@ +import dayjs from "dayjs"; + import type { IntegrationKindByCategory } from "@homarr/definitions"; import type { DownloadClientJobsAndStatus } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations"; -import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; -export const downloadClientRequestHandler = createIntegrationRequestHandler< +export const downloadClientRequestHandler = createCachedIntegrationRequestHandler< DownloadClientJobsAndStatus, IntegrationKindByCategory<"downloadClient">, { limit: number } @@ -13,4 +15,6 @@ export const downloadClientRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getClientJobsAndStatusAsync(input); }, + cacheDuration: dayjs.duration(5, "seconds"), + queryKey: "downloadClientJobStatus", }); diff --git a/packages/request-handler/src/indexer-manager.ts b/packages/request-handler/src/indexer-manager.ts index f21a61f8f..a957b32ad 100644 --- a/packages/request-handler/src/indexer-manager.ts +++ b/packages/request-handler/src/indexer-manager.ts @@ -1,10 +1,12 @@ +import dayjs from "dayjs"; + import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { Indexer } from "@homarr/integrations/types"; -import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; -export const indexerManagerRequestHandler = createIntegrationRequestHandler< +export const indexerManagerRequestHandler = createCachedIntegrationRequestHandler< Indexer[], IntegrationKindByCategory<"indexerManager">, Record @@ -13,4 +15,6 @@ export const indexerManagerRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getIndexersAsync(); }, + cacheDuration: dayjs.duration(5, "minutes"), + queryKey: "indexerManager", }); diff --git a/packages/request-handler/src/lib/cached-integration-request-handler.ts b/packages/request-handler/src/lib/cached-integration-request-handler.ts new file mode 100644 index 000000000..aee69e39c --- /dev/null +++ b/packages/request-handler/src/lib/cached-integration-request-handler.ts @@ -0,0 +1,49 @@ +import type { Duration } from "dayjs/plugin/duration"; + +import type { Modify } from "@homarr/common/types"; +import type { Integration, IntegrationSecret } from "@homarr/db/schema"; +import type { IntegrationKind } from "@homarr/definitions"; +import { createIntegrationOptionsChannel } from "@homarr/redis"; + +import { createCachedRequestHandler } from "./cached-request-handler"; + +type IntegrationOfKind = Omit & { + kind: TKind; + decryptedSecrets: Modify, { value: string }>[]; + externalUrl: string | null; +}; + +interface Options> { + queryKey: string; + requestAsync: (integration: IntegrationOfKind, input: TInput) => Promise; + cacheDuration: Duration; + cacheDurationForInput?: (input: TInput) => Duration | undefined; + fallbackToStaleOnError?: boolean; + retry?: { attempts?: number; delayMs?: number }; + isValid?: (data: TData) => boolean; +} + +export const createCachedIntegrationRequestHandler = < + TData, + TKind extends IntegrationKind, + TInput extends Record, +>( + options: Options, +) => { + return { + handler: (integration: IntegrationOfKind, itemOptions: TInput) => + createCachedRequestHandler({ + queryKey: options.queryKey, + requestAsync: async (input: { options: TInput; integration: IntegrationOfKind }) => { + return await options.requestAsync(input.integration, input.options); + }, + cacheDuration: options.cacheDurationForInput?.(itemOptions) ?? options.cacheDuration, + fallbackToStaleOnError: options.fallbackToStaleOnError, + retry: options.retry, + isValid: options.isValid, + createRedisChannel(input, handlerOptions) { + return createIntegrationOptionsChannel(input.integration.id, handlerOptions.queryKey, input.options); + }, + }).handler({ options: itemOptions, integration }), + }; +}; diff --git a/packages/request-handler/src/lib/cached-request-handler.ts b/packages/request-handler/src/lib/cached-request-handler.ts new file mode 100644 index 000000000..c2342eed3 --- /dev/null +++ b/packages/request-handler/src/lib/cached-request-handler.ts @@ -0,0 +1,160 @@ +import dayjs from "dayjs"; +import type { Duration } from "dayjs/plugin/duration"; + +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; +import { createLogger } from "@homarr/core/infrastructure/logs"; +import type { createChannelWithLatestAndEvents } from "@homarr/redis"; + +const logger = createLogger({ module: "cachedRequestHandler" }); + +type FetchResult = { data: TData; timestamp: Date }; +type InFlightEntry = Promise>; + +const inFlightFetches = new Map(); + +const isAbortedError = (error: unknown): boolean => { + if (!(error instanceof Error)) return false; + return error.name === "AbortError" || error.name === "TimeoutError"; +}; + +interface Options> { + // Unique key for this request handler + queryKey: string; + requestAsync: (input: TInput) => Promise; + createRedisChannel: ( + input: TInput, + options: Options, + ) => ReturnType>; + cacheDuration: Duration; + fallbackToStaleOnError?: boolean; + // Retry the upstream call on failure. attempts=1 means no retry (default). + // AbortError / TimeoutError are never retried. + retry?: { attempts?: number; delayMs?: number }; + // Predicate to decide whether a fetched response is worth caching. + // Return false to skip the cache write (next call will refetch). + isValid?: (data: TData) => boolean; +} + +const defaultRetry = { attempts: 1, delayMs: 0 }; +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export const createCachedRequestHandler = >( + options: Options, +) => { + return { + handler: (input: TInput) => { + const channel = options.createRedisChannel(input, options); + const retryConfig = { ...defaultRetry, ...options.retry }; + + return { + async getCachedOrUpdatedDataAsync({ forceUpdate = false }) { + const channelData = await channel.getAsync(); + + const requestNewDataAsync = (): Promise> => { + const inflight = inFlightFetches.get(channel.name); + if (inflight) { + logger.debug("Cached request handler joining in-flight fetch", { + channel: channel.name, + queryKey: options.queryKey, + }); + return inflight as Promise>; + } + + const promise = (async (): Promise> => { + let lastError: unknown; + for (let attempt = 1; attempt <= retryConfig.attempts; attempt++) { + try { + const data = await options.requestAsync(input); + if (options.isValid && !options.isValid(data)) { + logger.warn("Cached request handler received invalid data, not caching", { + channel: channel.name, + queryKey: options.queryKey, + attempt, + }); + return { data, timestamp: new Date() }; + } + await channel.publishAndUpdateLastStateAsync(data); + return { data, timestamp: new Date() }; + } catch (error) { + lastError = error; + if (isAbortedError(error) || attempt === retryConfig.attempts) { + break; + } + logger.warn("Cached request handler fetch failed, retrying", { + channel: channel.name, + queryKey: options.queryKey, + attempt, + nextDelayMs: retryConfig.delayMs, + error: error instanceof Error ? `${error.name}: ${error.message}` : String(error), + }); + await sleep(retryConfig.delayMs); + } + } + + if (options.fallbackToStaleOnError && channelData) { + logger.warn( + new ErrorWithMetadata( + "Cached request handler using stale cache after fetch failure", + { + channel: channel.name, + queryKey: options.queryKey, + attempts: retryConfig.attempts, + }, + { cause: lastError }, + ), + ); + return channelData; + } + + throw lastError; + })().finally(() => { + inFlightFetches.delete(channel.name); + }); + + inFlightFetches.set(channel.name, promise as InFlightEntry); + return promise; + }; + + if (forceUpdate) { + logger.debug("Cached request handler forced update", { + channel: channel.name, + queryKey: options.queryKey, + }); + return await requestNewDataAsync(); + } + + const shouldRequestNewData = + !channelData || + dayjs().diff(channelData.timestamp, "milliseconds") > options.cacheDuration.asMilliseconds(); + + if (shouldRequestNewData) { + logger.debug("Cached request handler cache miss", { + channel: channel.name, + queryKey: options.queryKey, + reason: !channelData ? "no data" : "cache expired", + }); + return await requestNewDataAsync(); + } + + logger.debug("Cached request handler cache hit", { + channel: channel.name, + queryKey: options.queryKey, + expiresAt: dayjs(channelData.timestamp).add(options.cacheDuration).toISOString(), + }); + + return channelData; + }, + async invalidateAsync() { + logger.debug("Cached request handler invalidating cache", { + channel: channel.name, + queryKey: options.queryKey, + }); + await this.getCachedOrUpdatedDataAsync({ forceUpdate: true }); + }, + subscribe(callback: (data: TData) => void) { + return channel.subscribe(callback); + }, + }; + }, + }; +}; diff --git a/packages/request-handler/src/lib/cached-request-integration-job-handler.ts b/packages/request-handler/src/lib/cached-request-integration-job-handler.ts new file mode 100644 index 000000000..63e7682c2 --- /dev/null +++ b/packages/request-handler/src/lib/cached-request-integration-job-handler.ts @@ -0,0 +1,115 @@ +import SuperJSON from "superjson"; + +import { hashObjectBase64, Stopwatch } from "@homarr/common"; +import { decryptSecret } from "@homarr/common/server"; +import type { MaybeArray } from "@homarr/common/types"; +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; +import { db } from "@homarr/db"; +import { getItemsWithIntegrationsAsync, getServerSettingsAsync } from "@homarr/db/queries"; +import type { WidgetKind } from "@homarr/definitions"; + +// This imports are done that way to avoid circular dependencies. +import type { inferSupportedIntegrationsStrict } from "../../../widgets/src"; +import { reduceWidgetOptionsWithDefaultValues } from "../../../widgets/src"; +import type { WidgetComponentProps } from "../../../widgets/src/definition"; +import type { createCachedIntegrationRequestHandler } from "./cached-integration-request-handler"; + +const logger = createLogger({ module: "cachedRequestIntegrationJobHandler" }); + +export const createRequestIntegrationJobHandler = < + TWidgetKind extends WidgetKind, + TIntegrationKind extends inferSupportedIntegrationsStrict, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + THandler extends ReturnType>["handler"], +>( + handler: THandler, + { + widgetKinds, + getInput, + }: { + widgetKinds: TWidgetKind[]; + getInput: { + [key in TWidgetKind]: (options: WidgetComponentProps["options"]) => MaybeArray[1]>; + }; + }, +) => { + return async () => { + const serverSettings = await getServerSettingsAsync(db); + const itemsForIntegration = await getItemsWithIntegrationsAsync(db, { + kinds: widgetKinds, + }); + + logger.debug("Found items for integration", { + widgetKinds: widgetKinds.join(","), + count: itemsForIntegration.length, + }); + + const distinctIntegrations: { + integrationId: string; + inputHash: string; + integration: (typeof itemsForIntegration)[number]["integrations"][number]["integration"]; + input: Parameters[1]; + }[] = []; + + for (const itemForIntegration of itemsForIntegration) { + const oneOrMultipleInputs = getInput[itemForIntegration.kind]( + reduceWidgetOptionsWithDefaultValues( + itemForIntegration.kind, + { + enableStatusByDefault: serverSettings.board.enableStatusByDefault, + forceDisableStatus: serverSettings.board.forceDisableStatus, + }, + SuperJSON.parse>(itemForIntegration.options), + ) as never, + ); + for (const { integration } of itemForIntegration.integrations) { + const inputArray = Array.isArray(oneOrMultipleInputs) ? oneOrMultipleInputs : [oneOrMultipleInputs]; + + for (const input of inputArray) { + const inputHash = hashObjectBase64(input); + if ( + distinctIntegrations.some( + (distinctIntegration) => + distinctIntegration.integrationId === integration.id && distinctIntegration.inputHash === inputHash, + ) + ) { + continue; + } + + distinctIntegrations.push({ integrationId: integration.id, inputHash, integration, input }); + } + } + } + + for (const { integrationId, integration, input, inputHash } of distinctIntegrations) { + try { + const decryptedSecrets = integration.secrets.map((secret) => ({ + ...secret, + value: decryptSecret(secret.value), + })); + + const innerHandler = handler( + { + ...integration, + kind: integration.kind as TIntegrationKind, + decryptedSecrets, + externalUrl: integration.app?.href ?? null, + }, + input, + ); + const stopWatch = new Stopwatch(); + await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true }); + logger.debug("Ran integration job", { + integration: integrationId, + inputHash, + elapsed: stopWatch.getElapsedInHumanWords(), + }); + } catch (error) { + logger.error( + new ErrorWithMetadata("Failed to run integration job", { integrationId, inputHash }, { cause: error }), + ); + } + } + }; +}; diff --git a/packages/request-handler/src/lib/cached-widget-request-handler.ts b/packages/request-handler/src/lib/cached-widget-request-handler.ts new file mode 100644 index 000000000..4016bc5b4 --- /dev/null +++ b/packages/request-handler/src/lib/cached-widget-request-handler.ts @@ -0,0 +1,36 @@ +import type { Duration } from "dayjs/plugin/duration"; + +import type { WidgetKind } from "@homarr/definitions"; +import { createWidgetOptionsChannel } from "@homarr/redis"; + +import { createCachedRequestHandler } from "./cached-request-handler"; + +interface Options> { + // Unique key for this request handler + queryKey: string; + requestAsync: (input: TInput) => Promise; + cacheDuration: Duration; + widgetKind: TKind; +} + +export const createCachedWidgetRequestHandler = < + TData, + TKind extends WidgetKind, + TInput extends Record, +>( + requestHandlerOptions: Options, +) => { + return { + handler: (widgetOptions: TInput) => + createCachedRequestHandler({ + queryKey: requestHandlerOptions.queryKey, + requestAsync: async (input: TInput) => { + return await requestHandlerOptions.requestAsync(input); + }, + cacheDuration: requestHandlerOptions.cacheDuration, + createRedisChannel(input, options) { + return createWidgetOptionsChannel(requestHandlerOptions.widgetKind, options.queryKey, input); + }, + }).handler(widgetOptions), + }; +}; diff --git a/packages/request-handler/src/media-release.ts b/packages/request-handler/src/media-release.ts index 02209f68f..c0ffe48f3 100644 --- a/packages/request-handler/src/media-release.ts +++ b/packages/request-handler/src/media-release.ts @@ -1,10 +1,12 @@ +import dayjs from "dayjs"; + import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { MediaRelease } from "@homarr/integrations/types"; -import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; -export const mediaReleaseRequestHandler = createIntegrationRequestHandler< +export const mediaReleaseRequestHandler = createCachedIntegrationRequestHandler< MediaRelease[], IntegrationKindByCategory<"mediaRelease">, Record @@ -13,4 +15,6 @@ export const mediaReleaseRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getMediaReleasesAsync(); }, + cacheDuration: dayjs.duration(5, "minutes"), + queryKey: "mediaReleases", }); diff --git a/packages/request-handler/src/media-request-list.ts b/packages/request-handler/src/media-request-list.ts index 62cf29a18..fd4e02234 100644 --- a/packages/request-handler/src/media-request-list.ts +++ b/packages/request-handler/src/media-request-list.ts @@ -1,10 +1,12 @@ +import dayjs from "dayjs"; + import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { MediaRequest } from "@homarr/integrations/types"; -import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; -export const mediaRequestListRequestHandler = createIntegrationRequestHandler< +export const mediaRequestListRequestHandler = createCachedIntegrationRequestHandler< MediaRequest[], IntegrationKindByCategory<"mediaRequest">, Record @@ -13,4 +15,6 @@ export const mediaRequestListRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getRequestsAsync(); }, + cacheDuration: dayjs.duration(1, "minute"), + queryKey: "mediaRequestList", }); diff --git a/packages/request-handler/src/media-request-stats.ts b/packages/request-handler/src/media-request-stats.ts index 61ff810b8..a6568aecd 100644 --- a/packages/request-handler/src/media-request-stats.ts +++ b/packages/request-handler/src/media-request-stats.ts @@ -1,10 +1,12 @@ +import dayjs from "dayjs"; + import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { MediaRequestStats } from "@homarr/integrations/types"; -import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; -export const mediaRequestStatsRequestHandler = createIntegrationRequestHandler< +export const mediaRequestStatsRequestHandler = createCachedIntegrationRequestHandler< MediaRequestStats, IntegrationKindByCategory<"mediaRequest">, Record @@ -16,4 +18,6 @@ export const mediaRequestStatsRequestHandler = createIntegrationRequestHandler< users: await integrationInstance.getUsersAsync(), }; }, + cacheDuration: dayjs.duration(1, "minute"), + queryKey: "mediaRequestStats", }); diff --git a/packages/request-handler/src/media-server.ts b/packages/request-handler/src/media-server.ts index 55a7850e0..b13b90eb0 100644 --- a/packages/request-handler/src/media-server.ts +++ b/packages/request-handler/src/media-server.ts @@ -1,10 +1,12 @@ +import dayjs from "dayjs"; + import type { IntegrationKindByCategory } from "@homarr/definitions"; import type { StreamSession } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations"; -import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; -export const mediaServerRequestHandler = createIntegrationRequestHandler< +export const mediaServerRequestHandler = createCachedIntegrationRequestHandler< StreamSession[], IntegrationKindByCategory<"mediaService">, { @@ -15,4 +17,6 @@ export const mediaServerRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getCurrentSessionsAsync({ showOnlyPlaying: input.showOnlyPlaying }); }, + cacheDuration: dayjs.duration(5, "seconds"), + queryKey: "mediaServerSessions", }); diff --git a/packages/request-handler/src/media-transcoding.ts b/packages/request-handler/src/media-transcoding.ts index ba6455ccd..698d7347b 100644 --- a/packages/request-handler/src/media-transcoding.ts +++ b/packages/request-handler/src/media-transcoding.ts @@ -1,14 +1,18 @@ +import dayjs from "dayjs"; + import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { TdarrQueue, TdarrStatistics, TdarrWorker } from "@homarr/integrations"; -import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; -export const mediaTranscodingRequestHandler = createIntegrationRequestHandler< +export const mediaTranscodingRequestHandler = createCachedIntegrationRequestHandler< MediaTranscoding, IntegrationKindByCategory<"mediaTranscoding">, { pageOffset: number; pageSize: number } >({ + queryKey: "mediaTranscoding", + cacheDuration: dayjs.duration(5, "minutes"), async requestAsync(integration, input) { const integrationInstance = await createIntegrationAsync(integration); return { diff --git a/packages/request-handler/src/minecraft-server-status.ts b/packages/request-handler/src/minecraft-server-status.ts index 664a0a7f3..98d850171 100644 --- a/packages/request-handler/src/minecraft-server-status.ts +++ b/packages/request-handler/src/minecraft-server-status.ts @@ -1,11 +1,14 @@ +import dayjs from "dayjs"; import { z } from "zod/v4"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; -import { createWidgetRequestHandler } from "./lib/widget-request-handler"; +import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; -export const minecraftServerStatusRequestHandler = createWidgetRequestHandler({ +export const minecraftServerStatusRequestHandler = createCachedWidgetRequestHandler({ + queryKey: "minecraftServerStatusApiResult", + widgetKind: "minecraftServerStatus", async requestAsync(input: { domain: string; isBedrockServer: boolean }) { const path = `${input.isBedrockServer ? "/bedrock" : ""}/3/${input.domain}`; @@ -14,6 +17,7 @@ export const minecraftServerStatusRequestHandler = createWidgetRequestHandler({ ); return responseSchema.parse(await response.json()); }, + cacheDuration: dayjs.duration(5, "minutes"), }); const responseSchema = z diff --git a/packages/request-handler/src/navidrome.ts b/packages/request-handler/src/navidrome.ts index 0ad39ed9a..b59f852a8 100644 --- a/packages/request-handler/src/navidrome.ts +++ b/packages/request-handler/src/navidrome.ts @@ -1,9 +1,11 @@ +import dayjs from "dayjs"; + import { createIntegrationAsync } from "@homarr/integrations"; import type { NavidromeDashboardData } from "@homarr/integrations/types"; -import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; -export const navidromeRequestHandler = createIntegrationRequestHandler< +export const navidromeRequestHandler = createCachedIntegrationRequestHandler< NavidromeDashboardData, "navidrome", Record @@ -12,4 +14,6 @@ export const navidromeRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getDashboardDataAsync(); }, + cacheDuration: dayjs.duration(10, "minutes"), + queryKey: "navidromeDashboard", }); diff --git a/packages/request-handler/src/network-controller.ts b/packages/request-handler/src/network-controller.ts index 123261025..f5975acf2 100644 --- a/packages/request-handler/src/network-controller.ts +++ b/packages/request-handler/src/network-controller.ts @@ -1,10 +1,12 @@ +import dayjs from "dayjs"; + import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { NetworkControllerSummary } from "@homarr/integrations/types"; -import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; -export const networkControllerRequestHandler = createIntegrationRequestHandler< +export const networkControllerRequestHandler = createCachedIntegrationRequestHandler< NetworkControllerSummary, IntegrationKindByCategory<"networkController">, Record @@ -13,4 +15,6 @@ export const networkControllerRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getNetworkSummaryAsync(); }, + cacheDuration: dayjs.duration(5, "minutes"), + queryKey: "networkControllerSummary", }); diff --git a/packages/request-handler/src/notifications.ts b/packages/request-handler/src/notifications.ts index 479ff81fa..41098e660 100644 --- a/packages/request-handler/src/notifications.ts +++ b/packages/request-handler/src/notifications.ts @@ -1,10 +1,12 @@ +import dayjs from "dayjs"; + import type { IntegrationKindByCategory } from "@homarr/definitions"; import type { Notification } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations"; -import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; -export const notificationsRequestHandler = createIntegrationRequestHandler< +export const notificationsRequestHandler = createCachedIntegrationRequestHandler< Notification[], IntegrationKindByCategory<"notifications">, Record @@ -13,4 +15,6 @@ export const notificationsRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getNotificationsAsync(); }, + cacheDuration: dayjs.duration(5, "minutes"), + queryKey: "notificationsJobStatus", }); diff --git a/packages/request-handler/src/paperless-ngx.ts b/packages/request-handler/src/paperless-ngx.ts index e6b088006..a3793658f 100644 --- a/packages/request-handler/src/paperless-ngx.ts +++ b/packages/request-handler/src/paperless-ngx.ts @@ -1,9 +1,11 @@ +import dayjs from "dayjs"; + import { createIntegrationAsync } from "@homarr/integrations"; import type { PaperlessNgxStats } from "@homarr/integrations"; -import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; -export const paperlessNgxStatsRequestHandler = createIntegrationRequestHandler< +export const paperlessNgxStatsRequestHandler = createCachedIntegrationRequestHandler< PaperlessNgxStats, "paperlessNgx", Record @@ -12,4 +14,6 @@ export const paperlessNgxStatsRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getStatsAsync(); }, + cacheDuration: dayjs.duration(15, "minute"), + queryKey: "paperless-ngx-stats", }); diff --git a/packages/request-handler/src/releases.ts b/packages/request-handler/src/releases.ts index 8b67e9793..7bfd29348 100644 --- a/packages/request-handler/src/releases.ts +++ b/packages/request-handler/src/releases.ts @@ -1,10 +1,12 @@ +import dayjs from "dayjs"; + import type { IntegrationKindByCategory } from "@homarr/definitions"; import type { ReleaseResponse, ReleasesRepository } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations"; -import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; -export const releasesRequestHandler = createIntegrationRequestHandler< +export const releasesRequestHandler = createCachedIntegrationRequestHandler< ReleaseResponse, IntegrationKindByCategory<"releasesProvider">, ReleasesRepository @@ -13,4 +15,6 @@ export const releasesRequestHandler = createIntegrationRequestHandler< const instance = await createIntegrationAsync(integration); return instance.getLatestMatchingReleaseAsync(input.identifier, input.versionRegex); }, + cacheDuration: dayjs.duration(5, "minutes"), + queryKey: "repositoriesReleases", }); diff --git a/packages/request-handler/src/rss-feeds.ts b/packages/request-handler/src/rss-feeds.ts index 0597a325f..9eaf026ea 100644 --- a/packages/request-handler/src/rss-feeds.ts +++ b/packages/request-handler/src/rss-feeds.ts @@ -1,15 +1,18 @@ import type { FeedData, FeedEntry } from "@extractus/feed-extractor"; import { extract } from "@extractus/feed-extractor"; +import dayjs from "dayjs"; import { z } from "zod/v4"; import type { Modify } from "@homarr/common/types"; import { createLogger } from "@homarr/core/infrastructure/logs"; -import { createWidgetRequestHandler } from "./lib/widget-request-handler"; +import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; const logger = createLogger({ module: "rssFeedsRequestHandler" }); -export const rssFeedsRequestHandler = createWidgetRequestHandler({ +export const rssFeedsRequestHandler = createCachedWidgetRequestHandler({ + queryKey: "rssFeedList", + widgetKind: "rssFeed", async requestAsync(input: { url: string; count: number }) { const result = (await extract(input.url, { getExtraEntryFields: (feedEntry) => { @@ -28,6 +31,7 @@ export const rssFeedsRequestHandler = createWidgetRequestHandler({ entries: result.entries?.map((entry) => ({ ...entry, feedUrl: input.url })).slice(0, input.count) ?? [], }; }, + cacheDuration: dayjs.duration(5, "minutes"), }); 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..5fe02b2ac 100644 --- a/packages/request-handler/src/smart-home-entity-state.ts +++ b/packages/request-handler/src/smart-home-entity-state.ts @@ -1,9 +1,11 @@ +import dayjs from "dayjs"; + import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; -import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; -export const smartHomeEntityStateRequestHandler = createIntegrationRequestHandler< +export const smartHomeEntityStateRequestHandler = createCachedIntegrationRequestHandler< string, IntegrationKindByCategory<"smartHomeServer">, { entityId: string } @@ -18,4 +20,6 @@ export const smartHomeEntityStateRequestHandler = createIntegrationRequestHandle return result.data.state; }, + cacheDuration: dayjs.duration(1, "minute"), + queryKey: "smartHome-entityState", }); diff --git a/packages/request-handler/src/speedtest-tracker.ts b/packages/request-handler/src/speedtest-tracker.ts index 3f03f7187..799d37f16 100644 --- a/packages/request-handler/src/speedtest-tracker.ts +++ b/packages/request-handler/src/speedtest-tracker.ts @@ -1,9 +1,11 @@ +import dayjs from "dayjs"; + import { createIntegrationAsync } from "@homarr/integrations"; import type { SpeedtestTrackerDashboardData } from "@homarr/integrations/types"; -import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; -export const speedtestTrackerRequestHandler = createIntegrationRequestHandler< +export const speedtestTrackerRequestHandler = createCachedIntegrationRequestHandler< SpeedtestTrackerDashboardData, "speedtestTracker", Record @@ -12,4 +14,6 @@ export const speedtestTrackerRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getDashboardDataAsync(); }, + cacheDuration: dayjs.duration(10, "minutes"), + queryKey: "speedtestTrackerDashboard", }); diff --git a/packages/request-handler/src/stock-price.ts b/packages/request-handler/src/stock-price.ts index 177b223aa..b0ca15e47 100644 --- a/packages/request-handler/src/stock-price.ts +++ b/packages/request-handler/src/stock-price.ts @@ -1,11 +1,14 @@ +import dayjs from "dayjs"; import { z } from "zod/v4"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; -import { createWidgetRequestHandler } from "./lib/widget-request-handler"; +import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; -export const fetchStockPriceHandler = createWidgetRequestHandler({ +export const fetchStockPriceHandler = createCachedWidgetRequestHandler({ + queryKey: "fetchStockPriceResult", + widgetKind: "stockPrice", async requestAsync(input: { stock: string; timeRange: string; timeInterval: string }) { const response = await withTimeoutAsync(async (signal) => { return await fetchWithTrustedCertificatesAsync( @@ -39,6 +42,7 @@ export const fetchStockPriceHandler = createWidgetRequestHandler({ shortName: firstResult.meta.shortName, }; }, + cacheDuration: dayjs.duration(5, "minutes"), }); const dataSchema = z diff --git a/packages/request-handler/src/tracearr.ts b/packages/request-handler/src/tracearr.ts index 641bf2922..8f593ac03 100644 --- a/packages/request-handler/src/tracearr.ts +++ b/packages/request-handler/src/tracearr.ts @@ -1,9 +1,11 @@ +import dayjs from "dayjs"; + import { createIntegrationAsync } from "@homarr/integrations"; import type { TracearrDashboardData } from "@homarr/integrations/types"; -import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; -export const tracearrRequestHandler = createIntegrationRequestHandler< +export const tracearrRequestHandler = createCachedIntegrationRequestHandler< TracearrDashboardData, "tracearr", Record @@ -12,4 +14,6 @@ export const tracearrRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getDashboardDataAsync(); }, + cacheDuration: dayjs.duration(5, "seconds"), + queryKey: "tracearrDashboard", }); diff --git a/packages/request-handler/src/ups.ts b/packages/request-handler/src/ups.ts index 4ca17b5ee..acc97eb6d 100644 --- a/packages/request-handler/src/ups.ts +++ b/packages/request-handler/src/ups.ts @@ -1,10 +1,12 @@ +import dayjs from "dayjs"; + import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { UpsSummary } from "@homarr/integrations/types"; -import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; -export const upsSummariesRequestHandler = createIntegrationRequestHandler< +export const upsSummariesRequestHandler = createCachedIntegrationRequestHandler< UpsSummary[], IntegrationKindByCategory<"ups">, Record @@ -13,4 +15,6 @@ export const upsSummariesRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getUpsSummariesAsync(); }, + cacheDuration: dayjs.duration(1, "minute"), + queryKey: "upsSummaries", }); diff --git a/packages/request-handler/src/uptime-kuma.ts b/packages/request-handler/src/uptime-kuma.ts index b9453eb76..39149c137 100644 --- a/packages/request-handler/src/uptime-kuma.ts +++ b/packages/request-handler/src/uptime-kuma.ts @@ -1,9 +1,11 @@ +import dayjs from "dayjs"; + import { createIntegrationAsync } from "@homarr/integrations"; import type { UptimeKumaDashboardData } from "@homarr/integrations/types"; -import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; -export const uptimeKumaRequestHandler = createIntegrationRequestHandler< +export const uptimeKumaRequestHandler = createCachedIntegrationRequestHandler< UptimeKumaDashboardData, "uptimeKuma", Record @@ -12,4 +14,6 @@ export const uptimeKumaRequestHandler = createIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getDashboardDataAsync(); }, + cacheDuration: dayjs.duration(1, "minute"), + queryKey: "uptimeKumaDashboard", }); diff --git a/packages/request-handler/src/weather.ts b/packages/request-handler/src/weather.ts index 5f5a097a4..eb4828048 100644 --- a/packages/request-handler/src/weather.ts +++ b/packages/request-handler/src/weather.ts @@ -1,11 +1,14 @@ +import dayjs from "dayjs"; import { z } from "zod"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; -import { createWidgetRequestHandler } from "./lib/widget-request-handler"; +import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; -export const weatherRequestHandler = createWidgetRequestHandler({ +export const weatherRequestHandler = createCachedWidgetRequestHandler({ + queryKey: "weatherAtLocation", + widgetKind: "weather", async requestAsync(input: { latitude: number; longitude: number }) { const res = await withTimeoutAsync(async (signal) => { return await fetchWithTrustedCertificatesAsync( @@ -31,6 +34,7 @@ export const weatherRequestHandler = createWidgetRequestHandler({ }), } satisfies Weather; }, + cacheDuration: dayjs.duration(1, "minute"), }); const atLocationOutput = z.object({