'use client'; import React, { useState, useMemo, useCallback } from 'react'; import Link from 'next/link'; import { useReactTable, getCoreRowModel, getFilteredRowModel, getSortedRowModel, getPaginationRowModel, ColumnDef, flexRender, SortingState, ColumnFiltersState, } from '@tanstack/react-table'; import { ExecutionStatus } from '@/lib/pipeline-types'; interface ExecutionsViewProps { executions: ExecutionStatus[]; pipelineId: string; onRefresh?: () => void; } // Helper to format duration function formatDuration(ms: number | null | undefined): string { if (!ms) return '-'; if (ms < 1000) return `${ms}ms`; const seconds = ms / 1000; if (seconds < 60) return `${seconds.toFixed(1)}s`; const mins = Math.floor(seconds / 60); const secs = Math.round(seconds % 60); return `${mins}m ${secs}s`; } // Helper to format date function formatDate(dateStr: string | null | undefined): string { if (!dateStr) return '-'; const date = new Date(dateStr); return date.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }); } // Sort icon component function SortIcon({ sorted }: { sorted: false | 'asc' | 'desc' }) { if (!sorted) { return ( ); } return ( ); } export default function ExecutionsView({ executions, pipelineId, onRefresh }: ExecutionsViewProps) { const [sorting, setSorting] = useState([{ id: 'created_at', desc: true }]); const [columnFilters, setColumnFilters] = useState([]); const [globalFilter, setGlobalFilter] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); // Debug modal state const [debugExecution, setDebugExecution] = useState(null); const [copyingReport, setCopyingReport] = useState(null); // Generate debug report for AI const generateDebugReport = useCallback((execution: ExecutionStatus): string => { const now = new Date().toISOString(); const duration = execution.total_duration_ms ? formatDuration(execution.total_duration_ms) : execution.started_at && execution.completed_at ? formatDuration(new Date(execution.completed_at).getTime() - new Date(execution.started_at).getTime()) : 'Unknown'; const stageMetricsFormatted = execution.stage_metrics ? Object.entries(execution.stage_metrics) .map(([stage, metrics]) => ` - ${stage}: ${metrics.duration_ms}ms, success=${metrics.success}, in=${metrics.records_in}, out=${metrics.records_out}${metrics.error ? `, error="${metrics.error}"` : ''}`) .join('\n') : ' No stage metrics available'; return `## Pipeline Execution Debug Report **Generated**: ${now} **Execution ID**: ${execution.id} **Pipeline**: ${execution.pipeline_id} **Status**: ${execution.status.toUpperCase()} ### Timeline - **Created**: ${execution.created_at || 'N/A'} - **Started**: ${execution.started_at || 'N/A'} - **Completed**: ${execution.completed_at || 'N/A'} - **Duration**: ${duration} ### Stages - **Requested**: ${execution.stages_requested?.join(' → ') || 'N/A'} - **Completed**: ${execution.stages_completed?.join(' → ') || 'None'} - **Progress**: ${execution.progress}% ### Stage Metrics ${stageMetricsFormatted} ### Error \`\`\` ${execution.error_message || 'No error message captured'} \`\`\` ### Input Summary \`\`\`json ${JSON.stringify(execution.input_summary || {}, null, 2)} \`\`\` ### Result Summary \`\`\`json ${JSON.stringify(execution.result_summary || {}, null, 2)} \`\`\` ### Context for Debugging - This is a ReviewIQ pipeline execution - Pipeline stages: normalize → classify → route → aggregate - The classify stage uses OpenAI/Anthropic for URT classification - Common failure points: API rate limits, database connection, invalid input data ### Suggested Investigation 1. Check if error is related to LLM API (rate limiting, auth, timeout) 2. Check database connectivity issues 3. Review input data validity 4. Check stage metrics for which stage failed and why `; }, []); const copyDebugReport = useCallback(async (execution: ExecutionStatus) => { setCopyingReport(execution.id); try { const report = generateDebugReport(execution); await navigator.clipboard.writeText(report); setTimeout(() => setCopyingReport(null), 1500); } catch (err) { console.error('Failed to copy debug report:', err); setCopyingReport(null); } }, [generateDebugReport]); // Filter executions by status const filteredExecutions = useMemo(() => { if (statusFilter === 'all') return executions; return executions.filter(e => e.status === statusFilter); }, [executions, statusFilter]); // Status counts for filter badges const statusCounts = useMemo(() => { const counts: Record = { all: executions.length }; executions.forEach(e => { counts[e.status] = (counts[e.status] || 0) + 1; }); return counts; }, [executions]); const columns = useMemo[]>( () => [ { accessorKey: 'id', header: 'Execution ID', cell: ({ row }) => ( {row.original.id.slice(0, 8)}... ), }, { accessorKey: 'job_id', header: 'Job', cell: ({ row }) => { const jobId = row.original.job_id; if (!jobId) return -; return ( e.stopPropagation()} > {jobId.slice(0, 8)}... ); }, }, { accessorKey: 'status', header: ({ column }) => ( ), cell: ({ row }) => { const status = row.original.status; return ( {status === 'running' && (
)} {status.charAt(0).toUpperCase() + status.slice(1)} ); }, }, { accessorKey: 'stages_completed', header: 'Stages', cell: ({ row }) => { const requested = row.original.stages_requested || []; const completed = row.original.stages_completed || []; return (
{requested.map((stage, i) => ( {stage.slice(0, 3)} ))}
); }, }, { accessorKey: 'total_duration_ms', header: ({ column }) => ( ), cell: ({ row }) => ( {formatDuration(row.original.total_duration_ms)} ), }, { accessorKey: 'created_at', header: ({ column }) => ( ), cell: ({ row }) => ( {formatDate(row.original.created_at)} ), }, { id: 'actions', header: 'Actions', cell: ({ row }) => { const execution = row.original; const isFailed = execution.status === 'failed'; const isCompleted = execution.status === 'completed'; return (
{/* Analytics button - only for completed */} {isCompleted && ( e.stopPropagation()} title="View Analytics" > Analytics )} {/* Metrics button - for completed or failed */} e.stopPropagation()} title="View Execution Metrics" > Metrics {/* Debug button - only for failed */} {isFailed && ( )}
); }, }, ], [pipelineId] ); const table = useReactTable({ data: filteredExecutions, columns, state: { sorting, columnFilters, globalFilter, }, onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, onGlobalFilterChange: setGlobalFilter, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel(), }); return (
{/* Filters */}
{['all', 'completed', 'running', 'failed', 'pending'].map((status) => ( ))}
{onRefresh && ( )}
{/* Table */}
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( ))} ))} {table.getRowModel().rows.length === 0 ? ( ) : ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( ))} )) )}
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
No executions found
{flexRender(cell.column.columnDef.cell, cell.getContext())}
{/* Pagination */}
Showing {table.getRowModel().rows.length} of {filteredExecutions.length} executions
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
{/* Debug Modal */} {debugExecution && (

Execution Failed

{debugExecution.id}

{/* Error Message */}

Error Message

                    {debugExecution.error_message || 'No error message captured'}
                  
{/* Stage Metrics */} {debugExecution.stage_metrics && (

Stage Metrics

{Object.entries(debugExecution.stage_metrics).map(([stage, metrics]) => (
{stage}
{metrics.duration_ms}ms in: {metrics.records_in} out: {metrics.records_out} {metrics.success ? '✓' : '✗'}
))}
)} {/* Execution Details */}

Execution Details

Started: {formatDate(debugExecution.started_at)}

Duration: {formatDuration(debugExecution.total_duration_ms)}

Stages Completed: {debugExecution.stages_completed?.join(' → ') || 'None'}

View Full Details →
)}
); }