import { useEffect, useRef, useState } from "react"; import { CheckCircle2, Loader2, XCircle } from "lucide-react"; import { Button } from "@/components/ui"; import { useAuthStore } from "@/store/auth"; const MAX_LINES = 2000; type Phase = "running" | "success" | "failed" | "error"; /** * Modal that runs `compose up -d` over the `/ws/deploy/{id}` WebSocket and * streams its output (image pulls, container creation) live, so the user sees * deploy progress instead of a blind spinner. The compose subprocess keeps * running on the server even if this modal is closed early. */ export function DeployConsole({ stackId, agentId, onClose, }: { stackId: string; agentId?: number; onClose: () => void; }) { const [lines, setLines] = useState([]); const [phase, setPhase] = useState("running"); const [errorDetail, setErrorDetail] = useState(null); const boxRef = useRef(null); const token = useAuthStore((s) => s.accessToken); useEffect(() => { if (!token) return; const proto = window.location.protocol === "https:" ? "wss" : "ws"; const path = agentId != null ? `/ws/agent-deploy/${agentId}/${stackId}` : `/ws/deploy/${stackId}`; const url = `${proto}://${window.location.host}${path}?token=${token}`; const ws = new WebSocket(url); ws.onmessage = (ev) => { try { const msg = JSON.parse(ev.data); if (msg.type === "log") { setLines((prev) => { const next = [...prev, msg.line as string]; return next.length > MAX_LINES ? next.slice(-MAX_LINES) : next; }); } else if (msg.type === "done") { setPhase(msg.returncode === 0 ? "success" : "failed"); } else if (msg.type === "error") { setPhase("error"); setErrorDetail(msg.detail || "Deploy error"); } } catch { /* ignore */ } }; ws.onclose = () => { // If the socket dropped before a done/error frame, surface it. setPhase((p) => (p === "running" ? "error" : p)); }; return () => ws.close(); }, [stackId, agentId, token]); useEffect(() => { if (boxRef.current) boxRef.current.scrollTop = boxRef.current.scrollHeight; }, [lines]); const done = phase !== "running"; return (
{phase === "running" && ( )} {phase === "success" && } {(phase === "failed" || phase === "error") && ( )}

{phase === "running" && `Deploying ${stackId}…`} {phase === "success" && `Deployed ${stackId} ✓`} {phase === "failed" && `Deploy of ${stackId} failed`} {phase === "error" && `Deploy of ${stackId} errored`}

{lines.length === 0 && phase === "running" ? ( Starting compose up… ) : ( lines.map((l, i) => (
{l}
)) )} {errorDetail &&
{errorDetail}
}
); }