"use strict"; const $ = (sel) => document.querySelector(sel); const BADGES = { done: "done", failed: "failed", cancelled: "cancelled", queued: "queued", dispatching: "dispatching", building: "building" }; function esc(s) { return String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]) ); } async function api(path, opts) { const resp = await fetch(path, opts); if (!resp.ok) { let msg = resp.statusText; try { msg = (await resp.json()).error || msg; } catch (_) {} throw new Error(msg); } return resp.status === 204 ? null : resp.json(); } async function refreshCapabilities() { try { const caps = await api("/api/capabilities"); const os = new Set(caps.map((c) => c.os).filter(Boolean)); const cpu = new Set(caps.map((c) => c.cpu).filter(Boolean)); fillSelect($("#os"), os); fillSelect($("#cpu"), cpu); } catch (e) { $("#submit-msg").textContent = `Could not reach controller API: ${e.message}`; $("#submit-msg").className = "msg error"; } } function fillSelect(sel, values) { const current = sel.value; sel.innerHTML = ``; [...values].sort().forEach((v) => { const opt = document.createElement("option"); opt.value = v; opt.textContent = v; sel.appendChild(opt); }); if (current) sel.value = current; } async function refreshWorkers() { try { const workers = await api("/api/workers"); const online = workers.filter((w) => w.online).length; const parts = workers.map((w) => `${w.host || w.url}${w.online ? "" : " (down)"}` ); $("#worker-summary").textContent = `${online}/${workers.length} workers online — ${parts.join(" · ")}`; } catch (_) { $("#worker-summary").textContent = "Worker summary unavailable"; } } async function refreshJobs() { let jobs; try { jobs = await api("/api/jobs"); } catch (e) { $("#jobs").innerHTML = `

Error: ${esc(e.message)}

`; return; } const box = $("#jobs"); if (!jobs.length) { box.innerHTML = `

No jobs yet.

`; return; } let html = ``; for (const j of jobs) { html += ``; } html += `
IDStatusSourceTargetCreated
${esc(j.id)} ${esc(j.status)} ${esc(j.source)} ${esc(j.os_req || "")} ${esc(j.cpu_req || "")} ${esc(shortTime(j.created_at))}
`; box.innerHTML = html; } function shortTime(iso) { if (!iso) return ""; const d = new Date(iso); return isNaN(d) ? iso : d.toLocaleString(); } function badgeFor(status) { return `${esc(status)}`; } function openJob(id) { const modal = $("#modal"); modal.hidden = false; $("#modal-title").textContent = `Job ${id}`; $("#log").textContent = ""; $("#artifacts").innerHTML = ""; let src = new EventSource(`/api/jobs/${encodeURIComponent(id)}/stream`); src.addEventListener("job", (e) => { try { const data = JSON.parse(e.data); if (data.status) $("#status-badge").innerHTML = badgeFor(data.status); } catch (_) {} }); src.addEventListener("log", (e) => { let line; try { line = JSON.parse(e.data); } catch (_) { line = e.data; } $("#log").textContent += line + String.fromCharCode(10); $("#log").scrollTop = $("#log").scrollHeight; }); src.onerror = () => src.close(); src.onopen = () => { // seed meta + artifacts from REST api(`/api/jobs/${encodeURIComponent(id)}`).then((j) => { $("#modal-meta").textContent = `${j.source} — ${j.os_req || "any"} / ${j.cpu_req || "any"} → ${j.worker_host || "unassigned"}`; $("#status-badge").innerHTML = badgeFor(j.status); const arts = j.artifacts || []; if (arts.length) { $("#artifacts").innerHTML = arts.map((a) => `⬇ ${esc(a)}` ).join(""); } }).catch(() => {}); }; } function closeModal() { $("#modal").hidden = true; } $("#jobs").addEventListener("click", (e) => { const tr = e.target.closest("tr[data-id]"); if (tr) openJob(tr.dataset.id); }); $("#modal-close").addEventListener("click", closeModal); $("#modal").addEventListener("click", (e) => { if (e.target === $("#modal")) closeModal(); }); document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeModal(); }); $("#mode").addEventListener("change", () => { const git = $("#mode").value === "git"; $("#url-field").hidden = !git; $("#upload-field").hidden = git; }); $("#job-form").addEventListener("submit", async (e) => { e.preventDefault(); const msg = $("#submit-msg"); msg.className = "msg"; msg.textContent = "Submitting…"; const git = $("#mode").value === "git"; const body = new FormData(); body.append("os", $("#os").value); body.append("cpu", $("#cpu").value); if (git) { body.append("repo_url", $("#url").value.trim()); } else { const file = $("#file").files[0]; if (!file) { msg.textContent = "Choose a zip file to upload."; msg.className = "msg error"; return; } body.append("file", file); } try { const res = await fetch("/api/jobs", { method: "POST", body }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || res.statusText); msg.textContent = `Submitted job ${data.id}`; $("#job-form").reset(); refreshJobs(); } catch (err) { msg.textContent = `Error: ${err.message}`; msg.className = "msg error"; } }); refreshCapabilities(); refreshWorkers(); refreshJobs(); setInterval(refreshWorkers, 15000); setInterval(refreshJobs, 5000);