"use client"; import { useCallback, useEffect, useState } from "react"; interface AuditRow { id: string; action: string; resource_type: string | null; resource_id: string | null; ip_address: string | null; user_agent: string | null; metadata: Record | null; created_at: string; user_email: string | null; user_name: string | null; family_name: string | null; } interface Data { events: AuditRow[]; actions: string[]; resourceTypes: string[]; error?: string } const WINDOWS = [ { label: "24h", hours: 24 }, { label: "7d", hours: 168 }, { label: "30d", hours: 720 }, { label: "90d", hours: 2160 }, ]; export default function AdminAudit() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [action, setAction] = useState(""); const [resourceType, setResourceType] = useState(""); const [hours, setHours] = useState(168); const [q, setQ] = useState(""); const [expanded, setExpanded] = useState(null); const load = useCallback(() => { setLoading(true); const params = new URLSearchParams({ sinceHours: String(hours) }); if (action) params.set("action", action); if (resourceType) params.set("resourceType", resourceType); if (q.trim()) params.set("q", q.trim()); fetch(`/api/admin/audit?${params}`, { credentials: "include" }) .then(r => r.json()) .then(d => { setData({ events: Array.isArray(d?.events) ? d.events : [], actions: Array.isArray(d?.actions) ? d.actions : [], resourceTypes: Array.isArray(d?.resourceTypes) ? d.resourceTypes : [], error: d?.error, }); setLoading(false); }) .catch(() => setLoading(false)); }, [hours, action, resourceType, q]); useEffect(() => { load(); }, [hours, action, resourceType]); // eslint-disable-line react-hooks/exhaustive-deps return (

Audit Log

Every recorded action across the platform

{WINDOWS.map(w => ( ))}
{ e.preventDefault(); load(); }} className="flex-1 min-w-[180px]"> setQ(e.target.value)} placeholder="Search user / family / action…" className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-1.5 text-sm" />
{data?.error &&
Feed error: {data.error}
} {loading ? (
Loading…
) : (
{(data?.events.length ?? 0) === 0 ? (
No audit events match these filters
) : (
{data!.events.map(e => { const hasMeta = e.metadata && Object.keys(e.metadata).length > 0; return (
hasMeta && setExpanded(expanded === e.id ? null : e.id)}> {e.action} {e.resource_type && {e.resource_type}}
{(e.user_email || e.user_name) && 👤 {e.user_name || e.user_email}} {e.family_name && 🏠 {e.family_name}} {e.ip_address && 🌐 {e.ip_address}} 🕒 {timeAgo(e.created_at)}
{hasMeta && {expanded === e.id ? "▲" : "▼"}}
{hasMeta && expanded === e.id && (
{JSON.stringify(e.metadata, null, 2)}
)}
); })}
)}
)}
); } function timeAgo(iso: string) { const diff = Date.now() - new Date(iso).getTime(); const m = Math.floor(diff / 60000); if (m < 1) return "just now"; if (m < 60) return `${m}m ago`; const h = Math.floor(m / 60); if (h < 24) return `${h}h ago`; return `${Math.floor(h / 24)}d ago`; }