'use client'; import { useCallback, useEffect, useRef, useState } from 'react'; import { formatDate } from '@/lib/format'; import type { AuditItem } from '@/lib/admin/list'; import { SortHeader, downloadUrl, type SortOrder } from '../table-ui'; const PER_PAGE = 50; export function AuditTable({ initialEntries, initialTotal, }: { initialEntries: AuditItem[]; initialTotal: number; }) { const [entries, setEntries] = useState(initialEntries); const [total, setTotal] = useState(initialTotal); const [page, setPage] = useState(1); const [action, setAction] = useState(''); const [targetType, setTargetType] = useState(''); const [sort, setSort] = useState('created_at'); const [order, setOrder] = useState('desc'); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const queryParams = useCallback(() => { const params = new URLSearchParams({ page: String(page), perPage: String(PER_PAGE), sort, order, }); if (action.trim()) params.set('action', action.trim()); if (targetType.trim()) params.set('target_type', targetType.trim()); return params; }, [page, action, targetType, sort, order]); const load = useCallback(async () => { setLoading(true); setError(null); try { const res = await fetch(`/api/admin/audit?${queryParams().toString()}`, { credentials: 'same-origin', }); if (!res.ok) { const b = (await res.json().catch(() => ({}))) as { error?: string }; throw new Error(b.error ?? `Request failed (${res.status})`); } const data = (await res.json()) as { entries: AuditItem[]; total: number; }; setEntries(data.entries); setTotal(data.total); } catch (e) { setError((e as Error).message); } finally { setLoading(false); } }, [queryParams]); // First page is server-rendered; skip the on-mount fetch to avoid racing the // SSR session-cookie refresh (which intermittently 401'd). const didMount = useRef(false); useEffect(() => { if (!didMount.current) { didMount.current = true; return; } const t = setTimeout(load, 250); return () => clearTimeout(t); }, [load]); function onSort(col: string) { setPage(1); if (col === sort) { setOrder((o) => (o === 'asc' ? 'desc' : 'asc')); } else { setSort(col); setOrder('asc'); } } function exportCsv() { const params = new URLSearchParams({ sort, order }); if (action.trim()) params.set('action', action.trim()); if (targetType.trim()) params.set('target_type', targetType.trim()); downloadUrl(`/api/admin/audit/export?${params.toString()}`); } const totalPages = Math.max(1, Math.ceil(total / PER_PAGE)); return (

Audit log

{ setPage(1); setAction(e.target.value); }} style={{ maxWidth: 240 }} /> { setPage(1); setTargetType(e.target.value); }} style={{ maxWidth: 240 }} />
{error &&

{error}

} {loading ? (

Loading…

) : entries.length === 0 ? (

No audit entries.

) : (
{entries.map((e) => ( ))}
Target Details
{formatDate(e.created_at)} {e.actor_email ?? e.actor_id ?? '—'} {e.action} {e.target_type ? `${e.target_type}:` : ''} {e.target_id ?? ''} {JSON.stringify(e.details ?? {})}
)}
Page {page} of {totalPages} ({total} total)
); }