Skip to content

Commit 7439767

Browse files
authored
fix(dashboard): stop automatic health polling (#331)
1 parent 0b41be7 commit 7439767

5 files changed

Lines changed: 133 additions & 18 deletions

File tree

apps/dashboard/app/(cockpit)/cockpit-shell.test.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,20 @@ test("returning to the tab restores the refresh cycle, not a single refresh", (t
413413

414414
// ── The LIVE badge ──────────────────────────────────────────────────────────
415415

416+
test("health never joins global polling, even when Live was persisted", (t) => {
417+
beginTest(t, { livePolling: true });
418+
const { refreshes, root } = mountShell(t, "/health", <div>Health</div>);
419+
420+
advance(60_000);
421+
422+
assert.equal(refreshes.length, 0, "health probes ran without a manual scan");
423+
assert.equal(
424+
root.findAllByProps({ "aria-label": "Toggle live updates" }).length,
425+
0,
426+
"health showed a Live control that cannot safely apply to this screen",
427+
);
428+
});
429+
416430
test("the badge does not claim live data while the tab is hidden and nothing polls", (t) => {
417431
// The worst failure mode: a frozen screen that also tells the user it is
418432
// current, removing their only cue to reload.

apps/dashboard/app/(cockpit)/cockpit-shell.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,15 @@ export function CockpitShell({
132132
// cadence. A surface with nothing in flight still watches for new work, just
133133
// slowly — that is the AIW-266 criterion "polling stops or slows when no
134134
// active runs are present", and it is what lets a new run appear in the list.
135-
const livePollFast = runRefreshCadence === "live" || !!t.livePolling;
136-
const livePollEnabled = livePollFast || runRefreshCadence === "idle";
135+
// Health probes hit every configured provider. They are intentionally
136+
// user-triggered so a persisted global Live preference cannot turn one open
137+
// health tab into a continuous fan-out of production requests.
138+
const globalPollingAllowed = screen !== "health";
139+
const livePollFast =
140+
globalPollingAllowed && (runRefreshCadence === "live" || !!t.livePolling);
141+
const livePollEnabled =
142+
globalPollingAllowed &&
143+
(livePollFast || runRefreshCadence === "idle");
137144
const liveCycleMs = livePollFast ? LIVE_POLL_MS : IDLE_POLL_MS;
138145

139146
// Timestamp of the next scheduled refresh, surfaced via context so the
@@ -189,16 +196,19 @@ export function CockpitShell({
189196
<main className="flex-1 flex flex-col min-w-0 min-h-0">
190197
{/* Mobile header */}
191198
<div className="lg:hidden">
192-
<MobileHeader title={TITLE_FOR_SCREEN[screen] ?? "AI Workflow"} />
199+
<MobileHeader
200+
title={TITLE_FOR_SCREEN[screen] ?? "AI Workflow"}
201+
showLivePoll={globalPollingAllowed}
202+
/>
193203
</div>
194204

195-
{/* Desktop top bar — global live-poll control, present on every screen */}
205+
{/* Desktop top bar — live polling is omitted for expensive health probes */}
196206
<div className="hidden lg:flex items-center justify-between flex-[0_0_44px] h-11 border-b border-neutral-200 bg-panel px-6">
197207
<span className="font-mono text-[10px] uppercase tracking-[0.06em] text-neutral-500">
198208
{TITLE_FOR_SCREEN[screen] ?? "AI Workflow"}
199209
</span>
200210
<div className="flex items-center gap-4">
201-
<LivePollControl />
211+
{globalPollingAllowed && <LivePollControl />}
202212
<LogoutButton />
203213
</div>
204214
</div>

apps/dashboard/components/cockpit/mobile/mobile-header.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,22 @@
44
import { BlazityLogo } from "@/components/ui";
55
import { LivePollControl } from "@/components/cockpit/controls";
66

7-
export function MobileHeader({ title }: { title: string }) {
7+
export function MobileHeader({
8+
title,
9+
showLivePoll = true,
10+
}: {
11+
title: string;
12+
showLivePoll?: boolean;
13+
}) {
814
return (
915
<header className="flex-[0_0_auto] h-12 bg-panel border-b border-neutral-200 flex items-center gap-2 px-4">
1016
<BlazityLogo size={20} color="#FD6027" wordmarkColor="#181B20" showWord={false} />
1117
<span className="font-display font-medium text-[15px] text-coal">{title}</span>
12-
<div className="ml-auto">
13-
<LivePollControl size="sm" />
14-
</div>
18+
{showLivePoll && (
19+
<div className="ml-auto">
20+
<LivePollControl size="sm" />
21+
</div>
22+
)}
1523
</header>
1624
);
1725
}

apps/dashboard/components/cockpit/screens/health.test.tsx

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import assert from "node:assert/strict";
2-
import test from "node:test";
2+
import test, { mock } from "node:test";
33
import React from "react";
44
import { act, create } from "react-test-renderer";
55
import { AppRouterContext } from "next/dist/shared/lib/app-router-context.shared-runtime";
@@ -70,19 +70,68 @@ test("health screen exposes the failed service, fix hint, and safe env names", (
7070
act(() => renderer.unmount());
7171
});
7272

73-
test("scan again refreshes the server data", () => {
73+
test("scan again runs once and recovers when fresh data arrives", () => {
7474
let refreshes = 0;
7575
const router = { refresh: () => refreshes++ };
76+
const tree = (nextData: SystemHealthResponse) => (
77+
<AppRouterContext.Provider value={router as never}>
78+
<HealthScreen data={nextData} />
79+
</AppRouterContext.Provider>
80+
);
81+
let renderer!: ReturnType<typeof create>;
82+
act(() => {
83+
renderer = create(tree(data));
84+
});
85+
const button = renderer.root.findByProps({ children: "Scan again" });
86+
act(() => {
87+
button.props.onClick();
88+
button.props.onClick();
89+
});
90+
assert.equal(refreshes, 1);
91+
assert.equal(
92+
renderer.root.findByProps({ children: "Scanning…" }).props.disabled,
93+
true,
94+
);
95+
96+
act(() => {
97+
renderer.update(
98+
tree({
99+
...data,
100+
generatedAt: "2026-08-20T12:00:01.000Z",
101+
}),
102+
);
103+
});
104+
assert.equal(
105+
renderer.root.findByProps({ children: "Scan again" }).props.disabled,
106+
false,
107+
);
108+
act(() => renderer.unmount());
109+
});
110+
111+
test("scan again recovers if a refresh never commits", (t) => {
112+
mock.timers.enable({ apis: ["setTimeout"] });
113+
t.after(() => mock.timers.reset());
114+
115+
const router = { refresh() {} };
76116
let renderer!: ReturnType<typeof create>;
77117
act(() => {
78118
renderer = create(
79119
<AppRouterContext.Provider value={router as never}>
80-
<HealthScreen data={{ ...data, alerts: [], summary: { ...data.summary, down: 0, criticalDown: 0 } }} />
120+
<HealthScreen data={data} />
81121
</AppRouterContext.Provider>,
82122
);
83123
});
84-
const button = renderer.root.findByProps({ children: "Scan again" });
85-
act(() => button.props.onClick());
86-
assert.equal(refreshes, 1);
124+
act(() => renderer.root.findByProps({ children: "Scan again" }).props.onClick());
125+
assert.equal(
126+
renderer.root.findByProps({ children: "Scanning…" }).props.disabled,
127+
true,
128+
);
129+
130+
act(() => mock.timers.tick(15_000));
131+
132+
assert.equal(
133+
renderer.root.findByProps({ children: "Scan again" }).props.disabled,
134+
false,
135+
);
87136
act(() => renderer.unmount());
88137
});

apps/dashboard/components/cockpit/screens/health.tsx

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useTransition } from "react";
3+
import { useEffect, useRef, useState } from "react";
44
import { useRouter } from "next/navigation";
55
import type {
66
SystemHealthGroup,
@@ -82,9 +82,43 @@ const STATUS: Record<
8282
},
8383
};
8484

85+
const REFRESH_TIMEOUT_MS = 15_000;
86+
8587
export function HealthScreen({ data }: { data: SystemHealthResponse }) {
8688
const router = useRouter();
87-
const [refreshing, startRefresh] = useTransition();
89+
const [refreshing, setRefreshing] = useState(false);
90+
const refreshInFlight = useRef(false);
91+
const refreshStartedAt = useRef<string | null>(null);
92+
93+
useEffect(() => {
94+
if (
95+
!refreshInFlight.current ||
96+
refreshStartedAt.current === data.generatedAt
97+
) {
98+
return;
99+
}
100+
refreshInFlight.current = false;
101+
refreshStartedAt.current = null;
102+
setRefreshing(false);
103+
}, [data.generatedAt]);
104+
105+
useEffect(() => {
106+
if (!refreshing) return;
107+
const timeout = setTimeout(() => {
108+
refreshInFlight.current = false;
109+
refreshStartedAt.current = null;
110+
setRefreshing(false);
111+
}, REFRESH_TIMEOUT_MS);
112+
return () => clearTimeout(timeout);
113+
}, [refreshing]);
114+
115+
const refresh = () => {
116+
if (refreshInFlight.current) return;
117+
refreshInFlight.current = true;
118+
refreshStartedAt.current = data.generatedAt;
119+
setRefreshing(true);
120+
router.refresh();
121+
};
88122
const criticalAlerts = data.alerts.filter((alert) => alert.severity === "critical");
89123
const overall = criticalAlerts.length > 0
90124
? {
@@ -131,7 +165,7 @@ export function HealthScreen({ data }: { data: SystemHealthResponse }) {
131165
<button
132166
type="button"
133167
disabled={refreshing}
134-
onClick={() => startRefresh(() => router.refresh())}
168+
onClick={refresh}
135169
className="appearance-none rounded-[3px] border border-neutral-300 bg-panel px-3 py-2 font-body text-[12px] font-semibold text-neutral-800 transition-colors duration-[120ms] hover:border-neutral-400 hover:bg-app-bg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-mariner disabled:cursor-wait disabled:opacity-60"
136170
>
137171
{refreshing ? "Scanning…" : "Scan again"}

0 commit comments

Comments
 (0)