'use client'; import { useState } from 'react'; import { AlertTriangle, AlertOctagon, Zap, Clock, TrendingUp, ChevronDown, ChevronUp, RefreshCw, Wrench, Lightbulb, Activity, MemoryStick, Boxes, } from 'lucide-react'; import type { StructuredLog } from './index'; // Crash analysis types interface CrashAnalysis { pattern: string; confidence: number; description: string; suggested_fix: string; auto_fix_params?: Record; } interface MetricsHistoryEntry { timestamp_ms: number; memory_mb: number; dom_nodes: number; } export interface CrashReportData { crash_id: string; crash_type: string; error_message: string; analysis: CrashAnalysis; metrics_history?: MetricsHistoryEntry[]; logs_before_crash?: StructuredLog[]; created_at: string; } export interface CrashReportProps { jobId: string; crashReport: CrashReportData; onRetry?: (applyFix: boolean) => void; } // Pattern icons mapping const PATTERN_ICONS: Record = { memory_exhaustion: MemoryStick, dom_explosion: Boxes, timeout: Clock, network_failure: Zap, rate_limited: AlertOctagon, default: AlertTriangle, }; // Crash type severity colors const CRASH_TYPE_COLORS: Record = { memory_exhaustion: { bg: 'bg-red-900', border: 'border-red-600', text: 'text-red-200', icon: 'text-red-400' }, dom_explosion: { bg: 'bg-orange-900', border: 'border-orange-600', text: 'text-orange-200', icon: 'text-orange-400' }, timeout: { bg: 'bg-yellow-900', border: 'border-yellow-600', text: 'text-yellow-200', icon: 'text-yellow-400' }, network_failure: { bg: 'bg-purple-900', border: 'border-purple-600', text: 'text-purple-200', icon: 'text-purple-400' }, rate_limited: { bg: 'bg-blue-900', border: 'border-blue-600', text: 'text-blue-200', icon: 'text-blue-400' }, default: { bg: 'bg-red-900', border: 'border-red-600', text: 'text-red-200', icon: 'text-red-400' }, }; // Log level colors for the logs section const LEVEL_BADGE_COLORS: Record = { DEBUG: 'bg-gray-600 text-gray-200', INFO: 'bg-blue-600 text-blue-100', WARN: 'bg-yellow-600 text-yellow-100', ERROR: 'bg-red-600 text-red-100', FATAL: 'bg-purple-600 text-purple-100', }; export default function CrashReport({ jobId, crashReport, onRetry }: CrashReportProps) { const [isLogsExpanded, setIsLogsExpanded] = useState(false); const [isRetrying, setIsRetrying] = useState(false); const [retryError, setRetryError] = useState(null); const { crash_type, error_message, analysis, metrics_history, logs_before_crash, created_at } = crashReport; // Get styling for crash type const crashColors = CRASH_TYPE_COLORS[crash_type] || CRASH_TYPE_COLORS.default; const PatternIcon = PATTERN_ICONS[analysis.pattern] || PATTERN_ICONS.default; // Format timestamp const formatTimestamp = (timestamp: string) => { try { const date = new Date(timestamp); return date.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }); } catch { return timestamp; } }; const formatLogTimestamp = (timestamp: string) => { try { const date = new Date(timestamp); return date.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3, }); } catch { return timestamp; } }; // Get confidence color const getConfidenceColor = (confidence: number) => { if (confidence >= 0.8) return 'bg-green-600 text-green-100'; if (confidence >= 0.5) return 'bg-yellow-600 text-yellow-100'; return 'bg-orange-600 text-orange-100'; }; // Mini timeline visualization const renderMetricsTimeline = () => { if (!metrics_history || metrics_history.length < 2) { return (
No metrics history available
); } const memoryValues = metrics_history.map((m) => m.memory_mb); const maxMemory = Math.max(...memoryValues); const minMemory = Math.min(...memoryValues); const range = maxMemory - minMemory || 1; // Calculate points for SVG path const width = 100; const height = 40; const points = metrics_history.map((entry, index) => { const x = (index / (metrics_history.length - 1)) * width; const y = height - ((entry.memory_mb - minMemory) / range) * height; return `${x},${y}`; }); const pathD = `M ${points.join(' L ')}`; return (
Memory Usage Over Time {maxMemory.toFixed(0)}MB peak
{/* Gradient fill under the line */} {/* Fill area */} {/* Line */} {/* Crash point indicator */} {/* Crash indicator */}
CRASH
{minMemory.toFixed(0)}MB {metrics_history.length} data points
); }; // Handle retry with/without fix const handleRetry = async (applyFix: boolean) => { setIsRetrying(true); setRetryError(null); try { const response = await fetch(`/api/jobs/${jobId}/retry?apply_fix=${applyFix}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, }); if (!response.ok) { const data = await response.json().catch(() => ({})); throw new Error(data.error || data.message || `Retry failed with status ${response.status}`); } // Call the onRetry callback if provided if (onRetry) { onRetry(applyFix); } } catch (err) { setRetryError(err instanceof Error ? err.message : 'Failed to retry job'); } finally { setIsRetrying(false); } }; return (
{/* Header */}

{crash_type.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())}

{error_message}

{formatTimestamp(created_at)}
{/* Content */}
{/* Timeline to Crash */}

Timeline to Crash

{renderMetricsTimeline()}
{/* Pattern Analysis */}

Pattern Analysis

{/* Pattern name with confidence */}
{analysis.pattern.replace(/_/g, ' ')} {Math.round(analysis.confidence * 100)}% confidence
{/* Confidence bar */}
= 0.8 ? 'bg-green-500' : analysis.confidence >= 0.5 ? 'bg-yellow-500' : 'bg-orange-500' }`} style={{ width: `${analysis.confidence * 100}%` }} />
{/* Description */}

{analysis.description}

{/* Suggested fix callout */}
Suggested Fix

{analysis.suggested_fix}

{/* Auto-Fix Options */} {analysis.auto_fix_params && Object.keys(analysis.auto_fix_params).length > 0 && (

Auto-Fix Parameters

{Object.entries(analysis.auto_fix_params).map(([key, value]) => (
{key} {typeof value === 'object' ? JSON.stringify(value) : String(value)}
))}
)} {/* Retry buttons */}
{/* Retry error message */} {retryError && (
{retryError}
)} {/* Logs Before Crash */} {logs_before_crash && logs_before_crash.length > 0 && (
{isLogsExpanded && (
{logs_before_crash.slice(-20).map((log, index) => (
{formatLogTimestamp(log.timestamp)} {log.level} {log.category} {log.message}
))}
)}
)}
); }