/** * Audit Log Page (S-7.6) * * Dashboard page showing audit log entries with: * - Filters: action type, actor, time range * - Paginated table * - Export to JSON button */ import React, { useState, useCallback, useEffect } from 'react'; import { useOrg } from './OrgContext'; import { queryAuditLog, exportAuditLog, type AuditEntry, type AuditLogFilters, } from './api'; const ACTION_TYPES = [ 'auth.login', 'auth.logout', 'auth.login_failed', 'api_key.created', 'api_key.revoked', 'member.invited', 'member.removed', 'member.role_changed', 'settings.updated', 'org.ownership_transferred', 'billing.plan_changed', 'billing.payment_failed', 'data.exported', 'permission.denied', ]; const PAGE_SIZE = 20; function formatDateTime(iso: string): string { return new Date(iso).toLocaleString(); } export function AuditLogPage(): React.ReactElement { const { currentOrg } = useOrg(); const [entries, setEntries] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(0); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Filters const [actionFilter, setActionFilter] = useState(''); const [actorFilter, setActorFilter] = useState(''); const [fromDate, setFromDate] = useState(''); const [toDate, setToDate] = useState(''); const load = useCallback(async () => { if (!currentOrg) return; setLoading(true); setError(null); try { const filters: AuditLogFilters = { limit: PAGE_SIZE, offset: page * PAGE_SIZE, }; if (actionFilter) filters.action = actionFilter; if (actorFilter) filters.actor = actorFilter; if (fromDate) filters.from = new Date(fromDate).toISOString(); if (toDate) filters.to = new Date(toDate).toISOString(); const result = await queryAuditLog(currentOrg.id, filters); setEntries(result.entries); setTotal(result.total); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load audit log'); } finally { setLoading(false); } }, [currentOrg, page, actionFilter, actorFilter, fromDate, toDate]); useEffect(() => { load(); }, [load]); const handleExport = useCallback(async () => { if (!currentOrg) return; try { const data = await exportAuditLog( currentOrg.id, fromDate ? new Date(fromDate).toISOString() : undefined, toDate ? new Date(toDate).toISOString() : undefined, ); const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `audit-log-${currentOrg.id}.json`; a.click(); URL.revokeObjectURL(url); } catch (err) { setError(err instanceof Error ? err.message : 'Export failed'); } }, [currentOrg, fromDate, toDate]); const handleFilterApply = useCallback(() => { setPage(0); load(); }, [load]); const totalPages = Math.ceil(total / PAGE_SIZE); if (!currentOrg) return

Select an organization

; return (

Audit Log

{/* Filters */}
setActorFilter(e.target.value)} data-testid="actor-filter" aria-label="Filter by actor" /> setFromDate(e.target.value)} data-testid="from-date" aria-label="From date" /> setToDate(e.target.value)} data-testid="to-date" aria-label="To date" />
{error &&

{error}

} {/* Results Table */} {loading ? (

Loading audit log...

) : entries.length === 0 ? (

No audit log entries found.

) : ( <> {entries.map((entry) => ( ))}
Time Action Actor Resource Result IP
{formatDateTime(entry.created_at)} {entry.action} {entry.actor_id} ({entry.actor_type}) {entry.resource_type}{entry.resource_id ? `: ${entry.resource_id}` : ''} {entry.result} {entry.ip_address ?? '—'}
{/* Pagination */}
Page {page + 1} of {totalPages} ({total} entries)
)}
); } export default AuditLogPage;