"use client"; import { useState, useEffect, useRef, useCallback } from "react"; import { Loader2, CheckCircle2, XCircle, AlertTriangle, Minimize2, Maximize2 } from "lucide-react"; import { Modal } from "@/app/_components/GlobalComponents/UIElements/Modal"; import { Button } from "@/app/_components/GlobalComponents/UIElements/Button"; import { useSSEContext } from "@/app/_contexts/SSEContext"; import { SSEEvent } from "@/app/_utils/sse-events"; import { usePageVisibility } from "@/app/_hooks/usePageVisibility"; import { useTranslations } from "next-intl"; interface LiveLogModalProps { isOpen: boolean; onClose: () => void; runId: string; jobId: string; jobComment?: string; } const MAX_LINES_FULL_RENDER = 10000; const TAIL_LINES = 5000; export const LiveLogModal = ({ isOpen, onClose, runId, jobId, jobComment, }: LiveLogModalProps) => { const t = useTranslations(); const [logContent, setLogContent] = useState(""); const [status, setStatus] = useState<"running" | "completed" | "failed">( "running" ); const [exitCode, setExitCode] = useState(null); const [tailMode, setTailMode] = useState(false); const [showSizeWarning, setShowSizeWarning] = useState(false); const logEndRef = useRef(null); const { subscribe } = useSSEContext(); const isPageVisible = usePageVisibility(); const lastOffsetRef = useRef(0); const abortControllerRef = useRef(null); const [fileSize, setFileSize] = useState(0); const [lineCount, setLineCount] = useState(0); const [maxLines, setMaxLines] = useState(500); const [totalLines, setTotalLines] = useState(0); const [truncated, setTruncated] = useState(false); const [showFullLog, setShowFullLog] = useState(false); const [isJobComplete, setIsJobComplete] = useState(false); useEffect(() => { if (isOpen) { lastOffsetRef.current = 0; setLogContent(""); setTailMode(false); setShowSizeWarning(false); setFileSize(0); setLineCount(0); setShowFullLog(false); setIsJobComplete(false); } }, [isOpen, runId]); useEffect(() => { if (isOpen && runId && !isJobComplete) { lastOffsetRef.current = 0; setLogContent(""); fetchLogs(); } }, [maxLines]); const fetchLogs = useCallback(async () => { if (abortControllerRef.current) { abortControllerRef.current.abort(); } const abortController = new AbortController(); abortControllerRef.current = abortController; try { const url = `/api/logs/stream?runId=${runId}&offset=${lastOffsetRef.current}&maxLines=${maxLines}`; const response = await fetch(url, { signal: abortController.signal, }); const data = await response.json(); if (data.fileSize !== undefined) { lastOffsetRef.current = data.fileSize; setFileSize(data.fileSize); if (data.fileSize > 10 * 1024 * 1024) { setShowSizeWarning(true); } } if (data.totalLines !== undefined) { setTotalLines(data.totalLines); } setLineCount(data.displayedLines || 0); if (data.truncated !== undefined) { setTruncated(data.truncated); } if (lastOffsetRef.current === 0 && data.content) { setLogContent(data.content); if (data.truncated) { setTailMode(true); } } else if (data.newContent) { setLogContent((prev) => { const combined = prev + data.newContent; const lines = combined.split("\n"); if (lines.length > maxLines) { return lines.slice(-maxLines).join("\n"); } return combined; }); } const jobStatus = data.status || "running"; setStatus(jobStatus); if (jobStatus === "completed" || jobStatus === "failed") { setIsJobComplete(true); } if (data.exitCode !== undefined) { setExitCode(data.exitCode); } } catch (error: any) { if (error.name !== "AbortError") { console.error("Failed to fetch logs:", error); } } }, [runId, maxLines]); useEffect(() => { if (!isOpen || !runId || !isPageVisible) return; fetchLogs(); let interval: NodeJS.Timeout | null = null; if (isPageVisible && !isJobComplete) { interval = setInterval(fetchLogs, 3000); } return () => { if (interval) { clearInterval(interval); } if (abortControllerRef.current) { abortControllerRef.current.abort(); } }; }, [isOpen, runId, isPageVisible, fetchLogs, isJobComplete]); useEffect(() => { if (!isOpen) return; const unsubscribe = subscribe((event: SSEEvent) => { if (event.type === "job-completed" && event.data.runId === runId) { setStatus("completed"); setExitCode(event.data.exitCode); fetch(`/api/logs/stream?runId=${runId}&offset=0`) .then((res) => res.json()) .then((data) => { if (data.content) { const lines = data.content.split("\n"); setLineCount(lines.length); if (tailMode && lines.length > TAIL_LINES) { setLogContent(lines.slice(-TAIL_LINES).join("\n")); } else { setLogContent(data.content); } } }); } else if (event.type === "job-failed" && event.data.runId === runId) { setStatus("failed"); setExitCode(event.data.exitCode); fetch(`/api/logs/stream?runId=${runId}&offset=0`) .then((res) => res.json()) .then((data) => { if (data.content) { const lines = data.content.split("\n"); setLineCount(lines.length); if (tailMode && lines.length > TAIL_LINES) { setLogContent(lines.slice(-TAIL_LINES).join("\n")); } else { setLogContent(data.content); } } }); } }); return unsubscribe; }, [isOpen, runId, subscribe, tailMode]); useEffect(() => { if (logEndRef.current) { logEndRef.current.scrollIntoView({ behavior: "instant" }); } }, [logContent]); const toggleTailMode = () => { setTailMode(!tailMode); if (!tailMode) { const lines = logContent.split("\n"); if (lines.length > TAIL_LINES) { setLogContent(lines.slice(-TAIL_LINES).join("\n")); } } }; const formatFileSize = (bytes: number): string => { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }; const titleWithStatus = (
{t("cronjobs.liveJobExecution")}{jobComment && `: ${jobComment}`} {status === "running" && ( {t("cronjobs.running")} )} {status === "completed" && ( {t("cronjobs.completed", { exitCode: exitCode ?? 0 })} )} {status === "failed" && ( {t("cronjobs.jobFailed", { exitCode: exitCode ?? 1 })} )}
); return (
{!showFullLog ? ( <> {truncated && ( )} ) : (
{totalLines > 0 ? t("cronjobs.viewingFullLog", { totalLines: totalLines.toLocaleString() }) : t("cronjobs.viewingFullLogNoCount")}
)}
{truncated && !showFullLog && (
{t("cronjobs.showingLastOf", { lineCount: lineCount.toLocaleString(), totalLines: totalLines.toLocaleString() })}
)}
{showSizeWarning && (

{t("cronjobs.largeLogFileDetected")} ({formatFileSize(fileSize)}) {tailMode && ` - ${t("cronjobs.tailModeEnabled", { tailLines: TAIL_LINES.toLocaleString() })}`}

)}
            {logContent || t("cronjobs.waitingForJobToStart")}
            
{t("cronjobs.runIdJobId", { runId, jobId })}
); };