import { useEffect, useRef, useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useTranslation } from "react-i18next"; import { api, type DiagnosticsLevel } from "../lib/api"; import { levelInForce } from "../lib/diagnostics"; import { focusIsUnclaimed } from "../lib/useFocusKeeper"; import { updateMeCache, useMe } from "../lib/useMe"; import { Button } from "./ui/Button"; import SegmentedToggle from "./ui/SegmentedToggle"; import { Section, SettingRow } from "./ui/form"; import MyLogViewer from "./MyLogViewer"; import { RecorderSettings } from "./DebugRecorder"; import { formatUnit } from "../lib/format"; 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 { // 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(() => tick((n) => n + 1), 30_000); return () => window.clearInterval(id); }, [until]); if (!until) return null; // eslint-disable-next-line react-hooks/purity -- same as DebugRecorder's elapsed clock, and the comment above records the prod UAT finding that a state-held `now` produced: a level raised two minutes after opening Settings read "24 h 2 m" until the first tick. const minutes = Math.max(0, Math.ceil((Date.parse(until) - Date.now()) / 60_000)); return { h: Math.floor(minutes / 60), m: minutes % 60 }; } // Settings → Diagnosztika: the consented diagnostics level and the tab's own log. // // There is no separate on/off switch: Alap IS off. Choosing Részletes or Teljes does not change // anything yet — it opens the consent box under the picker, which says what will be recorded, that // the server keeps it and for how long, and that it lapses; only its button raises the level. // Returning to Alap needs no consent and happens at once. export default function DiagnosticsSettings() { const { t, i18n } = useTranslation(); const lang = i18n.language; const me = useMe(); const qc = useQueryClient(); const state = me.diagnostics; const current = levelInForce(state); const [pending, setPending] = useState(null); const remaining = useRemaining(current === "INFO" ? null : (state?.until ?? null)); // The consent box and the status line each go away with the button that closed them — the one // holding the focus. Put it back on the level picker (its chosen option) rather than let it fall // to , where the next Tab starts over from the top of the page. Only when it did fall: a // user who moved on while the request was out keeps their place. // // Watched until that button is actually GONE, not for one render: a saved level arrives as two // updates (the `me` cache, then the closed box), and the first render still has the button in the // page — measured in Chrome, where a one-shot check spent itself there and the focus then fell. const pickerRef = useRef(null); const closedFrom = useRef(null); useEffect(() => { const from = closedFrom.current; if (!from || from.isConnected) return; closedFrom.current = null; if (!focusIsUnclaimed()) return; pickerRef.current?.querySelector('[aria-pressed="true"]')?.focus(); }); const watchFocus = () => { closedFrom.current = document.activeElement; }; const change = useMutation({ mutationFn: api.setDiagnosticsLevel, meta: { failTitle: "settings.diagnostics.failed" }, onSuccess: (result) => { watchFocus(); updateMeCache(qc, (prev) => ({ ...prev, diagnostics: result.diagnostics })); setPending(null); }, }); const cancel = () => { watchFocus(); setPending(null); }; const choose = (level: DiagnosticsLevel) => { if (level === "INFO") { setPending(null); if (current !== "INFO") change.mutate("INFO"); } else { setPending(level === current ? null : level); } }; const levelName = (level: DiagnosticsLevel) => t(`settings.diagnostics.levels.${level}`); return ( <>
({ value: level, label: levelName(level) }))} />
{pending && (

{t(`settings.diagnostics.consent.title.${pending}`)}

  • {t(`settings.diagnostics.consent.what.${pending}`)}
  • {t("settings.diagnostics.consent.server", { count: state?.retention_days ?? 7, })}
  • {t("settings.diagnostics.consent.never")}
  • {t("settings.diagnostics.consent.lapse")}{" "} {t("settings.diagnostics.consent.policy")}
)} {current !== "INFO" && !pending && remaining && ( // Not a live region: the countdown changes every minute, and a screen reader would read // the whole line out again each time for as long as the page is open.
{t("settings.diagnostics.status", { level: levelName(current), time: remaining.h > 0 ? `${formatUnit(remaining.h, "hour", lang)} ${formatUnit(remaining.m, "minute", lang)}` : formatUnit(remaining.m, "minute", lang), })}
)}
); }