fix(demo): stop seeded widget background polling (#6980)

* fix(demo): stop seeded widget background polling

* docs(demo): clarify single-replica requirements

* fix: abort startup when auth secret generation fails

* docs: remove demo deployment changes from PR

* fix: scope demo polling changes
This commit is contained in:
Thomas authored and GitHub committed 2026-10-05 09:51:35 +02:00
1 parent a17845aa1c
commit 4dbe4343e5
10 files changed
+48 -19

No files matched your search

@@ -34,6 +34,7 @@ import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared";
import { useSession } from "@homarr/auth/client";
import { env } from "@homarr/common/env";
import { showWarningNotification } from "@homarr/notifications";
import { DemoReadOnlyProvider } from "@homarr/widgets/demo-read-only";
import { widgetQueryRefetchIntervals } from "@homarr/widgets/refetch-intervals";
import { useAuthContext } from "./session";
@@ -66,7 +67,7 @@ const constructWebsocketUrl = () => {
return `${getWebSocketProtocol()}://${window.location.hostname}:${window.location.port}/websockets`;
};
export function TRPCReactProvider({ children }: PropsWithChildren) {
export function TRPCReactProvider({ children, demoReadOnly }: PropsWithChildren<{ demoReadOnly: boolean }>) {
const { data: session } = useSession();
const { logoutRedirectInProgress } = useAuthContext();
const sessionQueryScope = getSessionQueryScope(session);
@@ -81,7 +82,7 @@ export function TRPCReactProvider({ children }: PropsWithChildren) {
currentScope={sessionQueryScope}
onScopeChange={handleScopeChange}
>
<ScopedTRPCReactProvider>{children}</ScopedTRPCReactProvider>
<ScopedTRPCReactProvider demoReadOnly={demoReadOnly}>{children}</ScopedTRPCReactProvider>
</SessionQueryScopeGuard>
);
}
@@ -102,7 +103,7 @@ const clearLegacyDashboardPersistence = () => {
}
};
const ScopedTRPCReactProvider = ({ children }: PropsWithChildren) => {
const ScopedTRPCReactProvider = ({ children, demoReadOnly }: PropsWithChildren<{ demoReadOnly: boolean }>) => {
useEffect(clearLegacyDashboardPersistence, []);
const wsClient = useMemo(
() =>
@@ -150,21 +151,26 @@ const ScopedTRPCReactProvider = ({ children }: PropsWithChildren) => {
},
});
client.setQueryDefaults([["widget"]], {
refetchInterval: queryCacheDefaultRefetchIntervalMs,
refetchInterval: demoReadOnly ? false : queryCacheDefaultRefetchIntervalMs,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
});
for (const queryDefaults of widgetQueryRefetchIntervals) {
const policy: { refetchInterval?: number | false; staleTime?: number } = {};
if (queryDefaults.intervalSeconds === null) policy.refetchInterval = false;
if (typeof queryDefaults.intervalSeconds === "number") {
if (demoReadOnly || queryDefaults.intervalSeconds === null) policy.refetchInterval = false;
else if (typeof queryDefaults.intervalSeconds === "number") {
policy.refetchInterval = queryDefaults.intervalSeconds * 1000;
}
if ("staleTimeSeconds" in queryDefaults) policy.staleTime = queryDefaults.staleTimeSeconds * 1000;
client.setQueryDefaults(queryDefaults.queryKey, policy);
}
for (const { queryKey, ...policy } of dashboardSupportingQueryPolicies) {
client.setQueryDefaults(queryKey, policy);
client.setQueryDefaults(
queryKey,
demoReadOnly
? { ...policy, refetchInterval: false, refetchOnWindowFocus: false, refetchOnReconnect: false }
: policy,
);
}
if (typeof window === "undefined") {
client.getQueryCache().subscribe((event) => {
@@ -239,7 +245,7 @@ const ScopedTRPCReactProvider = ({ children }: PropsWithChildren) => {
},
}}
>
{children}
<DemoReadOnlyProvider value={demoReadOnly}>{children}</DemoReadOnlyProvider>
</ReactQueryStreamedHydration>
{process.env.NODE_ENV === "development" && <DevelopmentTools />}
</QueryClientProvider>
+1 -1
View File
@@ -167,7 +167,7 @@ export default async function Layout(props: {
/>
),
(innerProps) => <JotaiProvider {...innerProps} />,
(innerProps) => <TRPCReactProvider {...innerProps} />,
(innerProps) => <TRPCReactProvider demoReadOnly={env.DEMO_MODE && env.DEMO_READ_ONLY} {...innerProps} />,
(innerProps) => <DayJsLoader {...innerProps} />,
(innerProps) => <NextIntlClientProvider {...innerProps} />,
(innerProps) => (
+1
View File
@@ -19,6 +19,7 @@
"./custom-api/component": "./src/custom-api/component.tsx",
"./custom-api/custom-jsx-display": "./src/custom-api/custom-jsx-display.tsx",
"./custom-api/extract-display-data": "./src/custom-api/extract-display-data.ts",
"./demo-read-only": "./src/demo-read-only.tsx",
"./assistant/context": "./src/assistant/context.tsx",
"./catalog": "./src/catalog.ts",
"./definition": "./src/definition.ts",
@@ -28,6 +28,7 @@ import {
import { createByteChartAxisFormatters, formatPercent } from "./format";
import { makeTooltipProps } from "./tooltip";
import { useLiveStats } from "./use-live-stats";
import { useDemoReadOnly } from "../../demo-read-only";
const CHART_HEIGHT = 180;
const MEBIBYTE = 1024 * 1024;
@@ -97,9 +98,10 @@ export function BeszelStatsView({
);
const showDocker = visibility.dockerCpu || visibility.dockerMemory || visibility.dockerNetwork;
const isLive = timePeriod === "1m";
const demoReadOnly = useDemoReadOnly();
const historicalQuery = clientApi.widget.beszel.getSystemStats.useQuery(
{ integrationIds, systemId, timePeriod, includeDocker: showDocker },
{ refetchInterval: isLive ? false : 5_000, enabled: !isLive && systemId !== "" },
{ refetchInterval: isLive || demoReadOnly ? false : 5_000, enabled: !isLive && systemId !== "" },
);
const historicalData = isLive ? historicalQuery.data : getUsableWidgetQueryData(historicalQuery);
const { data: liveData, error: liveError } = useLiveStats(integrationIds, systemId, isLive && systemId !== "");
@@ -11,6 +11,7 @@ import { Link, zoomCompensatedSize } from "@homarr/ui";
import { WidgetQueryLoadingState } from "../common/query-state-indicator";
import type { WidgetComponentProps } from "../definition";
import { useDemoReadOnly } from "../demo-read-only";
import { isLegacyCustomWidgetMigrationError, isTerminalCustomWidgetDefinitionError } from "./migration-state";
const CustomJsxDisplay = dynamic(() => import("./custom-jsx-display"), { ssr: false });
@@ -23,6 +24,7 @@ export default function CustomApiWidget({
}: WidgetComponentProps<"customApi">) {
const t = useI18n("widget.customApi");
const tCustomJsx = useI18n("widget.customApi.customJsx");
const demoReadOnly = useDemoReadOnly();
const { data: session } = useSession();
const isAdmin = session?.user.permissions.includes("admin") ?? false;
const definitionId = options.definitionId;
@@ -33,8 +35,9 @@ export default function CustomApiWidget({
{
enabled: Boolean(itemId) && Boolean(definitionId),
refetchInterval: (currentQuery) =>
isTerminalCustomWidgetDefinitionError(currentQuery.state.error) ? false : intervalMs,
retry: (failureCount, error) => !isTerminalCustomWidgetDefinitionError(error) && failureCount < 3,
demoReadOnly || isTerminalCustomWidgetDefinitionError(currentQuery.state.error) ? false : intervalMs,
retry: (failureCount, error) =>
!demoReadOnly && !isTerminalCustomWidgetDefinitionError(error) && failureCount < 3,
},
);
+9
View File
@@ -0,0 +1,9 @@
"use client";
import { createContext, useContext } from "react";
const DemoReadOnlyContext = createContext(false);
export const DemoReadOnlyProvider = DemoReadOnlyContext.Provider;
export const useDemoReadOnly = () => useContext(DemoReadOnlyContext);
+6 -4
View File
@@ -22,6 +22,7 @@ import { clientApi } from "@homarr/api/client";
import { useI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../definition";
import { useDemoReadOnly } from "../demo-read-only";
import { useWidgetRuntimeActions } from "../runtime-hooks";
import { statsEntriesSchema } from "./config";
import { StatsTable } from "./table";
@@ -41,6 +42,7 @@ export default function StatsWidget({
displayScale = 1,
}: WidgetComponentProps<"stats">) {
const board = useRequiredBoard();
const demoReadOnly = useDemoReadOnly();
const advanced = displayMode === "advanced";
const t = useI18n("widget.stats");
const utils = clientApi.useUtils();
@@ -81,8 +83,8 @@ export default function StatsWidget({
{ integrationId },
{
staleTime: 30_000,
refetchInterval: 60_000,
refetchOnWindowFocus: true,
refetchInterval: demoReadOnly ? false : 60_000,
refetchOnWindowFocus: !demoReadOnly,
refetchIntervalInBackground: false,
retry: false,
},
@@ -153,13 +155,13 @@ export default function StatsWidget({
});
useEffect(() => {
if (document.hidden) return;
if (demoReadOnly || document.hidden) return;
for (const [index, integrationId] of visibleIds.entries()) {
const snapshot = snapshots[index];
if (!snapshot?.data || snapshot.error) continue;
if (snapshot.data.updatedAt === null && snapshot.data.retryAt <= Date.now()) void refresh(integrationId, false);
}
}, [visibleIds, snapshots, refresh]);
}, [demoReadOnly, visibleIds, snapshots, refresh]);
const tableEntries = advanced
? ids.flatMap((id) => {
+2 -1
View File
@@ -12,7 +12,7 @@ import classes from "./component.module.css";
import { UmamiEventsContent } from "./umami-events-content";
import { getUmamiLayout } from "./umami-layout";
import { UmamiTopPagesContent, UmamiTopReferrersContent } from "./umami-top-list";
import { formatTimeFrameLabel, formatXLabel, umamiQueryOptions } from "./umami-utils";
import { formatTimeFrameLabel, formatXLabel, useUmamiQueryOptions } from "./umami-utils";
import type { TimeFrame } from "./umami-utils";
import { getUsableWidgetQueryData, isInitialWidgetQueryPending } from "../common/query-state";
import { WidgetQueryLoadingState } from "../common/query-state-indicator";
@@ -48,6 +48,7 @@ export function UmamiContent({
}: UmamiContentProps) {
const t = useI18n("widget.umami");
const locale = useCurrentIntlLocale();
const umamiQueryOptions = useUmamiQueryOptions();
const tickColor = "var(--mantine-color-dimmed)";
const statsQuery = clientApi.widget.umami.getVisitorStats.useQuery(
@@ -7,7 +7,7 @@ import type { UmamiMetricItem } from "@homarr/integrations/types";
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
import classes from "./component.module.css";
import { umamiQueryOptions } from "./umami-utils";
import { useUmamiQueryOptions } from "./umami-utils";
import { getUsableWidgetQueryData } from "../common/query-state";
interface UmamiTopListProps {
@@ -19,6 +19,7 @@ interface UmamiTopListProps {
export function UmamiTopPagesContent({ integrationIds, websiteId, timeFrame, limit }: UmamiTopListProps) {
const t = useI18n("widget.umami");
const umamiQueryOptions = useUmamiQueryOptions();
const data =
getUsableWidgetQueryData(
clientApi.widget.umami.getTopPages.useQuery(
@@ -31,6 +32,7 @@ export function UmamiTopPagesContent({ integrationIds, websiteId, timeFrame, lim
export function UmamiTopReferrersContent({ integrationIds, websiteId, timeFrame, limit }: UmamiTopListProps) {
const t = useI18n("widget.umami");
const umamiQueryOptions = useUmamiQueryOptions();
const data =
getUsableWidgetQueryData(
clientApi.widget.umami.getTopReferrers.useQuery(
+4 -1
View File
@@ -1,8 +1,11 @@
import type { useI18n } from "@homarr/translation/client";
import { useDemoReadOnly } from "../demo-read-only";
import { timeFrameValues } from "./index";
export const umamiQueryOptions = { refetchInterval: 60_000 } as const;
export const useUmamiQueryOptions = (): { refetchInterval: number | false } => ({
refetchInterval: useDemoReadOnly() ? false : 60_000,
});
export const EVENT_COLORS = ["blue.5", "orange.5", "green.5", "red.5", "violet.5", "teal.5", "yellow.5", "pink.5"];