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.
This commit is contained in:
@@ -95,18 +95,69 @@ export interface AdminUserAccess extends AdminUserRow {
|
||||
export interface AuditRow {
|
||||
id: number;
|
||||
created_at: string | null;
|
||||
actor: string | null; // email of the acting admin; null = System (scheduler/background)
|
||||
/** The acting admin (address in the page's `accounts`); null = the background. */
|
||||
actor_id: number | null;
|
||||
action: string; // canonical key, localized on the client via auditActions.<action>
|
||||
target_type: string | null;
|
||||
target_id: string | null;
|
||||
summary: string | null;
|
||||
before: Record<string, unknown> | null;
|
||||
after: Record<string, unknown> | null;
|
||||
/** The request the change was made in — its log lines are one pivot away. Null on older rows. */
|
||||
request_id: string | null;
|
||||
}
|
||||
export interface AuditListResult {
|
||||
items: AuditRow[];
|
||||
total: number; // total rows in the log (may exceed `items` if capped)
|
||||
returned: number;
|
||||
|
||||
/** A page of a history list (audit log, runs), newest first; `next_cursor` continues it. */
|
||||
export interface HistoryPage<T> {
|
||||
items: T[];
|
||||
/** The accounts this page names, by id; an erased one is absent. */
|
||||
accounts: Record<string, string>;
|
||||
next_cursor: string | null;
|
||||
}
|
||||
|
||||
/** The filters every activity tab shares, as the APIs take them. */
|
||||
export interface SharedQuery {
|
||||
/** An account id, or `none` for what no account did. */
|
||||
user?: string;
|
||||
q?: string;
|
||||
since?: string;
|
||||
}
|
||||
|
||||
export interface AuditQuery extends SharedQuery {
|
||||
action?: string;
|
||||
}
|
||||
|
||||
/** One background run (`job_runs`), as the activity page lists it. */
|
||||
export interface RunRow {
|
||||
id: number;
|
||||
job: string;
|
||||
trigger: RunBrief["trigger"];
|
||||
actor_id: number | null;
|
||||
/** What a user's ad-hoc run worked on (a channel id), and that channel's title. */
|
||||
target: string | null;
|
||||
target_title: string | null;
|
||||
status: RunBrief["status"];
|
||||
started_at: string;
|
||||
finished_at: string | null;
|
||||
summary: string | null;
|
||||
result: JobResult | null;
|
||||
error: string | null;
|
||||
failure: { code: string; [key: string]: unknown } | null;
|
||||
quota_units: number;
|
||||
/** How many stored log lines the run left (WARNING and up only). */
|
||||
log_lines: number;
|
||||
}
|
||||
|
||||
export interface RunQuery extends SharedQuery {
|
||||
job?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
/** The job menu and the status chips, each counted under every filter but its own. */
|
||||
export interface RunFacets {
|
||||
jobs: LogFacet[];
|
||||
statuses: LogFacet[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
/** One stored log line (`log_events`), as the diagnostics console reads it. Values the server's
|
||||
@@ -124,6 +175,7 @@ export interface ServerLogRow {
|
||||
request_id: string | null;
|
||||
user_id: number | null;
|
||||
job_id: number | null;
|
||||
run_id: number | null;
|
||||
debug_session_id: string | null;
|
||||
version: string | null;
|
||||
}
|
||||
@@ -136,6 +188,7 @@ export interface LogQuery {
|
||||
event?: string;
|
||||
request_id?: string;
|
||||
job_id?: number;
|
||||
run_id?: number;
|
||||
q?: string;
|
||||
since?: string;
|
||||
}
|
||||
@@ -171,6 +224,7 @@ function logParams(query: LogQuery, extra: Record<string, string | number | null
|
||||
event: query.event,
|
||||
request_id: query.request_id,
|
||||
job_id: query.job_id,
|
||||
run_id: query.run_id,
|
||||
q: query.q,
|
||||
since: query.since,
|
||||
...extra,
|
||||
@@ -182,6 +236,16 @@ function logParams(query: LogQuery, extra: Record<string, string | number | null
|
||||
return s ? `?${s}` : "";
|
||||
}
|
||||
|
||||
/** `?a=1&b=x` from the set values (an empty string or null is left out), or "" when none is. */
|
||||
function queryString(values: object): string {
|
||||
const p = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(values) as [string, unknown][]) {
|
||||
if (value !== undefined && value !== null && value !== "") p.set(key, String(value));
|
||||
}
|
||||
const s = p.toString();
|
||||
return s ? `?${s}` : "";
|
||||
}
|
||||
|
||||
export type JobResult = { [key: string]: number | string | JobResult };
|
||||
|
||||
/** How a run ended — the server's `RunStatus` (app/runstatus.py) minus `running`. `interrupted`:
|
||||
@@ -277,7 +341,14 @@ export const adminApi = {
|
||||
testIssues: (): Promise<{ ok: true; repo: string; url: string | null }> =>
|
||||
req("/api/admin/config/test-issues", { method: "POST" }),
|
||||
// --- users & roles ---
|
||||
adminAudit: (limit = 500): Promise<AuditListResult> => req(`/api/admin/audit?limit=${limit}`),
|
||||
adminAudit: (query: AuditQuery, before: string | null): Promise<HistoryPage<AuditRow>> =>
|
||||
req(`/api/admin/audit${queryString({ ...query, before })}`),
|
||||
adminAuditFacets: (query: AuditQuery): Promise<{ actions: LogFacet[] }> =>
|
||||
req(`/api/admin/audit/facets${queryString(query)}`),
|
||||
adminRuns: (query: RunQuery, before: string | null): Promise<HistoryPage<RunRow>> =>
|
||||
req(`/api/admin/runs${queryString({ ...query, before })}`),
|
||||
adminRunFacets: (query: RunQuery): Promise<RunFacets> =>
|
||||
req(`/api/admin/runs/facets${queryString(query)}`),
|
||||
clearAudit: (): Promise<{ cleared: number }> => req("/api/admin/audit", { method: "DELETE" }),
|
||||
// --- diagnostics console ---
|
||||
adminLogs: (query: LogQuery, before: string | null): Promise<LogPage> =>
|
||||
|
||||
Reference in New Issue
Block a user