/** * What each event code means to the operator, in three fixed fields: what the * episode is (`title`), what it costs while it stays open (`impact`), and what * to do about it (`remediation`). The server sends a code and an error string; * every word of explanation the page shows comes from here. * * The record is exhaustive over `DiagnosticCode` by type, and a test walks * `DIAGNOSTIC_CODES` to prove it at runtime too. A sixteenth code added to the * enum fails `tsc` here before it can reach the page as a bare dotted string. * * `link` points at the configuration surface that governs the failure. The * path alone decides which tab of that surface the detail page opens, so the * copy never carries a tab of its own to drift from it. */ import { DIAGNOSTIC_CODES, type DiagnosticCode } from "@/lib/types"; /** The literal paths keep `link.to` assignable to a typed router `Link`. */ export type CopyLinkPath = "/configuration/system" | "/configuration/protection" | "/configuration/resolution" | "/clients"; export interface EventCopy { title: string; impact: string; remediation: string; link?: { to: CopyLinkPath; label: string }; } /** * The copy for a code, with a floor under it. `tsc` proves the record covers * the union, but a server one release ahead can send a code this build has * never heard of; showing the raw code beats rendering "undefined". */ export function copyFor(code: DiagnosticCode): EventCopy { return ( EVENT_COPY[code] ?? { title: code, impact: "This build has no description for this event code.", remediation: "The error detail below is the whole of what the server reported.", } ); } const SETTINGS = { to: "/configuration/system", label: "System" } as const; const BLOCKLISTS = { to: "/configuration/protection", label: "Blocklist sources" } as const; const UPSTREAMS = { to: "/configuration/resolution", label: "Upstreams" } as const; const CLIENTS = { to: "/clients", label: "Clients" } as const; /** * The component filter's options, derived from the codes rather than listed * again: the server matches `component` against the part of `code` before the * dot, so any list written by hand here could drift from the enum. */ export const DIAGNOSTIC_COMPONENTS: readonly string[] = [ ...new Set(DIAGNOSTIC_CODES.map((code) => code.slice(0, code.indexOf(".")))), ]; /** `query_log` → "Query log". Display only; the filter sends the raw component. */ export function componentLabel(component: string): string { const spaced = component.replaceAll("_", " "); return spaced.charAt(0).toUpperCase() + spaced.slice(1); } export const EVENT_COPY: Record = { "disk.space": { title: "Disk space low", impact: "Below the critical threshold nxdns stops blocklist updates and query log flushes to protect the disk.", remediation: "Free space on the data volume, or lower the retention window so the query log holds fewer days.", link: SETTINGS, }, "disk.probe": { title: "Disk usage probe failed", impact: "Free space is unknown, so the low-disk guard cannot act until a probe succeeds.", remediation: "Check that the data and log directories exist and that the service user can read them.", link: SETTINGS, }, "blocklist.refresh": { title: "Blocklist source failed to update", impact: "The source keeps serving its last good snapshot, so blocking continues but the list ages.", remediation: "Check the source url and the machine's internet access, then update the lists again.", link: BLOCKLISTS, }, "blocklist.snapshot": { title: "Filter snapshot failed to publish", impact: "The resolver keeps the snapshot it already holds; blocklist edits do not take effect until one publishes.", remediation: "Check free disk space and the data directory's permissions, then update the lists again.", link: BLOCKLISTS, }, "blocklist.storage": { title: "Blocklist storage operation failed", impact: "Cached list files or their database rows are out of step; a later pass can redownload what is missing.", remediation: "Check free disk space and the data directory's permissions.", link: BLOCKLISTS, }, "certificate.reload": { title: "TLS certificate reload failed", impact: "The endpoint keeps serving the certificate it already loaded, which expires on its own schedule.", remediation: "Check the certificate and key paths, and that renewal writes both files the service user can read.", link: SETTINGS, }, "query_log.write": { title: "Query log write failed", impact: "Queries are resolved and answered as usual, but they are not being recorded.", remediation: "Check free disk space and the log database's permissions, then restart nxdns.", link: SETTINGS, }, "query_log.maintenance": { title: "Query log maintenance failed", impact: "Old rows are not being trimmed, so the log database grows past its retention window.", remediation: "Check free disk space; the next maintenance pass retries on its own.", link: SETTINGS, }, "query_log.recreated": { title: "Query log recreated", impact: "The old log database was moved aside — a release changed its schema, or the file could not be read — and the history it held is not in the new one.", remediation: "Keep or delete the aside file named below. Nothing else is required — logging is running.", link: SETTINGS, }, /** * Legacy. Nothing emits this code any more: milestone 30 deleted the * upstream-minute history subsystem. Stored rows outlive it, and the list * endpoint passes their codes through verbatim, so the copy stays — a * retained episode must still read as prose rather than as a dotted string. */ "upstream_history.write": { title: "Upstream history write failed", impact: "A recorded failure of a subsystem this version no longer runs. Resolution was unaffected; the per-upstream aggregates it fed are gone.", remediation: "Nothing to do. Purge the entry once you have read it.", link: UPSTREAMS, }, "upstream.exchange": { title: "Upstream failing", impact: "Queries fall through to the remaining upstreams; answers are slower while this one backs off.", remediation: "Check the upstream's reachability and its TLS name. Remove it if it stays down.", link: UPSTREAMS, }, "client_names.storage": { title: "Client name storage failed", impact: "Learned reverse-DNS names are not persisted, so clients can show as bare addresses after a restart.", remediation: "Check free disk space and the configuration database's permissions.", link: CLIENTS, }, "clients.storage": { title: "Client record storage failed", impact: "New clients may not appear in the list and stale ones may not be pruned.", remediation: "Check free disk space and the configuration database's permissions.", link: CLIENTS, }, "listener.start": { title: "Encrypted DNS listener failed to start", impact: "That endpoint is not accepting queries. Plain DNS on port 53 is unaffected.", remediation: "Check the bind address, the port, and the certificate paths, then restart nxdns. The episode closes on a clean start.", link: SETTINGS, }, "configuration.load": { title: "Configuration problem at startup", impact: "The setting named below was rejected or replaced by its default for this run.", remediation: "Correct the setting and restart nxdns. The episode closes on a clean start.", link: SETTINGS, }, };