import { h } from 'preact'; import { useState, useEffect, useCallback } from 'preact/hooks'; // AuditEvent mirrors the JSON returned by GET /api/v1/audit (internal/audit.Event). interface AuditEvent { time: string; actor: string; actorType: string; // "user" | "system" action: string; ds?: string; signal?: string; value?: string; detail?: string; ip?: string; outcome: string; // "ok" | "error" error?: string; } interface Props { onClose: () => void; } /** Convert an RFC3339/ISO timestamp to a compact local datetime string. */ function fmtTime(iso: string): string { const d = new Date(iso); if (isNaN(d.getTime())) return iso; const pad = (n: number) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; } /** Convert a datetime-local input value (local time) to an RFC3339 string. */ function localToRFC3339(v: string): string { if (!v) return ''; const d = new Date(v); return isNaN(d.getTime()) ? '' : d.toISOString(); } export default function AuditViewer({ onClose }: Props) { const [events, setEvents] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // Filters const [user, setUser] = useState(''); const [action, setAction] = useState(''); const [signal, setSignal] = useState(''); const [start, setStart] = useState(''); const [end, setEnd] = useState(''); const load = useCallback(async () => { setLoading(true); setError(null); try { const q = new URLSearchParams(); if (user.trim()) q.set('user', user.trim()); if (action) q.set('action', action); if (signal.trim()) q.set('signal', signal.trim()); const s = localToRFC3339(start); const e = localToRFC3339(end); if (s) q.set('start', s); if (e) q.set('end', e); const res = await fetch(`/api/v1/audit?${q.toString()}`); if (res.status === 403) throw new Error('You are not permitted to view the audit log.'); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); setEvents(Array.isArray(data) ? data : []); } catch (err) { setError(err instanceof Error ? err.message : String(err)); setEvents([]); } finally { setLoading(false); } }, [user, action, signal, start, end]); // Initial load. useEffect(() => { load(); }, []); // eslint-disable-line react-hooks/exhaustive-deps function resetFilters() { setUser(''); setAction(''); setSignal(''); setStart(''); setEnd(''); } return (
{ if (e.target === e.currentTarget) onClose(); }}>
Audit log Every system-affecting action (signal writes, control-logic and interface changes).
setUser((e.target as HTMLInputElement).value)} /> setSignal((e.target as HTMLInputElement).value)} />
{error &&
{error}
}
{!error && events.length === 0 && !loading && (
No matching audit entries.
)} {events.length > 0 && ( {events.map((ev, i) => ( ))}
Time Actor Action Source Signal / target Value Detail Client Result
{fmtTime(ev.time)} {ev.actorType} {' '}{ev.actor || '—'} {ev.action} {ev.ds || '—'} {ev.signal || '—'} {ev.value ?? '—'} {ev.detail || '—'} {ev.ip || '—'} {ev.outcome === 'error' ? error : ok}
)}
); }