import React, { useEffect, useState } from 'react'; import { api } from '@renderer/api'; import { Button } from '@renderer/components/ui/button'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from '@renderer/components/ui/dialog'; import { useStore } from '@renderer/store'; import { useShallow } from 'zustand/react/shallow'; import { AlertTriangle, Clock, Loader2, Terminal } from 'lucide-react'; import { CliLogsRichView } from '../CliLogsRichView'; import { RunStatusBadge } from './ScheduleStatusBadge'; import type { ScheduleRun } from '@shared/types'; // ============================================================================= // Props // ============================================================================= interface ScheduleRunLogDialogProps { open: boolean; /** Initial run snapshot — used to identify the run; live data comes from store */ run: ScheduleRun | null; scheduleId: string; onClose: () => void; } // ============================================================================= // Helpers // ============================================================================= function formatTime(isoString: string): string { try { return new Date(isoString).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }); } catch { return isoString; } } function formatDuration(ms: number): string { if (ms < 1000) return `${ms}ms`; const seconds = Math.floor(ms / 1000); if (seconds < 60) return `${seconds}s`; const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; if (minutes < 60) return `${minutes}m ${remainingSeconds}s`; const hours = Math.floor(minutes / 60); const remainingMinutes = minutes % 60; return `${hours}h ${remainingMinutes}m`; } // ============================================================================= // Component // ============================================================================= export const ScheduleRunLogDialog = ({ open, run: initialRun, scheduleId, onClose, }: ScheduleRunLogDialogProps): React.JSX.Element => { // Read live run data from store — falls back to initial prop if not found const liveRun = useStore( useShallow((s) => { if (!initialRun) return null; const runs = s.scheduleRuns[scheduleId] ?? []; return runs.find((r) => r.id === initialRun.id) ?? initialRun; }) ); const run = liveRun ?? initialRun; const [logs, setLogs] = useState<{ stdout: string; stderr: string } | null>(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const runStatus = run?.status; const runId = run?.id; useEffect(() => { if (!open || !run) { setLogs(null); setError(null); return; } // Only fetch logs for completed/failed runs (not running/pending) const hasLogs = runStatus === 'completed' || runStatus === 'failed' || runStatus === 'failed_interrupted'; if (!hasLogs) return; let cancelled = false; setLoading(true); setError(null); void (async () => { try { const result = await api.schedules.getRunLogs(scheduleId, runId!); if (!cancelled) { setLogs(result); } } catch (err) { if (!cancelled) { setError(err instanceof Error ? err.message : 'Failed to load logs'); } } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [open, runId, runStatus, scheduleId]); if (!run) return <>; const isRunning = run.status === 'running' || run.status === 'warming_up' || run.status === 'warm' || run.status === 'pending'; const hasStderr = logs?.stderr && logs.stderr.trim().length > 0; return ( { if (!nextOpen) onClose(); }} > Run Log {/* Metadata */}
{formatTime(run.startedAt)} {run.durationMs != null ? ( {formatDuration(run.durationMs)} ) : null} {run.exitCode != null ? ( exit {run.exitCode} ) : null} {run.retryCount > 0 ? ( retry {run.retryCount}/2 ) : null}
{/* Content */}
{/* Running state */} {isRunning ? (
Task is still running... {run.summary ? ( {run.summary} ) : null}
) : null} {/* Loading */} {loading ? (
Loading logs...
) : null} {/* Error loading logs */} {error ? (
{error}
) : null} {/* Stdout — rich stream-json view (falls back to plain text for old logs) */} {logs ? ( <> {/* Stderr */} {hasStderr ? (
Errors
                    {logs.stderr}
                  
) : null} ) : null} {/* Run error message (from ScheduleRun.error) */} {!isRunning && run.error && !logs ? (
{run.error}
) : null}
); };