diff --git a/frontend/src/lib/serviceStatusStore.test.ts b/frontend/src/lib/serviceStatusStore.test.ts index 9e4181f..c37d1ad 100644 --- a/frontend/src/lib/serviceStatusStore.test.ts +++ b/frontend/src/lib/serviceStatusStore.test.ts @@ -82,6 +82,36 @@ describe("serviceStatusStore backend health", () => { }); }); + it("does not probe or accrue failures while the tab is hidden", async () => { + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); + + Object.defineProperty(document, "visibilityState", { + configurable: true, + get: () => "hidden", + }); + + try { + const { useServiceStatusStore } = await import("./serviceStatusStore"); + + await useServiceStatusStore.getState().checkBackend(); + + // A backgrounded tab is throttled by the browser, so a timed-out probe + // would be a false negative. We must not fetch, and must leave the + // previously-known-good status untouched. + expect(fetchMock).not.toHaveBeenCalled(); + expect(useServiceStatusStore.getState()).toMatchObject({ + backend: true, + backendFailCount: 0, + }); + } finally { + Object.defineProperty(document, "visibilityState", { + configurable: true, + get: () => "visible", + }); + } + }); + it("preserves deployment warnings when falling back to the public health probe", async () => { const fetchMock = vi .fn() diff --git a/frontend/src/lib/serviceStatusStore.ts b/frontend/src/lib/serviceStatusStore.ts index f04621e..37269b1 100644 --- a/frontend/src/lib/serviceStatusStore.ts +++ b/frontend/src/lib/serviceStatusStore.ts @@ -77,6 +77,22 @@ export const useServiceStatusStore = create((set, get) => { backendFailCount: 0, checkBackend: async () => { + // A hidden/backgrounded tab has its timers throttled and network + // requests deprioritised by the browser, so the health probe can time + // out even while the backend is perfectly healthy (active recording + // uploads keep succeeding throughout). Skip probing while hidden so we + // never accrue failures — and never raise a false "Server Unreachable" + // alert — during a call the user is watching in another window. The + // visibilitychange handler in ServiceStatusAlerts re-checks immediately + // on return to the foreground. + if ( + typeof document !== "undefined" && + document.visibilityState === "hidden" + ) { + scheduleNextBackend(); + return; + } + const apiBaseUrl = (process.env.NEXT_PUBLIC_API_URL || "/api").replace( /\/$/, "",