Files
siftlode/frontend/src/lib/logsUrlState.ts
T
peter 24448e7c6f feat(activity): one Activity page — runs, admin actions, server log and quota under one filter bar
The audit log, the server log and the admin quota view were three pages (the quota one a Stats tab),
each with its own filters, and the background runs had no list at all. Now one page ("Napló" /
"Activity") reads all four, each still its own table on the server: a shared bar (text, a period of
1 h to 90 days defaulting to 7, an account) and each tab's own filters, the whole view in the URL.
The old page ids open their tab.

Server side: the audit log and the new run list (`/api/admin/runs`) filter and page like the server
log, through one keyset helper (`routes/_keyset`) the log now shares; an audit row keeps the request
it was made in (migration 0111), so its log lines are a pivot away, as a run's are (`lrun`). The
quota breakdown takes since/until and one account. The Scheduler rows get a history button to their
job's runs.

Also fixed on the way: re-saving the value in force on the Configuration page staged its override
without committing it (the commit sat inside the audit branch), so the save was lost.
2026-10-18 03:45:18 +02:00

68 lines
2.6 KiB
TypeScript

// 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<string, string | null> {
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),
};
}