milestone 28: query provenance — every logged query is exactly explainable
Gates / frontend (push) Successful in 1m36s
Gates / test (push) Successful in 1m56s
Gates / test-aarch64 (push) Successful in 7m37s
Gates / package (push) Successful in 9m12s
Gates / container (push) Successful in 13s
CI / gates (push) Successful in 19m4s

query rows gain qclass, rcode, group, policy action and reason, the
matched rule or list entry with its source, cname and safe-search
targets, route kind, forward zone, and the resolver that actually
answered — the pool and local markers die. servfails are logged and
name the resolver that lost; post-parse protocol refusals become rows.
a detail page at /queries/:id renders the ordered explanation, and
coverage watermarks distinguish an empty history from a missing one.

the schema fingerprint changes: existing query history is recreated
with the old file kept aside and the reset filed as a resolved
diagnostic. fixes an oversized udp reply being rebuilt as noerror,
which handed clients a truncated nxdomain as success.
This commit is contained in:
2026-08-22 09:16:40 +02:00
parent 7e6cb507d2
commit 0fd6bbd312
65 changed files with 7036 additions and 685 deletions
+39 -2
View File
@@ -12,7 +12,7 @@ import {
import AppShell from "@/shell/AppShell";
import { ApiError } from "@/lib/api";
import { createQueryClient } from "@/lib/queryClient";
import type { DiagnosticSeverity, DiagnosticState, DiagnosticsFilter } from "@/lib/types";
import type { DiagnosticSeverity, DiagnosticState, DiagnosticsFilter, QueriesFilter } from "@/lib/types";
import {
blocklistsQuery,
clientPrefixesQuery,
@@ -24,6 +24,7 @@ import {
healthQuery,
localRecordsQuery,
queriesInfiniteQuery,
queryDetailQuery,
rulesQuery,
settingsQuery,
statsQuery,
@@ -138,13 +139,43 @@ const dashboardRoute = createRoute({
component: lazyRouteComponent(() => import("@/features/dashboard/DashboardPage")),
});
/**
* `domain` and `client` seed the filter form, so a detail page can link to
* "every query for this domain". Anything else in the search object is dropped:
* an unknown value would reach the api as a parameter the handler 400s.
*/
const queriesRoute = createRoute({
getParentRoute: () => shellRoute,
path: "/queries",
loader: ({ context }) => context.queryClient.ensureInfiniteQueryData(queriesInfiniteQuery({})),
validateSearch: (search: Record<string, unknown>): { domain?: string; client?: string } => {
const domain = search["domain"];
const client = search["client"];
return {
domain: typeof domain === "string" && domain !== "" ? domain : undefined,
client: typeof client === "string" && client !== "" ? client : undefined,
};
},
loaderDeps: ({ search }) => search,
loader: ({ context, deps }) => {
const filter: QueriesFilter = {};
if (deps.domain !== undefined) filter.domain = deps.domain;
if (deps.client !== undefined) filter.client = deps.client;
return context.queryClient.ensureInfiniteQueryData(queriesInfiniteQuery(filter));
},
component: lazyRouteComponent(() => import("@/features/queries/QueryLogPage")),
});
const queryDetailRoute = createRoute({
getParentRoute: () => shellRoute,
path: "/queries/$id",
// Swallowed on purpose, as the diagnostics detail route does: a row
// retention has pruned is a 404 the page explains, with the way back to the
// log. The whole-page error component would call it a request failure.
loader: ({ context, params }) =>
context.queryClient.ensureQueryData(queryDetailQuery(Number(params.id))).catch(() => undefined),
component: lazyRouteComponent(() => import("@/features/queries/QueryDetailPage")),
});
const liveRoute = createRoute({
getParentRoute: () => shellRoute,
path: "/live",
@@ -210,9 +241,14 @@ const upstreamsRoute = createRoute({
component: lazyRouteComponent(() => import("@/features/upstreams/UpstreamsPage")),
});
/** `domain` prefills and runs the lookup, so a query detail page can link into it. */
const lookupRoute = createRoute({
getParentRoute: () => shellRoute,
path: "/lookup",
validateSearch: (search: Record<string, unknown>): { domain?: string } => {
const domain = search["domain"];
return { domain: typeof domain === "string" && domain !== "" ? domain : undefined };
},
loader: ({ context }) => context.queryClient.ensureQueryData(groupsQuery()),
component: lazyRouteComponent(() => import("@/features/lookup/LookupPage")),
});
@@ -275,6 +311,7 @@ const routeTree = rootRoute.addChildren([
shellRoute.addChildren([
dashboardRoute,
queriesRoute,
queryDetailRoute,
liveRoute,
clientsRoute,
groupsRoute,