Gates / frontend (push) Successful in 1m32s
Gates / test (push) Successful in 1m54s
Gates / package (push) Successful in 5m28s
Gates / container (push) Successful in 14s
Gates / test-aarch64 (push) Failing after 3h10m0s
CI / gates (push) Failing after 3h11m55s
162 lines
7.1 KiB
TypeScript
162 lines
7.1 KiB
TypeScript
/**
|
|
* 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 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,
|
|
},
|
|
};
|