From 8f4f543d29bf9104c31cf6d4d25b4e2382489d3b Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Thu, 25 Jun 2026 14:47:58 +0200 Subject: [PATCH] fix: resilient RSS/releases fetching and effect deps - RSS feed: Promise.allSettled so one bad URL doesn't fail all feeds - Releases: Promise.allSettled so one bad repo doesn't blank widget - system-resources: add data to useEffect deps - media-requests: invalidate all mediaRequests queries on answer --- packages/api/src/router/widgets/releases.ts | 11 ++++++++++- packages/api/src/router/widgets/rssFeed.ts | 13 ++++++++++--- .../widgets/src/media-requests/list/component.tsx | 2 +- packages/widgets/src/system-resources/component.tsx | 2 +- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/api/src/router/widgets/releases.ts b/packages/api/src/router/widgets/releases.ts index 46ef33e3d..da043f343 100644 --- a/packages/api/src/router/widgets/releases.ts +++ b/packages/api/src/router/widgets/releases.ts @@ -1,12 +1,15 @@ import { escapeForRegEx } from "@tiptap/react"; import { z } from "zod/v4"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { releasesRequestHandler } from "@homarr/request-handler/releases"; import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createTRPCRouter, publicProcedure } from "../../trpc"; +const logger = createLogger({ module: "releases" }); + const formatVersionFilterRegex = (versionFilter: z.infer | undefined) => { if (!versionFilter) return undefined; @@ -38,7 +41,7 @@ export const releasesRouter = createTRPCRouter({ }), ) .query(async ({ ctx, input }) => { - return await Promise.all( + const settled = await Promise.allSettled( input.repositories.map(async (repository) => { const response = await releasesRequestHandler .handler(ctx.integration, { @@ -56,5 +59,11 @@ export const releasesRouter = createTRPCRouter({ }; }), ); + + return settled.flatMap((result, index) => { + if (result.status === "fulfilled") return [result.value]; + logger.warn("Release fetch failed", { repository: input.repositories[index]?.identifier, error: result.reason }); + return []; + }); }), }); diff --git a/packages/api/src/router/widgets/rssFeed.ts b/packages/api/src/router/widgets/rssFeed.ts index 075d8a91e..5c49ea9db 100644 --- a/packages/api/src/router/widgets/rssFeed.ts +++ b/packages/api/src/router/widgets/rssFeed.ts @@ -5,11 +5,14 @@ import type { Session } from "@homarr/auth"; import type { Database } from "@homarr/db"; import { eq } from "@homarr/db"; import { items, users } from "@homarr/db/schema"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds"; import type { WidgetComponentProps } from "../../../../widgets/src"; import { createTRPCRouter, publicProcedure } from "../../trpc"; +const logger = createLogger({ module: "rssFeed" }); + const feedsInput = z.object({ urls: z.array(z.string()).max(100), maximumAmountPosts: z.number(), @@ -21,7 +24,7 @@ export const rssFeedRouter = createTRPCRouter({ ? input.urls : await restrictUrlsAsync(ctx.db, input.urls); - const rssFeeds = await Promise.all( + const settled = await Promise.allSettled( urls.map(async (url) => { const innerHandler = rssFeedsRequestHandler.handler({ url, @@ -31,8 +34,12 @@ export const rssFeedRouter = createTRPCRouter({ }), ); - return rssFeeds - .flatMap((rssFeed) => rssFeed.data.entries) + return settled + .flatMap((result, index) => { + if (result.status === "fulfilled") return result.value.data.entries; + logger.warn("RSS feed fetch failed", { url: urls[index], error: result.reason }); + return []; + }) .slice(0, input.maximumAmountPosts) .toSorted((entryA, entryB) => { return entryA.published && entryB.published diff --git a/packages/widgets/src/media-requests/list/component.tsx b/packages/widgets/src/media-requests/list/component.tsx index 9b20ce671..0a4fb4598 100644 --- a/packages/widgets/src/media-requests/list/component.tsx +++ b/packages/widgets/src/media-requests/list/component.tsx @@ -200,7 +200,7 @@ interface DecisionButtonsProps { const DecisionButtons = ({ requestId, integrationId }: DecisionButtonsProps) => { const utils = clientApi.useUtils(); const { mutate: mutateRequestAnswer } = clientApi.widget.mediaRequests.answerRequest.useMutation({ - onSettled: () => void utils.widget.mediaRequests.getLatestRequests.invalidate(), + onSettled: () => void utils.widget.mediaRequests.invalidate(), }); const t = useScopedI18n("widget.mediaRequests-requestList"); const handleDecision = (answer: RouterInputs["widget"]["mediaRequests"]["answerRequest"]["answer"]) => { diff --git a/packages/widgets/src/system-resources/component.tsx b/packages/widgets/src/system-resources/component.tsx index 4cacac9ab..36f5d745d 100644 --- a/packages/widgets/src/system-resources/component.tsx +++ b/packages/widgets/src/system-resources/component.tsx @@ -50,7 +50,7 @@ export default function SystemResources({ integrationIds, options }: WidgetCompo const firstItem = data[0]; if (!firstItem) return; setItems((prev) => [...prev, toChartItem(firstItem.healthInfo)].slice(-MAX_QUEUE_SIZE)); - }, [dataUpdatedAt]); + }, [dataUpdatedAt, data]); const showNetwork = items.length === 0 || (items.every((item) => item.network !== null) && options.visibleCharts.includes("network"));