diff --git a/scripts/render-voidbot-swarm-dashboard.mjs b/scripts/render-voidbot-swarm-dashboard.mjs index 96ab072..7d175f3 100644 --- a/scripts/render-voidbot-swarm-dashboard.mjs +++ b/scripts/render-voidbot-swarm-dashboard.mjs @@ -1353,7 +1353,12 @@ function renderHtml(snapshot) { function renderControls(snapshot, participants) { const controls = snapshot.controls || {}; const latest = (controls.manualTurnRequests || [])[0]; + const paused = snapshot.summary?.paused === true; + const pauseReason = snapshot.summary?.pauseReason + ? "
" + esc(snapshot.summary.pauseReason) + "
" + : ""; return "

Controls

" + + "
Swarm brake
" + badge(paused ? "paused" : "running", paused ? "Paused" : "Unpaused") + "
" + pauseReason + "
" + "
Heartbeat cadence
x" + esc(number(controls.cadenceMultiplier || 1)) + "
" + "
Manual next turn
" + "
Latest request: " + esc(latest?.identityId || "none") + " " + esc(latest?.status || "") + "
" + @@ -1630,7 +1635,18 @@ function renderHtml(snapshot) { const cadenceApply = document.getElementById("cadence-apply"); const forceSelect = document.getElementById("force-identity"); const forceButton = document.getElementById("force-turn"); + const pauseButton = document.getElementById("swarm-pause-toggle"); if (forceSelect && selectedIdentity) forceSelect.value = selectedIdentity; + if (pauseButton) { + pauseButton.addEventListener("click", async () => { + const currentlyPaused = pauseButton.getAttribute("data-paused") === "true"; + await postJson("/api/swarm-pause", { + paused: !currentlyPaused, + reason: currentlyPaused ? "Unpaused from swarm dashboard." : "Paused from swarm dashboard.", + }); + await refresh(); + }); + } if (cadenceInput && cadenceValue) { cadenceInput.addEventListener("input", () => cadenceValue.textContent = "x" + Number(cadenceInput.value).toLocaleString(undefined, { maximumFractionDigits: 2 })); } @@ -1762,6 +1778,10 @@ async function serveDashboard({ host, port, rootDir, refreshSeconds }) { await handleForceTurn(request, response); return; } + if (request.method === "POST" && requestUrl.pathname === "/api/swarm-pause") { + await handleSwarmPause(request, response); + return; + } const pathname = requestUrl.pathname === "/" ? "/swarm-dashboard.html" : decodeURIComponent(requestUrl.pathname); const target = resolve(rootDir, `.${pathname}`); if (!target.startsWith(rootDir)) { @@ -1840,6 +1860,31 @@ async function handleForceTurn(request, response) { writeJsonResponse(response, 200, { ok: true, controls: state.controls }); } +async function handleSwarmPause(request, response) { + if (args.public) { + writeJsonResponse(response, 403, { ok: false, error: "Controls are disabled in public mode." }); + return; + } + const body = await readRequestJson(request); + if (typeof body.paused !== "boolean") { + writeJsonResponse(response, 400, { ok: false, error: "paused must be true or false." }); + return; + } + const payload = { + paused: body.paused, + reason: typeof body.reason === "string" && body.reason.trim() + ? body.reason.trim().slice(0, 280) + : body.paused + ? "Paused from swarm dashboard." + : "Unpaused from swarm dashboard.", + updatedAt: new Date().toISOString(), + }; + await mkdir(dirname(pausePath), { recursive: true }); + await writeFile(pausePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8"); + await render(); + writeJsonResponse(response, 200, { ok: true, pause: payload }); +} + async function readMutableHeartbeatState() { const parsed = await readJsonFile(heartbeatStatePath); if (!parsed.ok || !parsed.value || typeof parsed.value !== "object") {