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:
@@ -42,7 +42,8 @@ import {
|
||||
listAnalysisHistory,
|
||||
getCrawlerMetrics,
|
||||
listCrawlerOps,
|
||||
getAnalysisMetrics
|
||||
getAnalysisMetrics,
|
||||
listAuditLog
|
||||
} from './admin';
|
||||
|
||||
function ok(body: unknown, status = 200): Response {
|
||||
@@ -471,6 +472,40 @@ describe('admin crawler api client', () => {
|
||||
expect(fetchSpy.mock.calls[0][0]).toMatch(/\/v1\/admin\/crawler\/covers$/);
|
||||
});
|
||||
|
||||
it('listAuditLog GETs /v1/admin/audit with the paged envelope', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
ok({
|
||||
items: [
|
||||
{
|
||||
id: 'a-1',
|
||||
actor_user_id: 'u-1',
|
||||
actor_username: 'alice',
|
||||
action: 'crawler_run',
|
||||
target_kind: 'crawler',
|
||||
target_id: null,
|
||||
payload: {},
|
||||
at: '2026-06-01T00:00:00Z'
|
||||
}
|
||||
],
|
||||
page: { limit: 25, offset: 0, total: 1 }
|
||||
})
|
||||
);
|
||||
const r = await listAuditLog({ limit: 25 });
|
||||
expect(r.items[0].action).toBe('crawler_run');
|
||||
expect(fetchSpy.mock.calls[0][0]).toMatch(/\/v1\/admin\/audit\?limit=25$/);
|
||||
});
|
||||
|
||||
it('listAuditLog forwards action / target_kind / days filters', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
ok({ items: [], page: { limit: 25, offset: 0, total: 0 } })
|
||||
);
|
||||
await listAuditLog({ action: 'manga_resync', targetKind: 'manga', days: 7 });
|
||||
const url = fetchSpy.mock.calls[0][0] as string;
|
||||
expect(url).toContain('action=manga_resync');
|
||||
expect(url).toContain('target_kind=manga');
|
||||
expect(url).toContain('days=7');
|
||||
});
|
||||
|
||||
it('runCrawlerPass POSTs /v1/admin/crawler/run', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(ok({ started: true }));
|
||||
const r = await runCrawlerPass();
|
||||
|
||||
Reference in New Issue
Block a user