"use client"; import { isValidElement, useEffect, useMemo, useState } from "react"; import type { ReactNode } from "react"; import { format } from "date-fns"; import { useQuery } from "@tanstack/react-query"; import { Loader2 } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import type { BadgeVariant } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, } from "@/components/ui/sheet"; import { apiGet } from "@/lib/api/client"; const AUDIT_LOG_REFRESH_INTERVAL_MS = 10_000; const AUDIT_LOG_PAGE_SIZE = 50; const SUMMARY_MAX_LENGTH = 120; interface AuditLogDrawerProps { sessionId?: string; trigger: ReactNode; } interface AuditLogEntry { id: string; userId: string; sessionId: string; provider: string; action: string; content: string | null; metadata: unknown; createdAt: string; } interface AuditLogResponse { items: AuditLogEntry[]; total: number; page: number; pages: number; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } function truncateText(value: string, maxLength: number): string { if (value.length <= maxLength) { return value; } return `${value.slice(0, maxLength - 1)}…`; } function truncateSessionId(sessionId: string): string { return sessionId.slice(0, 8); } function formatTimestamp(value: string): string { const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) { return "Unknown"; } return format(parsed, "yyyy-MM-dd HH:mm:ss"); } function stringifyPayloadValue(value: unknown): string { if (typeof value === "string") { return value; } if (typeof value === "number" || typeof value === "boolean") { return String(value); } try { return JSON.stringify(value); } catch { return "[unserializable]"; } } function getPayloadSummary(entry: AuditLogEntry): string { const metadata = isRecord(entry.metadata) ? entry.metadata : undefined; const payload = metadata && isRecord(metadata.payload) ? metadata.payload : undefined; if (typeof entry.content === "string" && entry.content.trim().length > 0) { return truncateText(entry.content.trim(), SUMMARY_MAX_LENGTH); } if (payload) { const summary = Object.entries(payload) .map(([key, value]) => `${key}=${stringifyPayloadValue(value)}`) .join(", "); if (summary.length > 0) { return truncateText(summary, SUMMARY_MAX_LENGTH); } } return "—"; } function getActionVariant(action: string): BadgeVariant { switch (action) { case "inject": return "badge-blue"; case "pause": return "status-warning"; case "resume": return "status-success"; case "kill": return "status-error"; default: return "status-neutral"; } } async function fetchAuditLog( sessionId: string | undefined, page: number ): Promise { const params = new URLSearchParams({ page: String(page), limit: String(AUDIT_LOG_PAGE_SIZE), }); const normalizedSessionId = sessionId?.trim(); if (normalizedSessionId && normalizedSessionId.length > 0) { params.set("sessionId", normalizedSessionId); } return apiGet(`/api/mission-control/audit-log?${params.toString()}`); } export function AuditLogDrawer({ sessionId, trigger }: AuditLogDrawerProps): React.JSX.Element { const [open, setOpen] = useState(false); const [page, setPage] = useState(1); const triggerElement = useMemo( () => isValidElement(trigger) ? ( trigger ) : ( ), [trigger] ); const auditLogQuery = useQuery({ queryKey: ["mission-control", "audit-log", sessionId ?? "all", page], queryFn: async (): Promise => fetchAuditLog(sessionId, page), enabled: open, refetchInterval: open ? AUDIT_LOG_REFRESH_INTERVAL_MS : false, }); useEffect(() => { if (open) { setPage(1); } }, [open, sessionId]); useEffect(() => { const pages = auditLogQuery.data?.pages; if (pages !== undefined && pages > 0 && page > pages) { setPage(pages); } }, [auditLogQuery.data?.pages, page]); const totalItems = auditLogQuery.data?.total ?? 0; const totalPages = auditLogQuery.data?.pages ?? 0; const items = auditLogQuery.data?.items ?? []; const canGoPrevious = page > 1; const canGoNext = totalPages > 0 && page < totalPages; const errorMessage = auditLogQuery.error instanceof Error ? auditLogQuery.error.message : "Failed to load audit log"; return ( {triggerElement}
Audit Log {auditLogQuery.isFetching ? (
{sessionId ? `Showing actions for session ${sessionId}.` : "Showing operator actions across all mission control sessions."}
{auditLogQuery.isLoading ? ( ) : auditLogQuery.error ? ( ) : items.length === 0 ? ( ) : ( items.map((entry) => { const payloadSummary = getPayloadSummary(entry); return ( ); }) )}
Timestamp Action Session Operator Payload
Loading audit log...
{errorMessage}
No audit entries found.
{formatTimestamp(entry.createdAt)} {entry.action} {truncateSessionId(entry.sessionId)} {entry.userId} {payloadSummary}

{totalItems} total entries

Page {page} of {Math.max(totalPages, 1)}
); }