import Link from "next/link"; import { getAuditLog, getAuditFacets, formatRelative } from "@/lib/reports"; export const dynamic = "force-dynamic"; /** * Audit log viewer. Cursor-paginated, with action / entity / actor filters * driven by the GET query string. Server-rendered so the URL is the source * of truth — bookmarking a filtered view just works, and page reloads don't * drop state. * * The JSON `before`/`after` payloads can be long; we render them lazily * inside a
block so the table stays scannable. */ export default async function AuditPage({ searchParams, }: { searchParams: Promise<{ action?: string; entity?: string; actorId?: string; cursor?: string; }>; }) { const { action, entity, actorId, cursor } = await searchParams; const [facets, { rows, nextCursor }] = await Promise.all([ getAuditFacets(), getAuditLog({ limit: 50, action: action || null, entity: entity || null, actorId: actorId || null, cursor: cursor || null, }), ]); const now = new Date(); // Build a filter-preserving URL for the "Next page" link. const nextHref = nextCursor ? `/admin/audit?${new URLSearchParams({ ...(action ? { action } : {}), ...(entity ? { entity } : {}), ...(actorId ? { actorId } : {}), cursor: nextCursor, }).toString()}` : null; const filtersActive = !!(action || entity || actorId); return (

Audit log

Every write — claim, release, close, create, update, delete, login — lands here. Newest first.

{/* ─── Filter bar ──────────────────────────────────────────── */}
{filtersActive ? ( Clear ) : null}
{/* ─── Results ─────────────────────────────────────────────── */}
{rows.length === 0 ? (

No events match. Clear filters or widen your search.

) : ( {rows.map((row) => ( ))}
When Actor Action Entity Payload IP
{formatRelative(row.at, now)}
{row.at.toLocaleString()}
{row.actor ? ( <>
{row.actor.name}
{row.actor.role}
) : ( system )}
{row.action} {row.entity} {row.entityId ? /{row.entityId} : null} {row.before || row.after ? (
{row.after ? "after" : "before"} {row.before && row.after ? " / before" : ""}
{row.after ? ( ) : null} {row.before ? ( ) : null}
) : ( )}
{row.ipAddress ?? "—"}
)}
{/* ─── Pagination ──────────────────────────────────────────── */}
{rows.length} row{rows.length === 1 ? "" : "s"} on this page
{nextHref ? ( Older → ) : ( End of log )}
); } /** * Pretty-prints a JSON audit payload. Falls back to the raw string if the * row happens to contain non-JSON (older rows, or writes that logged a * free-form message). */ function JsonBlock({ label, value }: { label: string; value: string }) { let pretty = value; try { pretty = JSON.stringify(JSON.parse(value), null, 2); } catch { // leave as-is } return (
{label}
        {pretty}
      
); }