refactor: resilient integration queries + simplification

- Extract settleIntegrationQueries helper (Promise.allSettled + logging)
  so one failing integration no longer kills the entire widget
- Convert 16 widget routers from Promise.all to settleIntegrationQueries
- Add onSettled query invalidation to downloads and media-requests mutations
- Inline 4 single-use firewall useUpdating* hooks
- Deduplicate firewall router (4 identical procedures → shared factory)
- Fix system-resources chart duplicate initial point (ref-based dedup)
- Simplify docker table refresh (refetch + isFetching, drop manual state)
- Trim invalidateIntegrationCacheAsync to session-store only (dead scan)
- Fix Beszel getAlerts: inner Promise.allSettled so systems failure
  doesn't discard alerts
This commit is contained in:
Thomas Camlong committed 2026-06-25 11:56:05 +02:00
1 parent cf5934aef6
commit 26a8d219af
24 files changed
+242 -408

No files matched your search

+5 -4
View File
@@ -111,10 +111,11 @@ export default function DownloadClientsWidget({
return statuses;
}, [currentItems]);
//Get API mutation functions
const { mutate: mutateResumeItem } = clientApi.widget.downloads.resumeItem.useMutation();
const { mutate: mutatePauseItem } = clientApi.widget.downloads.pauseItem.useMutation();
const { mutate: mutateDeleteItem } = clientApi.widget.downloads.deleteItem.useMutation();
const utils = clientApi.useUtils();
const invalidateDownloads = { onSettled: () => void utils.widget.downloads.getJobsAndStatuses.invalidate() };
const { mutate: mutateResumeItem } = clientApi.widget.downloads.resumeItem.useMutation(invalidateDownloads);
const { mutate: mutatePauseItem } = clientApi.widget.downloads.pauseItem.useMutation(invalidateDownloads);
const { mutate: mutateDeleteItem } = clientApi.widget.downloads.deleteItem.useMutation(invalidateDownloads);
//Flatten Data array for which each element has it's integration, data (base + calculated) and actions. Memoized on data subscription
const data = useMemo<ExtendedDownloadClientItem[]>(
+4 -35
View File
@@ -29,10 +29,10 @@ export default function FirewallWidget({ integrationIds, width, itemId }: Widget
}
}, []);
const firewallsCpuData = useUpdatingCpuStatus(integrationIds);
const firewallsMemoryData = useUpdatingMemoryStatus(integrationIds);
const firewallsVersionData = useUpdatingVersionStatus(integrationIds);
const firewallsInterfacesData = useUpdatingInterfacesStatus(integrationIds);
const { data: firewallsCpuData = [] } = clientApi.widget.firewall.getFirewallCpuStatus.useQuery({ integrationIds });
const { data: firewallsMemoryData = [] } = clientApi.widget.firewall.getFirewallMemoryStatus.useQuery({ integrationIds });
const { data: firewallsVersionData = [] } = clientApi.widget.firewall.getFirewallVersionStatus.useQuery({ integrationIds });
const { data: firewallsInterfacesData = [] } = clientApi.widget.firewall.getFirewallInterfacesStatus.useQuery({ integrationIds });
const initialSelectedFirewall = firewallsVersionData[0] ? firewallsVersionData[0].integration.id : "undefined";
const isTiny = width < 256;
@@ -182,37 +182,6 @@ export default function FirewallWidget({ integrationIds, width, itemId }: Widget
);
}
export const useUpdatingCpuStatus = (integrationIds: string[]) => {
const { data: firewallsCpuData = [] } = clientApi.widget.firewall.getFirewallCpuStatus.useQuery({
integrationIds,
});
return firewallsCpuData;
};
export const useUpdatingMemoryStatus = (integrationIds: string[]) => {
const { data: firewallsMemoryData = [] } = clientApi.widget.firewall.getFirewallMemoryStatus.useQuery({
integrationIds,
});
return firewallsMemoryData;
};
export const useUpdatingVersionStatus = (integrationIds: string[]) => {
const { data: firewallsVersionData = [] } = clientApi.widget.firewall.getFirewallVersionStatus.useQuery({
integrationIds,
});
return firewallsVersionData;
};
export const useUpdatingInterfacesStatus = (integrationIds: string[]) => {
const { data: firewallsInterfacesData = [] } = clientApi.widget.firewall.getFirewallInterfacesStatus.useQuery({
integrationIds,
});
return firewallsInterfacesData;
};
export function formatBitsPerSec(bytes: number, decimals: number): string {
if (bytes === 0) return "0 b/s";
@@ -198,7 +198,10 @@ interface DecisionButtonsProps {
}
const DecisionButtons = ({ requestId, integrationId }: DecisionButtonsProps) => {
const { mutate: mutateRequestAnswer } = clientApi.widget.mediaRequests.answerRequest.useMutation();
const utils = clientApi.useUtils();
const { mutate: mutateRequestAnswer } = clientApi.widget.mediaRequests.answerRequest.useMutation({
onSettled: () => void utils.widget.mediaRequests.getLatestRequests.invalidate(),
});
const t = useScopedI18n("widget.mediaRequests-requestList");
const handleDecision = (answer: RouterInputs["widget"]["mediaRequests"]["answerRequest"]["answer"]) => {
mutateRequestAnswer({
@@ -1,6 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { Box, Group, Stack } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
@@ -43,7 +43,10 @@ export default function SystemResources({ integrationIds, options }: WidgetCompo
{ cpu: number; memory: number; gpu: number; network: { up: number; down: number } | null }[]
>(() => (data[0] ? [toChartItem(data[0].healthInfo)] : []));
const lastUpdatedAt = useRef(dataUpdatedAt);
useEffect(() => {
if (dataUpdatedAt === lastUpdatedAt.current) return;
lastUpdatedAt.current = dataUpdatedAt;
const firstItem = data[0];
if (!firstItem) return;
setItems((prev) => [...prev, toChartItem(firstItem.healthInfo)].slice(-MAX_QUEUE_SIZE));