(() => { const ws = new WebSocket(`ws://${location.host}/ws`); const clientTabs = document.getElementById("client-tabs"); const clientPanels = document.getElementById("client-panels"); const cardTemplate = document.getElementById("client-card-template"); const tabTemplate = document.getElementById("client-tab-template"); const addClientBtn = document.getElementById("add-client"); const leftTabServer = document.getElementById("left-tab-server"); const leftTabSystem = document.getElementById("left-tab-system"); const leftPaneServer = document.getElementById("left-pane-server"); const leftPaneSystem = document.getElementById("left-pane-system"); const clients = new Map(); const closedTabs = new Set(); let activeClient = null; function append(target, text, stream, severity) { const pane = document.getElementById(target); if (!pane) return; const line = document.createElement("div"); line.className = [stream || "stdout", severity].filter(Boolean).join(" "); line.textContent = text; pane.appendChild(line); pane.scrollTop = pane.scrollHeight; } function appendStatus(text) { append("system-log", text, "status"); } function setLeftTab(name) { const serverActive = name === "server"; leftTabServer.classList.toggle("active", serverActive); leftTabSystem.classList.toggle("active", !serverActive); leftPaneServer.hidden = !serverActive; leftPaneSystem.hidden = serverActive; } function parseClientId(process) { if (!process.startsWith("client-")) return null; const raw = process.slice("client-".length); if (!/^\d+$/.test(raw)) return null; return Number(raw); } async function closeClient(process) { const id = parseClientId(process); if (id == null) return; await fetch(`/api/clients/${id}`, { method: "DELETE" }); removeClient(process); closedTabs.add(process); } function setActiveClient(process) { activeClient = process; for (const [name, meta] of clients.entries()) { const active = name === process; meta.tab.classList.toggle("active", active); meta.panel.classList.toggle("active", active); meta.panel.hidden = !active; } } function removeClient(process) { const meta = clients.get(process); if (!meta) return; meta.tab.remove(); meta.panel.remove(); clients.delete(process); if (activeClient === process) { const next = clients.keys().next(); if (!next.done) setActiveClient(next.value); else activeClient = null; } } function closeTab(process) { const meta = clients.get(process); if (!meta) return; meta.tab.remove(); meta.panel.remove(); clients.delete(process); closedTabs.add(process); if (activeClient === process) { const next = clients.keys().next(); if (!next.done) setActiveClient(next.value); else activeClient = null; } } function ensureClient(process) { if (closedTabs.has(process)) return; if (clients.has(process)) return; const tabClone = tabTemplate.content.cloneNode(true); const tab = tabClone.querySelector(".client-tab"); const tabSelect = tabClone.querySelector(".client-tab-select"); tabSelect.textContent = process; const cardClone = cardTemplate.content.cloneNode(true); const panel = cardClone.querySelector(".client-card"); const name = cardClone.querySelector(".client-name"); const stdout = cardClone.querySelector(".client-stdout"); const stderr = cardClone.querySelector(".client-stderr"); const closeTabBtn = cardClone.querySelector(".btn-close-tab"); const killBtn = cardClone.querySelector(".btn-kill"); name.textContent = process; stdout.id = `${process}-stdout`; stderr.id = `${process}-stderr`; panel.hidden = true; tabSelect.addEventListener("click", () => setActiveClient(process)); closeTabBtn.addEventListener("click", async () => closeClient(process)); killBtn.addEventListener("click", async () => closeClient(process)); clientTabs.appendChild(tabClone); clientPanels.appendChild(cardClone); clients.set(process, { tab, panel }); if (!activeClient) { setActiveClient(process); } } function appendToProcess(process, stream, text) { const safeStream = stream === "stderr" ? "stderr" : "stdout"; let severity = ""; if (safeStream === "stderr") { const lower = text.toLowerCase(); if (lower.startsWith("[") && lower.includes("] ")) { const idx = lower.indexOf("] "); const raw = lower.slice(idx + 2).trimStart(); if (raw.startsWith("error")) severity = "stderr-error"; else if (raw.startsWith("warning")) severity = "stderr-warning"; else severity = "stderr-default"; } else if (lower.startsWith("error")) { severity = "stderr-error"; } else if (lower.startsWith("warning")) { severity = "stderr-warning"; } else { severity = "stderr-default"; } } append(`${process}-${safeStream}`, text, stream, severity); } addClientBtn.addEventListener("click", async () => { const res = await fetch("/api/clients", { method: "POST" }); if (!res.ok) return; const payload = await res.json(); if (payload && payload.name) { closedTabs.delete(payload.name); ensureClient(payload.name); setActiveClient(payload.name); } }); leftTabServer.addEventListener("click", () => setLeftTab("server")); leftTabSystem.addEventListener("click", () => setLeftTab("system")); setLeftTab("server"); ws.onopen = () => { appendStatus("[orchestrator/status] websocket connected"); }; ws.onmessage = (event) => { const msg = JSON.parse(event.data); const line = `[${msg.process}/${msg.stream}] ${msg.line}`; if (msg.process === "server") { appendToProcess("server", msg.stream, line); return; } if (msg.process.startsWith("client-")) { closedTabs.delete(msg.process); ensureClient(msg.process); appendToProcess(msg.process, msg.stream, line); return; } appendStatus(line); }; ws.onclose = () => { appendStatus("[orchestrator/status] websocket disconnected"); }; })();