// The admin diagnostics console's filters in the address bar (`?page=logs&lgrp=error&lreq=…`), so a // filtered view — a request's lines, say — is a link. Keys are namespaced (`l…`) like the Plex and // Channels ones; setPage clears them (LOGS_KEYS) and the console re-mirrors them while it is open. import type { LevelGroup } from "./log"; export type LogWindow = "1h" | "24h" | "7d"; export interface LogsUrlState { group: LevelGroup; sources: string[]; /** An account id, or `none` for the rows without one. */ user: string | null; /** An event code, or `none` for the rows without one. */ event: string | null; requestId: string | null; jobId: number | null; q: string; window: LogWindow; } export const DEFAULT_LOGS_STATE: LogsUrlState = { group: "all", sources: [], user: null, event: null, requestId: null, jobId: null, q: "", window: "24h", }; export const LOGS_KEYS = ["lgrp", "lsrc", "lu", "lev", "lreq", "ljob", "lq", "lwin"] as const; export const WINDOW_HOURS: Record = { "1h": 1, "24h": 24, "7d": 24 * 7 }; const GROUPS: readonly LevelGroup[] = ["all", "error", "warning", "info", "detail"]; // The same shapes the API validates: a link someone edited by hand degrades to "no filter" here // instead of to a 422 on every request the console makes. const USER_RE = /^(none|\d{1,10})$/; const EVENT_RE = /^(none|[a-z][a-z0-9_]*(\.[a-z0-9_]+)*)$/; const REQUEST_RE = /^[A-Za-z0-9._-]{8,64}$/; const SOURCE_RE = /^[a-z]{1,16}$/; const SOURCES_MAX = 8; const Q_MAX = 200; const pick = (value: string | null, re: RegExp, max = 64) => value && value.length <= max && re.test(value) ? value : null; export function logsParamsToState(params: URLSearchParams): LogsUrlState { const group = params.get("lgrp"); const window = params.get("lwin"); const job = params.get("ljob"); return { group: GROUPS.includes(group as LevelGroup) ? (group as LevelGroup) : DEFAULT_LOGS_STATE.group, sources: (params.get("lsrc") ?? "") .split(",") .filter((s) => SOURCE_RE.test(s)) .slice(0, SOURCES_MAX), user: pick(params.get("lu"), USER_RE), event: pick(params.get("lev"), EVENT_RE), requestId: pick(params.get("lreq"), REQUEST_RE), jobId: job && /^\d{1,10}$/.test(job) && Number(job) > 0 ? Number(job) : null, q: (params.get("lq") ?? "").slice(0, Q_MAX), window: window && window in WINDOW_HOURS ? (window as LogWindow) : DEFAULT_LOGS_STATE.window, }; } /** The URL updates for a state: a filter at its default clears its key. */ export function logsParamUpdates(state: LogsUrlState): Record { return { lgrp: state.group === DEFAULT_LOGS_STATE.group ? null : state.group, lsrc: state.sources.length ? state.sources.join(",") : null, lu: state.user, lev: state.event, lreq: state.requestId, ljob: state.jobId === null ? null : String(state.jobId), lq: state.q.trim() ? state.q : null, lwin: state.window === DEFAULT_LOGS_STATE.window ? null : state.window, }; }