fix: scope polling to widgets, fix retry, guard dates, throttle umami

- Remove global refetchInterval; scope to widget queries via setQueryDefaults
- Fix error boundary retry: correct queryKey to [["widget"]], move invalidation to onReset
- Add toValidDate() helper in @homarr/common for cache hydration safety
- Guard date formatting in media-releases and media-requests against non-Date values
- Set umami refetchInterval to 60s via shared umamiQueryOptions constant
- Simplify dns-hole mutations to invalidate() instead of manual optimistic setData
This commit is contained in:
Thomas Camlong committed 2026-06-25 17:38:13 +02:00
1 parent 210a62b35a
commit c848dbfc1f
11 files changed
+84 -114

No files matched your search

@@ -56,54 +56,11 @@ export default function DnsHoleControlsWidget({
});
const utils = clientApi.useUtils();
// Mutations for dnsHole state, set to undefined on click, and change again on settle
const { mutate: enableDns } = clientApi.widget.dnsHole.enable.useMutation({
onSettled: (_, error, { integrationId }) => {
utils.widget.dnsHole.summary.setData(
{
integrationIds,
},
(prevData) => {
if (!prevData) return [];
return prevData.map((item) =>
item.integration.id === integrationId
? {
...item,
summary: {
...item.summary,
status: error ? "disabled" : "enabled",
},
}
: item,
);
},
);
},
onSettled: () => void utils.widget.dnsHole.summary.invalidate(),
});
const { mutate: disableDns } = clientApi.widget.dnsHole.disable.useMutation({
onSettled: (_, error, { integrationId }) => {
utils.widget.dnsHole.summary.setData(
{
integrationIds,
},
(prevData) => {
if (!prevData) return [];
return prevData.map((item) =>
item.integration.id === integrationId
? {
...item,
summary: {
...item.summary,
status: error ? "enabled" : "disabled",
},
}
: item,
);
},
);
},
onSettled: () => void utils.widget.dnsHole.summary.invalidate(),
});
const toggleDns = (integrationId: string) => {
const integrationStatus = summaries.find(({ integration }) => integration.id === integrationId);
@@ -6,7 +6,7 @@ import { IconBook, IconCalendar, IconClock, IconStarFilled } from "@tabler/icons
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { getMantineColor } from "@homarr/common";
import { getMantineColor, toValidDate } from "@homarr/common";
import { getIconUrl } from "@homarr/definitions";
import type { MediaRelease } from "@homarr/integrations/types";
import { mediaTypeConfigurations } from "@homarr/integrations/types";
@@ -39,6 +39,19 @@ interface ItemProps {
options: WidgetComponentProps<"mediaReleases">["options"];
}
const formatReleaseDate = (value: unknown, locale: string) => {
const date = toValidDate(value);
if (!date) return "—";
return Intl.DateTimeFormat(locale, {
month: "2-digit",
year: "numeric",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hour12: false,
}).format(date);
};
const Item = ({ item, options }: ItemProps) => {
const locale = useCurrentLocale();
const t = useI18n();
@@ -93,14 +106,7 @@ const Item = ({ item, options }: ItemProps) => {
<Group gap={6} style={{ rowGap: 0 }}>
<Info
icon={IconCalendar}
label={Intl.DateTimeFormat(locale, {
month: "2-digit",
year: "numeric",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hour12: false,
}).format(item.releaseDate)}
label={formatReleaseDate(item.releaseDate, locale)}
/>
{length !== undefined && (
<>
@@ -19,6 +19,7 @@ import { IconSearch, IconThumbDown, IconThumbUp } from "@tabler/icons-react";
import type { RouterInputs, RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
import { toValidDate } from "@homarr/common";
import type { MediaRequestStatus } from "@homarr/integrations/types";
import { mediaAvailabilityConfiguration, mediaRequestStatusConfiguration } from "@homarr/integrations/types";
import { openMediaRequestSearch } from "@homarr/spotlight";
@@ -134,7 +135,7 @@ const MediaRequestCard = ({ request, isTiny, options }: MediaRequestCardProps) =
<Group justify="space-between" gap="xs" className="mediaRequests-list-item-top-group">
<Group gap="xs">
<Text className="mediaRequests-list-item-media-year" size="xs">
{request.airDate?.getFullYear() ?? t("toBeDetermined")}
{toValidDate(request.airDate)?.getFullYear() ?? t("toBeDetermined")}
</Text>
{!isTiny && (
<Badge
+10 -9
View File
@@ -10,7 +10,7 @@ import { useScopedI18n } from "@homarr/translation/client";
import { UmamiEventsContent } from "./umami-events-content";
import { UmamiTopPagesContent, UmamiTopReferrersContent } from "./umami-top-list";
import { formatTimeFrameLabel, formatXLabel } from "./umami-utils";
import { formatTimeFrameLabel, formatXLabel, umamiQueryOptions } from "./umami-utils";
import type { TimeFrame } from "./umami-utils";
interface UmamiContentProps {
@@ -40,19 +40,20 @@ export function UmamiContent({
const { colorScheme } = useMantineColorScheme();
const tickColor = colorScheme === "dark" ? "#c1c2c5" : "#495057";
const { data: results = [] } = clientApi.widget.umami.getVisitorStats.useQuery({
integrationIds,
websiteId,
timeFrame,
eventName,
});
const { data: results = [] } = clientApi.widget.umami.getVisitorStats.useQuery(
{ integrationIds, websiteId, timeFrame, eventName },
umamiQueryOptions,
);
const activeVisitorsInput = { integrationId: integrationIds[0] ?? "", websiteId };
const { data: activeVisitors = 0 } = clientApi.widget.umami.getActiveVisitors.useQuery(activeVisitorsInput);
const { data: activeVisitors = 0 } = clientApi.widget.umami.getActiveVisitors.useQuery(
activeVisitorsInput,
umamiQueryOptions,
);
const { data: multiEventSeries } = clientApi.widget.umami.getMultiEventTimeSeries.useQuery(
{ integrationId: integrationIds[0] ?? "", websiteId, timeFrame, eventNames: [...eventNames].toSorted() },
{ enabled: viewMode === "events" && eventNames.length > 0 },
{ enabled: viewMode === "events" && eventNames.length > 0, ...umamiQueryOptions },
);
const multiEventTotal = multiEventSeries
@@ -7,7 +7,7 @@ import { clientApi } from "@homarr/api/client";
import type { UmamiEventSeries } from "@homarr/integrations/types";
import { useScopedI18n } from "@homarr/translation/client";
import { EVENT_COLORS, formatXLabel } from "./umami-utils";
import { EVENT_COLORS, formatXLabel, umamiQueryOptions } from "./umami-utils";
interface UmamiEventsContentProps {
integrationIds: string[];
@@ -28,12 +28,10 @@ export function UmamiEventsContent({
const { colorScheme } = useMantineColorScheme();
const tickColor = colorScheme === "dark" ? "#c1c2c5" : "#495057";
const { data: series = [] } = clientApi.widget.umami.getMultiEventTimeSeries.useQuery({
integrationId: integrationIds[0] ?? "",
websiteId,
timeFrame,
eventNames: [...eventNames].toSorted(),
});
const { data: series = [] } = clientApi.widget.umami.getMultiEventTimeSeries.useQuery(
{ integrationId: integrationIds[0] ?? "", websiteId, timeFrame, eventNames: [...eventNames].toSorted() },
umamiQueryOptions,
);
if (eventNames.length === 0) {
return (
+10 -12
View File
@@ -6,6 +6,8 @@ import { clientApi } from "@homarr/api/client";
import type { UmamiMetricItem } from "@homarr/integrations/types";
import { useScopedI18n } from "@homarr/translation/client";
import { umamiQueryOptions } from "./umami-utils";
interface UmamiTopListProps {
integrationIds: string[];
websiteId: string;
@@ -15,23 +17,19 @@ interface UmamiTopListProps {
export function UmamiTopPagesContent({ integrationIds, websiteId, timeFrame, limit }: UmamiTopListProps) {
const t = useScopedI18n("widget.umami");
const { data = [] } = clientApi.widget.umami.getTopPages.useQuery({
integrationId: integrationIds[0] ?? "",
websiteId,
timeFrame,
limit,
});
const { data = [] } = clientApi.widget.umami.getTopPages.useQuery(
{ integrationId: integrationIds[0] ?? "", websiteId, timeFrame, limit },
umamiQueryOptions,
);
return <UmamiTopList items={data} heading={t("option.viewMode.option.topPages")} emptyLabel={t("topPages.direct")} />;
}
export function UmamiTopReferrersContent({ integrationIds, websiteId, timeFrame, limit }: UmamiTopListProps) {
const t = useScopedI18n("widget.umami");
const { data = [] } = clientApi.widget.umami.getTopReferrers.useQuery({
integrationId: integrationIds[0] ?? "",
websiteId,
timeFrame,
limit,
});
const { data = [] } = clientApi.widget.umami.getTopReferrers.useQuery(
{ integrationId: integrationIds[0] ?? "", websiteId, timeFrame, limit },
umamiQueryOptions,
);
return (
<UmamiTopList
items={data}
@@ -2,6 +2,8 @@ import type { useScopedI18n } from "@homarr/translation/client";
import { timeFrameValues } from "./index";
export const umamiQueryOptions = { refetchInterval: 60_000 } as const;
export const EVENT_COLORS = ["blue.5", "orange.5", "green.5", "red.5", "violet.5", "teal.5", "yellow.5", "pink.5"];
export function formatXLabel(timestamp: string, timeFrame: string): string {