189 lines
6.1 KiB
JavaScript
189 lines
6.1 KiB
JavaScript
(() => {
|
|
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");
|
|
};
|
|
})();
|