import { useEffect, useState } from "react"; import { api, type DiagLogEntry } from "../api/client"; import { formatDateTime, parseDbTimestamp } from "../utils/date"; import { useSortable } from "../hooks/useSortable"; import SortableTh from "../components/SortableTh"; import { downloadCsv } from "../utils/csv"; import { confirmDialog } from "../utils/dialogs"; const SOURCE_LABELS: Record = { cloudflare: "Cloudflare", loopia: "Loopia", pihole: "Pi-hole", azure: "Azure DNS", cpanel: "cPanel", technitium: "Technitium", tailscale: "Tailscale", proxmox: "Proxmox", synology: "Synology", semaphore: "Semaphore", gitea: "Gitea", dockhand: "Dockhand", }; const PAGE_SIZE = 100; export default function DiagLog() { const [entries, setEntries] = useState(null); const [total, setTotal] = useState(0); const [page, setPage] = useState(0); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [filterSource, setFilterSource] = useState(""); const [filterOk, setFilterOk] = useState(""); const [clearing, setClearing] = useState(false); function load(pg: number) { setLoading(true); setError(null); api.diagLog .list({ source: filterSource || undefined, ok: filterOk === "" ? undefined : filterOk === "true", limit: PAGE_SIZE, offset: pg * PAGE_SIZE, }) .then((res) => { setEntries(res.entries); setTotal(res.total); }) .catch((err) => setError(err instanceof Error ? err.message : String(err))) .finally(() => setLoading(false)); } useEffect(() => { setPage(0); load(0); // eslint-disable-next-line react-hooks/exhaustive-deps }, [filterSource, filterOk]); function goToPage(pg: number) { setPage(pg); load(pg); } async function handleClear() { if (!(await confirmDialog({ title: "Clear diagnostic log", message: "Clear the entire diagnostic log? This cannot be undone.", confirmLabel: "Clear log", danger: true }))) return; setClearing(true); try { await api.diagLog.clear(); setEntries([]); setTotal(0); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setClearing(false); } } const { sorted, sortKey, sortDir, requestSort } = useSortable(entries, "createdAt", "desc"); const totalPages = Math.ceil(total / PAGE_SIZE); const errorCount = entries?.filter((e) => !e.ok).length ?? 0; function exportCsv() { if (!sorted) return; downloadCsv( "diagnostic-log.csv", ["Time", "Source", "Operation", "OK", "Latency (ms)", "Error"], sorted.map((e) => [formatDateTime(parseDbTimestamp(e.createdAt)), e.source, e.operation, e.ok ? "yes" : "no", e.latencyMs, e.error ?? ""]), ); } return ( <>

Diagnostic Log

Every call this app makes to a DNS provider or integration — the last 500 calls are kept. Use this to troubleshoot connectivity issues.
{error &&
{error}
}

{total} entries {errorCount > 0 && {errorCount} errors on this page}

label="Time" sortKeyName="createdAt" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> label="Source" sortKeyName="source" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> label="Operation" sortKeyName="operation" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> label="Result" sortKeyName="ok" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> label="Latency" sortKeyName="latencyMs" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> {sorted?.map((e) => ( ))} {sorted?.length === 0 && ( )}
Error
{formatDateTime(parseDbTimestamp(e.createdAt))} {SOURCE_LABELS[e.source] ?? e.source} {e.operation} {e.ok ? ( OK ) : ( Error )} 2000 ? "text-yellow" : ""}`}>{e.latencyMs}ms {e.error ?? ""}
No diagnostic entries{filterSource || filterOk ? " match these filters" : " yet"}.
{totalPages > 1 && (
Page {page + 1} of {totalPages}
)}
); }