import type { LiveQueryEvent, PolicyReason, QueryRow, RouteKind } from "@/lib/types"; /** * What the query-log table renders for one row, whichever surface it came from. * * The stored list row and the live stream's provenance event describe the same * query in two different shapes — flat summary against nested full detail — and * both pages share one set of cells, so both project into this. * * `id` is null for a streamed event: the frame precedes its own insert, so no * row exists to link to yet. */ export interface QuerySummary { id: number | null; ts: number; domain: string; client_ip: string; qtype: number | null; blocked: boolean; policy_reason: PolicyReason; /** The twelve-bit extended code the client saw, including a synthesized SERVFAIL. */ rcode: number; route_kind: RouteKind; response_time_us: number | null; cache_hit: boolean | null; upstream: string; } /** * Whether the cache answered, or null where it never applied. Mirrors * `Context.cacheHit` in src/server/handler.zig, which derives the stored * `cache_hit` column from the same route: a local record, a blocked answer and * a protocol refusal all bypass the cache, and "miss" would claim a lookup that * never happened. */ export function cacheHitFor(kind: RouteKind): boolean | null { switch (kind) { case "cache": return true; case "upstream": case "forward_zone": return false; case "local": case "blocked": case "rejected": return null; } } export function summarizeRow(row: QueryRow): QuerySummary { return { id: row.id, ts: row.ts, domain: row.domain, client_ip: row.client_ip, qtype: row.qtype, blocked: row.blocked, policy_reason: row.policy_reason, rcode: row.rcode, route_kind: row.route_kind, response_time_us: row.response_time_us, cache_hit: row.cache_hit, upstream: row.upstream, }; } /** * The same summary out of a live frame. `blocked` and `cache_hit` are derived * rather than sent: the server derives the stored columns from exactly these * two fields (handler.zig's `Entry.init` call), so the projection reproduces * them instead of the DTO carrying the same fact twice. */ export function summarizeEvent(event: LiveQueryEvent): QuerySummary { return { id: null, ts: event.request.time, domain: event.request.domain, client_ip: event.request.client, qtype: event.request.qtype, blocked: event.policy.action === "block", policy_reason: event.policy.reason, rcode: event.response.rcode, route_kind: event.route.kind, response_time_us: event.response.duration_us, cache_hit: cacheHitFor(event.route.kind), upstream: event.route.upstream, }; }