feat(admin): Arc Reactor SETUP button — live log popup

- Replace arcSetup() confirm/toast with openModal() live popup
- Add arc_setup_log PHP backend (polls /var/log/jarvis/arc-setup.log)
- Color-coded line output: green=ok/done/active, red=error/fail, yellow=warn
- Auto-detects completion keywords and updates status header
- CLOSE button stops polling and refreshes worker table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014p87VFec84hNaf2WpvmLrW
This commit is contained in:
2026-07-01 22:30:39 -05:00
parent d6a1fc9456
commit 46dabe3c31
+66 -10
View File
@@ -589,6 +589,14 @@ if ($action) {
} }
j(['lines'=>$out, 'next_line'=>$total]); j(['lines'=>$out, 'next_line'=>$total]);
case 'arc_setup_log':
$logFile = '/var/log/jarvis/arc-setup.log';
$since = max(0, (int)($_GET['since'] ?? 0));
if (!file_exists($logFile)) { j(['lines'=>[],'next_line'=>0]); }
$allLines = file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$total = count($allLines);
if (!empty($_GET['snapshot'])) { j(['next_line'=>$total]); }
j(['lines'=>array_values(array_slice($allLines, $since)), 'next_line'=>$total]);
case 'arc_status': case 'arc_status':
$ch = curl_init('http://127.0.0.1:7474/status'); $ch = curl_init('http://127.0.0.1:7474/status');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5, CURLOPT_CONNECTTIMEOUT=>3]); curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5, CURLOPT_CONNECTTIMEOUT=>3]);
@@ -2327,18 +2335,66 @@ async function runIntentGenerator() {
} }
async function arcSetup() { async function arcSetup() {
if (!confirm('Run Arc Reactor setup?\n\nThis will:\n• Copy reactor.py from deploy/\n• Install Python packages\n• Install/update systemd service\n• Restart jarvis-arc')) return; const modalHtml = `
const btn = event.currentTarget; <div style="font-family:var(--mono);font-size:0.72rem;line-height:1.8">
btn.disabled = true; btn.textContent = '⏳ SETTING UP...'; <div id="as-status" style="color:var(--cyan);margin-bottom:8px;font-size:0.65rem;letter-spacing:1px">LAUNCHING...</div>
try { <div id="as-log" style="background:#060a0e;border:1px solid var(--border);border-radius:3px;padding:10px 12px;min-height:160px;max-height:360px;overflow-y:auto;white-space:pre-wrap;font-size:0.62rem;line-height:1.7;color:var(--text-dim)">Waiting for output...</div>
const d = await api('worker', {type:'daemon', id:'arc_reactor', action:'setup'}); </div>`;
toast(d.msg || (d.ok ? 'Setup started' : 'Setup failed'), d.ok ? 'ok' : 'err');
setTimeout(() => { loadWorkers(); btn.disabled=false; btn.innerHTML='&#9881; SETUP'; }, 5000); openModal("&#9881; ARC REACTOR SETUP", modalHtml, null, null);
} catch(e) { document.getElementById("modalSave").textContent = "CLOSE";
toast('Setup failed: ' + e.message, 'err'); let _stop = false;
btn.disabled=false; btn.innerHTML='&#9881; SETUP'; document.getElementById("modalSave").onclick = () => { _stop = true; closeModal(); loadWorkers(); };
document.getElementById("modalClose").onclick = () => { _stop = true; closeModal(); loadWorkers(); };
const logEl = document.getElementById("as-log");
const statEl = document.getElementById("as-status");
const snap = await api("arc_setup_log", {snapshot:1});
let lastLine = snap?.next_line ?? 0;
const kick = await api("worker", {type:"daemon", id:"arc_reactor", action:"setup"});
if (!kick?.ok) {
statEl.style.color = "var(--red)";
statEl.textContent = "LAUNCH FAILED: " + (kick?.msg || kick?.error || "unknown");
return;
} }
statEl.textContent = "RUNNING — polling every 2s...";
async function pollLog() {
if (_stop) return;
try {
const res = await api("arc_setup_log", {since: lastLine});
if (res?.lines?.length) {
lastLine = res.next_line;
if (logEl.textContent === "Waiting for output...") logEl.textContent = "";
res.lines.forEach(line => {
const d = document.createElement("div");
const l = line.toLowerCase();
if (l.includes("error") || l.includes("fail")) d.style.color = "var(--red)";
else if (l.includes("warn") || l.includes("skip")) d.style.color = "var(--yellow)";
else if (l.includes("ok") || l.includes("done") || l.includes("active") ||
l.includes("success") || l.includes("restart") || l.includes("enabled")) d.style.color = "var(--green)";
else d.style.color = "var(--text-dim)";
d.textContent = line;
logEl.appendChild(d);
});
logEl.scrollTop = logEl.scrollHeight;
const joined = res.lines.join(" ").toLowerCase();
if (joined.includes("systemctl restart") || joined.includes("active") ||
joined.includes("done") || joined.includes("failed")) {
statEl.style.color = (joined.includes("error") || joined.includes("fail")) ? "var(--red)" : "var(--green)";
statEl.textContent = (joined.includes("error") || joined.includes("fail")) ? "SETUP FAILED" : "SETUP COMPLETE";
document.getElementById("modalSave").textContent = "CLOSE";
loadWorkers(); return;
}
}
} catch(e) {}
setTimeout(pollLog, 2000);
}
setTimeout(pollLog, 1500);
} }
async function workerAction(type,id,action) { async function workerAction(type,id,action) {
if (type === 'agent' && action === 'update') { if (type === 'agent' && action === 'update') {
await agentUpdateFlow(id); await agentUpdateFlow(id);