milestone 27: diagnostics — operational failures land in one curated log, resolved history purgeable
Gates / frontend (push) Successful in 1m33s
Gates / test (push) Successful in 1m48s
Gates / test-aarch64 (push) Successful in 7m10s
Gates / package (push) Successful in 5m31s
Gates / container (push) Successful in 15s
CI / gates (push) Successful in 14m51s
Gates / frontend (push) Successful in 1m33s
Gates / test (push) Successful in 1m48s
Gates / test-aarch64 (push) Successful in 7m10s
Gates / package (push) Successful in 5m31s
Gates / container (push) Successful in 15s
CI / gates (push) Successful in 14m51s
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* 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. Those
|
||||
* are today's routes; the navigation restructure re-points them.
|
||||
*/
|
||||
|
||||
import { DIAGNOSTIC_CODES, type DiagnosticCode } from "@/lib/types";
|
||||
|
||||
/** The literal paths keep `link.to` assignable to a typed router `Link`. */
|
||||
export type CopyLinkPath = "/settings" | "/blocklists" | "/upstreams" | "/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: "/settings", label: "Settings" } as const;
|
||||
const BLOCKLISTS = { to: "/blocklists", label: "Blocklists" } as const;
|
||||
const UPSTREAMS = { to: "/upstreams", 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<DiagnosticCode, EventCopy> = {
|
||||
"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 unreadable and was moved aside; 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,
|
||||
},
|
||||
"upstream_history.write": {
|
||||
title: "Upstream history write failed",
|
||||
impact: "Resolution is unaffected; the per-upstream success and failure aggregates lose the affected window.",
|
||||
remediation: "Check free disk space and the configuration database's permissions.",
|
||||
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,
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user