// The server-log tab's own filters in the address bar (`?page=activity&at=logs&lgrp=error&lreq=…`), // so a filtered view — a request's lines, say — is a link. The filters every activity tab shares // (time range, account, text) live in ./activityUrlState. Keys are namespaced (`l…`) like the Plex // and Channels ones; setPage clears them (via ACTIVITY_KEYS) and the tab re-mirrors them while open. import type { LevelGroup } from "./log"; export interface LogsUrlState { group: LevelGroup; sources: string[]; /** An event code, or `none` for the rows without one. */ event: string | null; requestId: string | null; jobId: number | null; /** A background run's lines (`job_runs.id`). */ runId: number | null; } export const DEFAULT_LOGS_STATE: LogsUrlState = { group: "all", sources: [], event: null, requestId: null, jobId: null, runId: null, }; export const LOGS_KEYS = ["lgrp", "lsrc", "lev", "lreq", "ljob", "lrun"] as const; 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 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 pick = (value: string | null, re: RegExp, max = 64) => value && value.length <= max && re.test(value) ? value : null; const positive = (value: string | null) => value && /^\d{1,10}$/.test(value) && Number(value) > 0 ? Number(value) : null; export function logsParamsToState(params: URLSearchParams): LogsUrlState { const group = params.get("lgrp"); 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), event: pick(params.get("lev"), EVENT_RE), requestId: pick(params.get("lreq"), REQUEST_RE), jobId: positive(params.get("ljob")), runId: positive(params.get("lrun")), }; } /** 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, lev: state.event, lreq: state.requestId, ljob: state.jobId === null ? null : String(state.jobId), lrun: state.runId === null ? null : String(state.runId), }; }