"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { type ColumnDef, type ColumnFiltersState, type VisibilityState, getCoreRowModel, useReactTable, } from "@tanstack/react-table"; import { getEvents, type EventLogEntry } from "@/lib/api"; import { DataTable } from "@/components/data-table/data-table"; import { DataTableColumnHeader } from "@/components/data-table/data-table-column-header"; import { DataTableToolbar } from "@/components/data-table/data-table-toolbar"; import { CodeBlock } from "@/components/code-block"; import { SiteHeader } from "@/components/site-header"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { ResponsiveDialog, ResponsiveDialogContent, ResponsiveDialogHeader, ResponsiveDialogTitle, } from "@/components/ui/responsive-dialog"; import { ChevronLeft, ChevronRight } from "lucide-react"; function severityBadge(severity: string) { switch (severity) { case "error": return error; case "warn": return ( warn ); default: return info; } } function timeAgo(dateStr: string): string { const now = Date.now(); const then = new Date(dateStr).getTime(); const seconds = Math.floor((now - then) / 1000); if (seconds < 60) return `${seconds}s ago`; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); return `${days}d ago`; } /** Known detail keys that get their own sections — excluded from the "Other" dump. */ const KNOWN_KEYS = [ "error", "errorType", "method", "line", "input", "params", "response", "script_source", "caller_did", "duration_ms", "response_size", "message", "level", ] as const; function EventDetailBody({ event }: { event: EventLogEntry }) { const d = event.detail; // Collect any keys not in KNOWN_KEYS for the "Other" section const otherKeys = Object.keys(d).filter( (k) => !(KNOWN_KEYS as readonly string[]).includes(k), ); const otherDetail = otherKeys.length > 0 ? Object.fromEntries(otherKeys.map((k) => [k, d[k]])) : null; return (
{/* Metadata grid */}
Subject

{event.subject ?? "--"}

Actor

{event.actor_did ?? "System"}

Time

{new Date(event.created_at).toLocaleString()}

{d.method != null && (
Method

{String(d.method)}

)} {d.caller_did != null && (
Caller DID

{String(d.caller_did)}

)} {d.duration_ms != null && (
Duration

{String(d.duration_ms)}ms

)} {d.response_size != null && (
Response Size

{Number(d.response_size).toLocaleString()} bytes

)}
{/* Plugin log message */} {d.message != null && (
Message {d.level != null && ( {String(d.level)} )}

{String(d.message)}

)} {/* Error section */} {d.error != null && (
Error
{d.errorType != null && ( {String(d.errorType)} )} {d.line != null && ( line {String(d.line)} )}

{String(d.error)}

)} {/* Request input/params */} {(d.input != null || d.params != null) && (
{d.input != null ? "Request Input" : "Request Params"}
)} {/* Response */} {d.response != null && (
Response
)} {/* Script source */} {d.script_source != null && (
Script Source
)} {/* Other detail fields */} {otherDetail && (
Other
)}
); } export default function EventsPage() { const [events, setEvents] = useState([]); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const [viewEvent, setViewEvent] = useState(null); // Pagination const [cursorStack, setCursorStack] = useState([]); const [nextCursor, setNextCursor] = useState(null); // Filters — driven by TanStack column filter state const [columnFilters, setColumnFilters] = useState([]); const [columnVisibility, setColumnVisibility] = useState({}); // Debounce subject filter to avoid firing on every keystroke const debounceRef = useRef>(null); const [debouncedFilters, setDebouncedFilters] = useState(columnFilters); useEffect(() => { const subjectFilter = columnFilters.find((f) => f.id === "subject"); const prevSubjectFilter = debouncedFilters.find((f) => f.id === "subject"); const subjectChanged = subjectFilter?.value !== prevSubjectFilter?.value; if (subjectChanged) { if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => { setDebouncedFilters(columnFilters); }, 300); } else { setDebouncedFilters(columnFilters); } }, [columnFilters]); const fetchEvents = useCallback( async (cursor?: string) => { setLoading(true); setError(null); try { const categoryFilter = debouncedFilters.find( (f) => f.id === "event_type", )?.value as string[] | undefined; const severityFilter = debouncedFilters.find((f) => f.id === "severity") ?.value as string[] | undefined; const subjectFilter = debouncedFilters.find((f) => f.id === "subject") ?.value as string | undefined; const data = await getEvents({ category: categoryFilter?.length ? categoryFilter.join(",") : undefined, severity: severityFilter?.length ? severityFilter.join(",") : undefined, subject: subjectFilter || undefined, cursor, limit: 50, }); setEvents(data.events); setNextCursor(data.cursor); } catch (e: unknown) { setError(e instanceof Error ? e.message : String(e)); setEvents([]); setNextCursor(null); } finally { setLoading(false); } }, [debouncedFilters], ); // Fetch on mount and when filters change (reset to first page) useEffect(() => { setCursorStack([]); fetchEvents(); }, [fetchEvents]); // Auto-refresh every 5s when on first page useEffect(() => { if (cursorStack.length > 0) return; const interval = setInterval(() => fetchEvents(), 5000); return () => clearInterval(interval); }, [fetchEvents, cursorStack.length]); function handleNext() { if (!nextCursor) return; setCursorStack((prev) => [...prev, nextCursor]); fetchEvents(nextCursor); } function handlePrevious() { if (cursorStack.length === 0) return; const stack = [...cursorStack]; stack.pop(); const prevCursor = stack.length > 0 ? stack[stack.length - 1] : undefined; setCursorStack(stack); fetchEvents(prevCursor); } const columns = useMemo[]>( () => [ { id: "subject", accessorKey: "subject", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.subject ?? "--"} ), enableColumnFilter: true, enableSorting: false, meta: { label: "Subject", placeholder: "Filter by subject...", variant: "text", }, }, { id: "severity", accessorKey: "severity", header: ({ column }) => ( ), cell: ({ row }) => severityBadge(row.original.severity), enableColumnFilter: true, enableSorting: false, enableHiding: false, meta: { label: "Severity", variant: "select", options: [ { label: "Info", value: "info" }, { label: "Warn", value: "warn" }, { label: "Error", value: "error" }, ], }, }, { id: "event_type", accessorKey: "event_type", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.event_type} ), enableColumnFilter: true, enableSorting: false, meta: { label: "Category", variant: "select", options: [ { label: "Lexicon", value: "lexicon" }, { label: "Record", value: "record" }, { label: "Script", value: "script" }, { label: "Admin", value: "admin" }, { label: "Backfill", value: "backfill" }, { label: "Plugin", value: "plugin" }, ], }, }, { id: "actor_did", accessorKey: "actor_did", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.actor_did ?? "System"} ), enableSorting: false, }, { id: "created_at", accessorKey: "created_at", header: ({ column }) => ( ), cell: ({ row }) => ( {timeAgo(row.original.created_at)} ), enableSorting: false, }, ], [], ); const table = useReactTable({ data: events, columns, state: { columnFilters, columnVisibility }, defaultColumn: { enableColumnFilter: false, }, onColumnFiltersChange: setColumnFilters, onColumnVisibilityChange: setColumnVisibility, getCoreRowModel: getCoreRowModel(), getRowId: (row) => row.id, }); return ( <>
{error &&

{error}

}

{events.length} event(s) on this page.

{viewEvent && ( setViewEvent(null)}> {severityBadge(viewEvent.severity)} {viewEvent.event_type} )}
); }