feat(audit): server-side paging, search and category filter; one event format
The page rendered a map of eleven event types to labels and seven to colours.
The server emits forty-seven. Everything unmapped fell through to its raw
string, so "Key Assigned" in green sat above "workflow.schedule_updated" in
grey — the same kind of fact in two formats, which made the column look like it
carried a meaning it did not.
Presentation is now derived rather than enumerated. Event types are named
<category>.<action> by every call site, so the category becomes a chip, the
action is humanised, and the tone comes from the verb. A type added to the
server tomorrow gets a sensible label and colour with no second list to update;
the override table holds only the dozen the rule reads badly for. Every row is
one treatment, and colour never carries meaning alone — the sentence beside it
says the same thing in words.
Paging and filtering are server-side, unlike the fleet lists that answer with
everything and slice in the browser. audit_retention_days is a licensed
entitlement measured in months, and this log is read to answer questions about
the past, so a browser filtering the most recent page would report "no results"
for events that exist. GET /api/audit now takes q, category, limit and skip and
answers {events, total} — a short page is not evidence of the end of the log,
which is why the total is counted rather than inferred.
audit_logs had no indexes at all: every read was a collection scan with an
in-memory sort over an append-only collection. Adds (instance_id, created_at)
and warns rather than failing, matching EnsureSecretIndexes.
Two bugs found by running the deriver over all forty-seven real types rather
than eyeballing it: the tone rules matched only past-tense verbs, leaving
auth_provider.delete drawn as neutral beside key.deleted in red; and
"unaccepted" matched "accepted", so withdrawing an acceptance read as the same
caution as granting one.
This commit is contained in:
+26
-3
@@ -164,6 +164,24 @@ export interface AuditEvent {
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface AuditQuery {
|
||||
q?: string;
|
||||
category?: string;
|
||||
limit?: number;
|
||||
skip?: number;
|
||||
}
|
||||
|
||||
/*
|
||||
* The audit log pages on the server, unlike the fleet endpoints that answer
|
||||
* with everything and slice in the browser. It is kept for months and read to
|
||||
* answer questions about the past, so a search that only saw the most recent
|
||||
* page would report "no results" for events that exist.
|
||||
*/
|
||||
export interface AuditPage {
|
||||
events: AuditEvent[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface AlertSettings {
|
||||
offline_threshold_minutes: number;
|
||||
offline_channel_ids: string[] | null;
|
||||
@@ -651,9 +669,14 @@ export const api = {
|
||||
});
|
||||
},
|
||||
|
||||
listAuditEvents(limit?: number): Promise<AuditEvent[]> {
|
||||
const qs = limit ? `?limit=${limit}` : "";
|
||||
return request<AuditEvent[]>(`/audit${qs}`);
|
||||
listAuditEvents(params: AuditQuery = {}): Promise<AuditPage> {
|
||||
const qs = new URLSearchParams();
|
||||
if (params.q) qs.set("q", params.q);
|
||||
if (params.category) qs.set("category", params.category);
|
||||
if (params.limit) qs.set("limit", String(params.limit));
|
||||
if (params.skip) qs.set("skip", String(params.skip));
|
||||
const suffix = qs.toString();
|
||||
return request<AuditPage>(`/audit${suffix ? `?${suffix}` : ""}`);
|
||||
},
|
||||
|
||||
getSettings(): Promise<Settings> {
|
||||
|
||||
Reference in New Issue
Block a user