diff --git a/web/components/JobsView.tsx b/web/components/JobsView.tsx index 37d3089..7241c06 100644 --- a/web/components/JobsView.tsx +++ b/web/components/JobsView.tsx @@ -211,6 +211,110 @@ export default function JobsView({ jobs, onSelectJob, isLoadingJob, onRefresh }: } }; + // Generate crash report for a job + const [copyingCrashReport, setCopyingCrashReport] = useState(null); + + const generateCrashReport = async (job: JobStatus): Promise => { + // Fetch logs for the job + let logs: LogEntry[] = []; + let logCount = 0; + try { + const response = await fetch(`/api/jobs/${job.job_id}/logs`); + if (response.ok) { + const data = await response.json(); + logs = data.logs || []; + logCount = data.log_count || 0; + } + } catch (err) { + console.error('Failed to fetch logs for crash report:', err); + } + + const businessName = extractBusinessName(job); + const now = new Date().toISOString(); + + // Format logs (last 50 entries) + const recentLogs = [...logs] + .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()) + .slice(0, 50) + .reverse(); + + const logsFormatted = recentLogs.length > 0 + ? recentLogs.map(log => + `[${new Date(log.timestamp).toISOString()}] [${log.level.toUpperCase()}] [${log.source}] ${log.message}` + ).join('\n') + : 'No logs available'; + + // Calculate duration + const duration = job.scrape_time + ? formatDuration(job.scrape_time) + : job.started_at + ? formatDuration((Date.now() - new Date(job.started_at).getTime()) / 1000) + : 'Unknown'; + + // Build the crash report + const report = `## Crash Report: ${job.job_id} + +**Generated**: ${now} +**Status**: ${job.status.toUpperCase()} +**Job Type**: google-reviews + +### Business Info +- **Name**: ${businessName} +- **Address**: ${job.business_address || 'N/A'} +- **Category**: ${job.business_category || 'N/A'} +- **URL**: ${job.url} + +### Job Timeline +- **Created**: ${job.created_at} +- **Started**: ${job.started_at || 'N/A'} +- **Completed**: ${job.completed_at || 'N/A'} +- **Last Update**: ${job.updated_at || 'N/A'} +- **Duration**: ${duration} + +### Progress at Failure +- **Reviews Collected**: ${job.reviews_count ?? 0}${job.total_reviews ? ` / ${job.total_reviews}` : ''} +- **Expected Total**: ${job.total_reviews_snapshot ?? 'Unknown'} +- **Rating Snapshot**: ${job.rating_snapshot ?? 'N/A'} + +### Error +\`\`\` +${job.error_message || 'No error message captured'} +\`\`\` + +### Logs (${logCount} total, showing last ${recentLogs.length}) +\`\`\` +${logsFormatted} +\`\`\` + +### Context for Debugging +- This is a Google Reviews scraper job +- The scraper uses Playwright to navigate Google Maps +- Reviews are extracted by scrolling through the reviews panel +- Common failure points: rate limiting, DOM structure changes, network timeouts + +### Suggested Investigation +1. Check if error is related to rate limiting (look for 429 or "too many requests") +2. Check if DOM selectors have changed (look for "element not found" errors) +3. Check network/timeout issues (look for "timeout" or "navigation" errors) +4. Review the last few log entries before the error for context +`; + + return report; + }; + + const copyCrashReport = async (job: JobStatus) => { + setCopyingCrashReport(job.job_id); + try { + const report = await generateCrashReport(job); + await navigator.clipboard.writeText(report); + // Brief visual feedback + setTimeout(() => setCopyingCrashReport(null), 1500); + } catch (err) { + console.error('Failed to copy crash report:', err); + setCopyingCrashReport(null); + } + }; + // Live monitoring functions const startMonitoring = useCallback((job: JobStatus) => { setMonitoredJob(job); @@ -887,6 +991,30 @@ export default function JobsView({ jobs, onSelectJob, isLoadingJob, onRefresh }: )} + {/* Copy Crash Report - for failed or partial jobs */} + {(job.status === 'failed' || job.status === 'partial') && ( + + )} + {/* Delete Job - allow for non-running or stuck jobs */} {(() => { const isStuck = job.status === 'running' && @@ -914,7 +1042,7 @@ export default function JobsView({ jobs, onSelectJob, isLoadingJob, onRefresh }: }, }, ], - [isLoadingJob, loadingLogs, expandedErrors, jobsWithUpdates, startMonitoring, isDeleting] + [isLoadingJob, loadingLogs, expandedErrors, jobsWithUpdates, startMonitoring, isDeleting, copyingCrashReport, copyCrashReport] ); const table = useReactTable({