diff --git a/frontend/src/components/DiagnosticsSettings.dom.test.tsx b/frontend/src/components/DiagnosticsSettings.dom.test.tsx index f4fb8c7b..882c6c65 100644 --- a/frontend/src/components/DiagnosticsSettings.dom.test.tsx +++ b/frontend/src/components/DiagnosticsSettings.dom.test.tsx @@ -16,8 +16,14 @@ vi.mock("react-i18next", async (importOriginal) => { return { ...actual, useTranslation: () => ({ - t: (key: string, opts?: { count?: number }) => - opts?.count != null ? `${key}:${opts.count}` : key, + t: (key: string, opts?: { count?: number; h?: number; m?: number; time?: string }) => + opts?.count != null + ? `${key}:${opts.count}` + : opts?.m != null + ? `${key}:${opts.h ?? 0}h${opts.m}m` + : opts?.time != null + ? `${key}:${opts.time}` + : key, i18n: { language: "en" }, }), }; @@ -106,6 +112,23 @@ describe("DiagnosticsSettings", () => { expect(setLevel).toHaveBeenLastCalledWith("INFO"); }); + it("counts the 24 hours from the moment the level was raised, not from when the page opened", async () => { + vi.useFakeTimers({ toFake: ["Date"] }); + try { + vi.setSystemTime(new Date("2026-09-19T02:54:00Z")); + mount({}); + vi.setSystemTime(new Date("2026-09-19T02:56:30Z")); // the page sat open for 2.5 minutes + fireEvent.click(levelButton("settings.diagnostics.levels.DEBUG")); + fireEvent.click(screen.getByTestId("diagnostics-consent-confirm")); + await waitFor(() => expect(screen.getByTestId("diagnostics-status")).toBeTruthy()); + expect(screen.getByTestId("diagnostics-status").textContent).toContain( + "settings.diagnostics.hoursMinutes:24h0m", + ); + } finally { + vi.useRealTimers(); + } + }); + it("going back to the base level needs no consent", async () => { const until = new Date(Date.now() + 3_600_000).toISOString(); mount({ diagnostics: { level: "DEBUG", until, retention_days: 7 } }); diff --git a/frontend/src/components/DiagnosticsSettings.tsx b/frontend/src/components/DiagnosticsSettings.tsx index c1a67440..420459ed 100644 --- a/frontend/src/components/DiagnosticsSettings.tsx +++ b/frontend/src/components/DiagnosticsSettings.tsx @@ -15,14 +15,17 @@ const LEVELS: readonly DiagnosticsLevel[] = ["INFO", "DEBUG", "TRACE"]; /** Hours and minutes until `until`, refreshed every 30 s — the status line counts down. */ function useRemaining(until: string | null): { h: number; m: number } | null { - const [now, setNow] = useState(() => Date.now()); + // The clock is read at render, and the interval only re-renders: a `now` held in state was the + // moment the page MOUNTED, so a level raised two minutes after opening Settings read "24 h 2 m" + // until the first tick (prod UAT 2026-09-19). + const [, tick] = useState(0); useEffect(() => { if (!until) return; - const id = window.setInterval(() => setNow(Date.now()), 30_000); + const id = window.setInterval(() => tick((n) => n + 1), 30_000); return () => window.clearInterval(id); }, [until]); if (!until) return null; - const minutes = Math.max(0, Math.ceil((Date.parse(until) - now) / 60_000)); + const minutes = Math.max(0, Math.ceil((Date.parse(until) - Date.now()) / 60_000)); return { h: Math.floor(minutes / 60), m: minutes % 60 }; }