feat(admin): audit-log viewer

Surface the admin_audit table (written by every mutating admin action but
previously unreadable) as a new /admin/audit tab. New repo::admin_audit::list
(LEFT JOIN users for the actor's username, filter by action/target_kind/actor/
since, at DESC via admin_audit_at_idx) behind GET /v1/admin/audit, mirroring the
crawler history endpoint's paged/clamped idiom.

Frontend: a self-contained AuditTable (target-kind + window + action filters,
expandable JSON payload, AbortController-cancelled fetches, loading/error/empty
states) and shared fmtAgo() in format.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-19 11:26:09 +02:00
parent dd300a150c
commit 31013cc893
11 changed files with 748 additions and 5 deletions

View File

@@ -978,3 +978,43 @@ export async function updateAnalysisSettings(
body: JSON.stringify(settings)
});
}
// ---- admin audit log -------------------------------------------------------
export type AuditEntry = {
id: string;
actor_user_id: string | null;
/** Actor's current username; null when the account was deleted. */
actor_username: string | null;
action: string;
target_kind: string;
target_id: string | null;
payload: unknown;
at: string;
};
export type AuditPage = { items: AuditEntry[]; page: Page };
export type ListAuditOptions = {
action?: string;
targetKind?: string;
actorUserId?: string;
days?: number;
limit?: number;
offset?: number;
};
export async function listAuditLog(
opts: ListAuditOptions = {},
init?: RequestInit
): Promise<AuditPage> {
const params = new URLSearchParams();
if (opts.action) params.set('action', opts.action);
if (opts.targetKind) params.set('target_kind', opts.targetKind);
if (opts.actorUserId) params.set('actor_user_id', opts.actorUserId);
if (opts.days != null && opts.days > 0) params.set('days', String(opts.days));
if (opts.limit != null) params.set('limit', String(opts.limit));
if (opts.offset != null) params.set('offset', String(opts.offset));
const qs = params.toString();
return request<AuditPage>(`/v1/admin/audit${qs ? `?${qs}` : ''}`, init);
}