import { useEffect, useState } from "react"; import { api, type AuditLogEntry } from "../api/client"; import { formatDateTime, parseDbTimestamp } from "../utils/date"; import { useSortable } from "../hooks/useSortable"; import SortableTh from "../components/SortableTh"; import { usePagination } from "../hooks/usePagination"; import Pagination from "../components/Pagination"; import { downloadCsv } from "../utils/csv"; function targetLabel(e: AuditLogEntry): string { if (!e.targetType) return "—"; let detail: Record | null = null; if (e.detail) { try { detail = JSON.parse(e.detail); } catch { detail = null; } } const name = detail?.name ?? detail?.targetLabel ?? detail?.ipAddress; const typeLabel = e.targetType.replace(/_/g, " "); if (typeof name === "string" && name) return `${typeLabel} "${name}"`; return `${typeLabel}${e.targetId ? ` #${e.targetId}` : ""}`; } /** What was done, beyond the target — the link's label and URL, a scan's address and range, and so on. The name is already in the Target column. */ function detailSummary(e: AuditLogEntry): string { if (!e.detail) return ""; let parsed: unknown; try { parsed = JSON.parse(e.detail); } catch { return e.detail; } if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return String(parsed); return Object.entries(parsed as Record) .filter(([key, value]) => key !== "name" && value !== null && value !== undefined && value !== "" && !(Array.isArray(value) && value.length === 0)) .map(([key, value]) => `${key}: ${typeof value === "object" ? JSON.stringify(value) : String(value)}`) .join(" · "); } export default function AuditLog() { const [entries, setEntries] = useState(null); const [error, setError] = useState(null); useEffect(() => { api.auditLog .list() .then((res) => setEntries(res.entries)) .catch((err) => setError(String(err))); }, []); const { sorted, sortKey, sortDir, requestSort } = useSortable(entries, "createdAt", "desc"); const { pageItems, page, setPage, pageCount, totalCount } = usePagination(sorted); function exportCsv() { if (!sorted) return; downloadCsv( "audit-log.csv", ["When", "Actor", "Category", "Action", "Target", "Details"], sorted.map((e) => [formatDateTime(parseDbTimestamp(e.createdAt)), e.actorLabel ?? "", e.category, e.action, targetLabel(e), detailSummary(e)]), ); } return ( <>

Audit Log

{error &&
{error}
}
label="When" sortKeyName="createdAt" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> label="Actor" sortKeyName="actorLabel" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> label="Category" sortKeyName="category" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> label="Action" sortKeyName="action" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> label="Target" sortKeyName="targetType" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> {pageItems?.map((e) => ( ))} {sorted?.length === 0 && ( )}
Details
{formatDateTime(parseDbTimestamp(e.createdAt))} {e.actorLabel ?? "—"} {e.category} {e.action} {targetLabel(e)} {detailSummary(e) || "—"}
No activity recorded yet.
); }