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 { updateMeCache, useMe } from "../lib/useMe"; import { Button } from "./ui/Button"; import InlineNotice from "./ui/InlineNotice"; import SegmentedToggle from "./ui/SegmentedToggle"; import { Section, SettingRow } from "./ui/form"; import MyLogViewer from "./MyLogViewer"; 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()); useEffect(() => { if (!until) return; const id = window.setInterval(() => setNow(Date.now()), 30_000); return () => window.clearInterval(id); }, [until]); if (!until) return null; const minutes = Math.max(0, Math.ceil((Date.parse(until) - now) / 60_000)); return { h: Math.floor(minutes / 60), m: minutes % 60 }; } // Settings → Diagnosztika (E22 S5): 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 } = useTranslation(); 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 (document.activeElement && document.activeElement !== document.body) 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 ( <>
{me.is_demo ? ( {t("settings.diagnostics.demo")} ) : (
({ 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 ? t("settings.diagnostics.hoursMinutes", remaining) : t("settings.diagnostics.minutes", remaining), })}
)}
); }