Gates / test (push) Successful in 1m40s
Gates / package (push) Successful in 3m58s
Gates / container (push) Successful in 14s
CI / gates (push) Successful in 12m51s
Gates / frontend (push) Successful in 1m18s
Gates / test-aarch64 (push) Successful in 6m57s
query log, live and lookup merge into /activity. history filters live in the url, so a pasted link or back/forward reproduces the exact view; the result column separates servfail and nxdomain from success in the list. live is follow-by-default with freeze, and a streamed row opens its in-memory provenance detail — no correlation invented for rows sqlite has not written. lookup survives as the current policy simulation under /activity/test. investigation links carry absolute bounds, and the diagnostics page now honors since/until instead of ignoring them. the old routes are gone without aliases.
89 lines
3.4 KiB
TypeScript
89 lines
3.4 KiB
TypeScript
/**
|
|
* The Activity search parameters, validated as pure functions so the route's
|
|
* `validateSearch` stays a one-liner and every rejection is testable without a
|
|
* router.
|
|
*
|
|
* A search value arrives from a URL, from history state, or from a hand-typed
|
|
* link, so nothing about its type is given. Anything that is not exactly the
|
|
* value the API can filter on becomes `undefined`: an unbounded page is honest,
|
|
* a page filtered on a coerced guess is not.
|
|
*/
|
|
|
|
import type { QueriesFilter } from "@/lib/types";
|
|
|
|
export const ACTIVITY_MODES = ["history", "live"] as const;
|
|
export type ActivityMode = (typeof ACTIVITY_MODES)[number];
|
|
|
|
export interface ActivitySearch {
|
|
mode: ActivityMode;
|
|
since: number | undefined;
|
|
until: number | undefined;
|
|
domain: string | undefined;
|
|
client: string | undefined;
|
|
blocked: boolean | undefined;
|
|
}
|
|
|
|
/** History is the surface a bare `/activity` should open on: it answers questions. */
|
|
export function validateMode(value: unknown): ActivityMode {
|
|
return value === "live" ? "live" : "history";
|
|
}
|
|
|
|
/**
|
|
* A unix-second bound. `Number.isSafeInteger` is the whole test: it rejects a
|
|
* fraction, an infinity, a NaN and a magnitude past 2^53 in one step, and a
|
|
* string never passes, so `?since=now` cannot reach the API as garbage.
|
|
*/
|
|
export function validateTimestamp(value: unknown): number | undefined {
|
|
return Number.isSafeInteger(value) ? (value as number) : undefined;
|
|
}
|
|
|
|
/**
|
|
* The blocked filter. `false` is a real filter — "allowed only" — so it must
|
|
* survive; only a genuine boolean does, because `"false"` out of a URL parser
|
|
* that did not decode JSON would otherwise read as true.
|
|
*/
|
|
export function validateBlocked(value: unknown): boolean | undefined {
|
|
return typeof value === "boolean" ? value : undefined;
|
|
}
|
|
|
|
/**
|
|
* A text filter, trimmed. An empty result becomes `undefined` rather than `""`:
|
|
* the server treats an empty filter as no filter, and a URL that showed
|
|
* `domain=` as applied state would claim a filter that is not filtering.
|
|
*/
|
|
export function validateText(value: unknown): string | undefined {
|
|
if (typeof value !== "string") return undefined;
|
|
const trimmed = value.trim();
|
|
return trimmed === "" ? undefined : trimmed;
|
|
}
|
|
|
|
/**
|
|
* The API filter for a validated search, built field by field.
|
|
*
|
|
* Only the fields that are actually set are written, so an unfiltered request
|
|
* carries no keys at all: `GET /api/queries` rejects a parameter it does not
|
|
* know, and the infinite query's cache key is the filter object, so a key
|
|
* present-but-undefined and a key absent must not be two different windows onto
|
|
* the same rows. `mode` never appears — it selects the surface, not the rows.
|
|
*/
|
|
export function queriesFilterOf(search: Omit<ActivitySearch, "mode">): QueriesFilter {
|
|
const filter: QueriesFilter = {};
|
|
if (search.domain !== undefined) filter.domain = search.domain;
|
|
if (search.client !== undefined) filter.client = search.client;
|
|
if (search.blocked !== undefined) filter.blocked = search.blocked;
|
|
if (search.since !== undefined) filter.since = search.since;
|
|
if (search.until !== undefined) filter.until = search.until;
|
|
return filter;
|
|
}
|
|
|
|
export function validateActivitySearch(search: Record<string, unknown>): ActivitySearch {
|
|
return {
|
|
mode: validateMode(search["mode"]),
|
|
since: validateTimestamp(search["since"]),
|
|
until: validateTimestamp(search["until"]),
|
|
domain: validateText(search["domain"]),
|
|
client: validateText(search["client"]),
|
|
blocked: validateBlocked(search["blocked"]),
|
|
};
|
|
}
|