import { useCallback, useEffect, useMemo, useState } from "react" import { Activity, Eye, RefreshCw } from "lucide-react" import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-client" import { ActionsCell, BadgeCell, DataTable, DateCell, Pagination, useTable, type BadgeTone, type Column, } from "@crema/table-ui" import { SearchInput } from "@crema/search-ui" import { AlertBanner, EmptyState, LoadingOverlay } from "@crema/feedback-ui" import { AppShell } from "~/components/layout/app-shell" import { Button } from "~/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "~/components/ui/card" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "~/components/ui/dialog" import { Input } from "~/components/ui/input" import { Label } from "~/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "~/components/ui/select" import { listAuditLogs, type AuditLog, type AuditSeverity, } from "~/lib/arcadia/audit-logs" import { pageTitle } from "~/lib/page-meta" import { useSession } from "~/lib/session" import { useRegisterAdminContext } from "~/lib/admin-context" export const meta = () => pageTitle("Audit log") export default function ActivityRoute() { const session = useSession() const arcadia = useArcadiaClient() const [logs, setLogs] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [search, setSearch] = useState("") const [severityFilter, setSeverityFilter] = useState<"all" | AuditSeverity>("all") const [resourceFilter, setResourceFilter] = useState("") const [from, setFrom] = useState("") const [to, setTo] = useState("") const [detail, setDetail] = useState(null) const refresh = useCallback(async () => { setError(null) setLoading(true) try { const list = await listAuditLogs(arcadia, { severity: severityFilter === "all" ? undefined : severityFilter, resource_type: resourceFilter || undefined, from: from ? new Date(from).toISOString() : undefined, to: to ? new Date(to).toISOString() : undefined, limit: 200, }) setLogs(list) } catch (err) { setError(err instanceof ArcadiaError ? err.message : "Failed to load audit logs.") } finally { setLoading(false) } }, [arcadia, severityFilter, resourceFilter, from, to]) useEffect(() => { if (session) refresh() }, [session, refresh]) const columns = useMemo[]>( () => [ { id: "time", header: "Time", accessor: "inserted_at", sortable: true, cell: (l) => , }, { id: "user", header: "User", accessor: (l) => l.user?.email ?? "", sortable: true, cell: (l) => ( {l.user?.email ?? system} ), }, { id: "action", header: "Action", accessor: "action", sortable: true, cell: (l) => ( {l.action} ), }, { id: "resource", header: "Resource", accessor: "resource_type", sortable: true, cell: (l) => ( {l.resource_type} {l.resource_id ? ( {l.resource_id.slice(0, 8)}… ) : null} ), }, { id: "severity", header: "Severity", accessor: "severity", sortable: true, cell: (l) => , }, { id: "ip", header: "IP", accessor: "ip_address", cell: (l) => ( {l.ip_address ?? "—"} ), }, { id: "actions", header: "", align: "right", cell: (l) => ( , dataAction: `audit-${l.id}-view`, onSelect: () => setDetail(l), }, ]} triggerDataAction={`audit-${l.id}-actions`} /> ), }, ], [], ) const summary = useMemo( () => ({ total: logs.length, bySeverity: countBy(logs, (l) => l.severity || "info"), byResource: countBy(logs, (l) => l.resource_type), latest: logs.slice(0, 5).map((l) => ({ time: l.inserted_at, user: l.user?.email ?? "system", action: l.action, resource: `${l.resource_type}${l.resource_id ? `/${l.resource_id}` : ""}`, })), }), [logs], ) useRegisterAdminContext("audit_log", summary) const table = useTable({ data: logs, columns, getRowId: (l) => l.id, initialPageSize: 50, initialSearch: search, }) useEffect(() => { table.setSearch(search) }, [search, table]) return (

Audit log

Every authenticated action against the platform. Filter by date, severity, or resource type.

{error ? ( setError(null)}> {error} ) : null}
setResourceFilter(e.target.value)} placeholder="e.g. user" className="w-40" data-action="audit-resource-filter" />
setFrom(e.target.value)} className="w-44" data-action="audit-from-filter" />
setTo(e.target.value)} className="w-44" data-action="audit-to-filter" />
{table.total === 0 && !loading ? ( } title="No events match those filters." description="Loosen the filter set or wait for new platform activity." className="py-12" /> ) : ( <> l.id} sort={table.sort} onSortToggle={table.toggleSort} loading={loading && logs.length > 0} stickyHeader /> )}
!o && setDetail(null)}> Audit event {detail ? `${detail.action} on ${detail.resource_type} at ${new Date( detail.inserted_at, ).toLocaleString()}` : ""} {detail ? : null}
) } function AuditDetailBody({ log }: { log: AuditLog }) { const rows: { k: string; v: string }[] = [ { k: "ID", v: log.id }, { k: "Tenant", v: log.tenant_id }, { k: "User", v: log.user?.email ?? log.user_id ?? "—" }, { k: "Action", v: log.action }, { k: "Resource", v: `${log.resource_type}${log.resource_id ? `/${log.resource_id}` : ""}` }, { k: "Severity", v: log.severity }, { k: "IP", v: log.ip_address ?? "—" }, { k: "User agent", v: log.user_agent ?? "—" }, { k: "Time", v: new Date(log.inserted_at).toISOString() }, ] return (
{rows.map((r) => (
{r.k}
{r.v}
))}
{log.changes ? (

Changes

            {JSON.stringify(log.changes, null, 2)}
          
) : null} {log.metadata && Object.keys(log.metadata).length > 0 ? (

Metadata

            {JSON.stringify(log.metadata, null, 2)}
          
) : null}
) } function severityTone(s: AuditSeverity): BadgeTone { if (s === "critical" || s === "error") return "danger" if (s === "warning") return "warning" return "default" } function countBy(arr: T[], key: (x: T) => string): Record { return arr.reduce>((acc, x) => { const k = key(x) acc[k] = (acc[k] ?? 0) + 1 return acc }, {}) }