crasa-transport/webtest/static/app.js

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");
};
})();