From 3ce8fdcb0212db75229c047851021579d2134c2a Mon Sep 17 00:00:00 2001 From: pptx704 Date: Wed, 25 Mar 2026 05:18:04 +0600 Subject: [PATCH] Add audit logs frontend page Infinite-scroll table with hierarchical filter dropdown, expandable metadata rows, and status-coded visual signals per event severity. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/api/audit.ts | 38 ++ .../src/routes/dashboard/audit/+page.svelte | 644 ++++++++++++++++++ 2 files changed, 682 insertions(+) create mode 100644 frontend/src/lib/api/audit.ts create mode 100644 frontend/src/routes/dashboard/audit/+page.svelte diff --git a/frontend/src/lib/api/audit.ts b/frontend/src/lib/api/audit.ts new file mode 100644 index 0000000..7d4dfe1 --- /dev/null +++ b/frontend/src/lib/api/audit.ts @@ -0,0 +1,38 @@ +import { apiFetch, type ApiResult } from '$lib/api/client'; + +export type AuditLog = { + id: string; + actor_type: 'user' | 'api_key' | 'system'; + actor_id?: string; + actor_name?: string; + resource_type: string; + resource_id?: string; + action: string; + scope: 'team' | 'admin'; + status: 'success' | 'info' | 'warning' | 'error'; + metadata?: Record; + created_at: string; +}; + +export type AuditListResponse = { + items: AuditLog[]; + next_before?: string; + next_before_id?: string; +}; + +export async function listAuditLogs(params?: { + before?: string; + before_id?: string; + resource_types?: string[]; + actions?: string[]; + limit?: number; +}): Promise> { + const q = new URLSearchParams(); + if (params?.before) q.set('before', params.before); + if (params?.before_id) q.set('before_id', params.before_id); + params?.resource_types?.forEach((t) => q.append('resource_type', t)); + params?.actions?.forEach((a) => q.append('action', a)); + if (params?.limit != null) q.set('limit', String(params.limit)); + const qs = q.toString(); + return apiFetch('GET', `/api/v1/audit-logs${qs ? '?' + qs : ''}`); +} diff --git a/frontend/src/routes/dashboard/audit/+page.svelte b/frontend/src/routes/dashboard/audit/+page.svelte new file mode 100644 index 0000000..4154287 --- /dev/null +++ b/frontend/src/routes/dashboard/audit/+page.svelte @@ -0,0 +1,644 @@ + + + + Wrenn — Audit Logs + + +
+ + +
+
+ + +
+
+

+ Audit Logs +

+

+ A complete record of activity across your team. +

+
+
+
+ + +
+
+ + +
+ + + {#if filterDropdownOpen} +
+ {#each RESOURCES as r} + {@const rState = getResourceCheckState(r)} + {@const actions = ACTIONS_BY_RESOURCE[r]} + + + + + + {#each actions as a} + {@const checked = selectedActions.get(r)?.has(a) ?? false} + + {/each} + + + {#if r !== RESOURCES[RESOURCES.length - 1]} +
+ {/if} + {/each} +
+ {/if} +
+ +
+ + + {#if activeFilterCount > 0} +
+ {#each RESOURCES as r} + {#if (selectedActions.get(r)?.size ?? 0) > 0} + + {tagLabel(r)} + + + {/if} + {/each} + +
+ {/if} +
+ + +
+ + {#if error} +
+ {error} + +
+ {/if} + + {#if loading} +
+
+ + + + Loading events... +
+
+ {:else if logs.length === 0} + +
+
+
+
+ + + +
+
+

+ {activeFilterCount > 0 ? 'No matching events' : 'No activity yet'} +

+

+ {activeFilterCount > 0 + ? 'Try adjusting or clearing the filters.' + : 'Events will appear here as your team takes actions.'} +

+ {#if activeFilterCount > 0} + + {/if} +
+ {:else} + +
+ + +
+
Time
+
Actor
+
Event
+
+
+ + + {#each logs as log, i (log.id)} + {@const ts = formatEventDate(log.created_at)} +
+ +
+ + + + + + {#if expandedId === log.id && hasMetadata(log)} +
+
+ {#each Object.entries(log.metadata ?? {}) as [key, value]} + + {META_LABELS[key] ?? key.replace(/_/g, ' ')} + + + {String(value)} + + {/each} +
+
+ {/if} +
+ {/each} +
+ + +
+ {#if loadingMore} +
+ + + + Loading more... +
+ {:else if !hasMore} +

+ {logs.length} {logs.length === 1 ? 'event' : 'events'} total +

+ {/if} +
+ {/if} + +
+
+ +
+
+
+ +