"use client" import * as React from 'react' import { useRouter } from 'next/navigation' import type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy' import { CheckCircle2, Clock, Gavel, Replace, Undo2, Workflow } from 'lucide-react' import { Page, PageBody } from '@open-mercato/ui/backend/Page' import { DataTable } from '@open-mercato/ui/backend/DataTable' import { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail' import { EmptyState } from '@open-mercato/ui/primitives/empty-state' import { Avatar } from '@open-mercato/ui/primitives/avatar' import { agentAvatarIcon } from '../../components/agentChips' import { useAgentIconMap } from '../../components/useAgentIcons' import { Button } from '@open-mercato/ui/primitives/button' import { StatusBadge, type StatusMap } from '@open-mercato/ui/primitives/status-badge' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@open-mercato/ui/primitives/select' import { apiCall } from '@open-mercato/ui/backend/utils/apiCall' import { useT, useLocale } from '@open-mercato/shared/lib/i18n/context' import { formatDateTime, formatNumber, mapAgent, mapOverviewMetrics, type OverviewMetricsView } from '../../components/types' import { subjectLabelOf } from '../../components/subjectRef' type Disposition = 'pending' | 'approved' | 'edited' | 'rejected' | 'auto_approved' type DispositionFilter = 'all' | Disposition type AuditRow = { id: string when: string | null agentId: string agentLabel: string subjectRef: string disposition: Disposition operator: string | null reason: string | null workflowInstanceId: string | null } const dispositionVariant: StatusMap = { approved: 'success', auto_approved: 'success', edited: 'warning', rejected: 'error', pending: 'neutral', } const DISPOSITION_FILTERS: DispositionFilter[] = ['all', 'pending', 'approved', 'auto_approved', 'edited', 'rejected'] function asString(value: unknown): string | null { return typeof value === 'string' ? value : null } function asObject(value: unknown): Record | null { return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record) : null } function fieldOf(item: Record, ...keys: string[]): string { for (const key of keys) { const value = asString(item[key]) if (value) return value } return '' } function dispositionOf(value: string): Disposition { return (['approved', 'edited', 'rejected', 'auto_approved'] as string[]).includes(value) ? (value as Disposition) : 'pending' } type ListResponse = { items?: Array>; total?: number; totalIsCapped?: boolean } export default function AgentAuditPage() { const t = useT() const locale = useLocale() const router = useRouter() const [rows, setRows] = React.useState([]) const [total, setTotal] = React.useState(0) const [totalIsCapped, setTotalIsCapped] = React.useState(false) const [metrics, setMetrics] = React.useState(null) const [isLoading, setIsLoading] = React.useState(true) const [error, setError] = React.useState(null) const [page, setPage] = React.useState(1) const [pageSize, setPageSize] = React.useState(20) const [disposition, setDisposition] = React.useState('all') React.useEffect(() => { let cancelled = false async function load() { setIsLoading(true) setError(null) const listParams = new URLSearchParams({ page: String(page), pageSize: String(pageSize), sortField: 'createdAt', sortDir: 'desc', }) if (disposition !== 'all') listParams.set('disposition', disposition) // Server-paginated log + org-level KPIs — the old 100-row client join is // gone; totals now describe the whole tenant, not a sample. const [agentsCall, proposalsCall, overviewCall] = await Promise.all([ apiCall('/api/agent_orchestrator/agents', undefined, { fallback: { items: [] } }), apiCall(`/api/agent_orchestrator/proposals?${listParams.toString()}`, undefined, { fallback: { items: [], total: 0 }, }), apiCall>('/api/agent_orchestrator/metrics/overview?window=7d'), ]) if (cancelled) return if (!proposalsCall.ok) { setError(t('agent_orchestrator.audit.error', 'Could not load the audit log.')) setIsLoading(false) return } const agentLabels = new Map() for (const item of agentsCall.ok && Array.isArray(agentsCall.result?.items) ? agentsCall.result.items : []) { const agent = mapAgent(item) if (agent) agentLabels.set(agent.id, agent.label || agent.id) } const proposals = Array.isArray(proposalsCall.result?.items) ? proposalsCall.result.items : [] setTotal(typeof proposalsCall.result?.total === 'number' ? proposalsCall.result.total : proposals.length) setTotalIsCapped(proposalsCall.result?.totalIsCapped === true) setMetrics(overviewCall.ok && overviewCall.result ? mapOverviewMetrics(overviewCall.result) : null) // Per-page run enrichment (caseload precedent) — only the visible rows. const runIds = Array.from(new Set(proposals.map((p) => fieldOf(p, 'run_id', 'runId')).filter(Boolean))) const runById = new Map>() if (runIds.length > 0) { const runsCall = await apiCall( `/api/agent_orchestrator/runs?ids=${runIds.map((id) => encodeURIComponent(id)).join(',')}&pageSize=${Math.min(runIds.length, 100)}`, undefined, { fallback: { items: [] } }, ) if (cancelled) return for (const run of runsCall.ok && Array.isArray(runsCall.result?.items) ? runsCall.result.items : []) { const runId = fieldOf(run, 'id') if (runId) runById.set(runId, run) } } const built: AuditRow[] = proposals.map((proposal) => { const agentId = fieldOf(proposal, 'agent_id', 'agentId') const runId = fieldOf(proposal, 'run_id', 'runId') const run = runById.get(runId) const input = run ? asObject(run.input) : null const subjectRef = subjectLabelOf(input, runId || fieldOf(proposal, 'id')) return { id: fieldOf(proposal, 'id'), when: fieldOf(proposal, 'created_at', 'createdAt') || null, agentId, agentLabel: agentLabels.get(agentId) || agentId || '—', subjectRef, disposition: dispositionOf(fieldOf(proposal, 'disposition') || 'pending'), operator: fieldOf(proposal, 'disposition_by', 'dispositionBy') || null, reason: fieldOf(proposal, 'disposition_reason', 'dispositionReason') || null, workflowInstanceId: fieldOf(proposal, 'workflow_instance_id', 'workflowInstanceId') || null, } }) setRows(built) setIsLoading(false) } load() return () => { cancelled = true } }, [t, page, pageSize, disposition]) const agentIcons = useAgentIconMap() const columns = React.useMemo[]>(() => [ { accessorKey: 'when', header: t('agent_orchestrator.audit.col.when', 'When'), cell: ({ row }) => {formatDateTime(row.original.when, locale) ?? '—'}, }, { accessorKey: 'agentLabel', header: t('agent_orchestrator.audit.col.agent', 'Agent'), meta: { maxWidth: '280px' }, cell: ({ row }) => { const info = agentIcons.get(row.original.agentId) return (
{row.original.agentLabel}
{row.original.agentId}
) }, }, { accessorKey: 'subjectRef', header: t('agent_orchestrator.audit.col.claim', 'Claim'), cell: ({ row }) => {row.original.subjectRef}, }, { accessorKey: 'disposition', header: t('agent_orchestrator.audit.col.action', 'Action'), cell: ({ row }) => ( {t(`agent_orchestrator.disposition.${row.original.disposition}`, titleCase(row.original.disposition))} ), }, { accessorKey: 'operator', header: t('agent_orchestrator.audit.col.operator', 'Operator'), cell: ({ row }) => row.original.operator ? ( ) : —, }, { accessorKey: 'reason', header: t('agent_orchestrator.audit.col.reason', 'Reason'), meta: { maxWidth: '320px' }, cell: ({ row }) => row.original.reason ? {row.original.reason} : —, }, ], [t, locale, router, agentIcons]) if (isLoading && rows.length === 0) { return ( ) } if (error) { return ( ) } // KPIs are org-level aggregates from /metrics/overview — dispositionCounts is // the CURRENT backlog state (all proposals), correctionsCount is windowed. const counts = metrics?.dispositionCounts ?? {} const decisionsTotal = Object.values(counts).reduce((sum, count) => sum + count, 0) const approvedCount = (counts.approved ?? 0) + (counts.auto_approved ?? 0) const overriddenCount = (counts.edited ?? 0) + (counts.rejected ?? 0) const pendingCount = counts.pending ?? 0 const correctionsCount = metrics?.correctionsCount ?? null const totalPages = Math.max(1, Math.ceil(total / pageSize)) return (

{t('agent_orchestrator.audit.title', 'Audit log')}

{total === 0 && disposition === 'all' ? ( ) : (
columns={columns} data={rows} title={ } pagination={{ page, pageSize, total, totalPages, totalIsCapped, onPageChange: setPage, pageSizeOptions: [10, 20, 50], onPageSizeChange: (next) => { setPageSize(next); setPage(1) }, }} columnChooser={{ auto: true }} perspective={{ tableId: 'agent_orchestrator.audit.list', align: 'right' }} onRowClick={(row) => router.push(`/backend/caseload/${encodeURIComponent(row.id)}`)} rowActions={(row) => row.workflowInstanceId ? ( ) : null } />

{t('agent_orchestrator.audit.log.serverPaginatedNote', 'The log is paginated server-side across all decisions; narrow it with the disposition filter.')}

)}
) } function titleCase(value: string): string { return value.charAt(0).toUpperCase() + value.slice(1) } // Matches the canonical cockpit KPI tile (overview): a white card with an icon // chip and a thin brand gradient accent bar along the bottom. function StatCard({ icon: Icon, label, value, sub, }: { icon: React.ComponentType<{ className?: string }> label: string value: string sub: string }) { return (

{label}

{value}

{sub}

) }