From 7013a80428ef10eb5cc6cb38b063b49a6f083af0 Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Thu, 11 Jun 2026 04:07:28 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Phase=201=20=E2=80=94=20Arc=20Reactor?= =?UTF-8?q?=20Core=20Daemon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Python asyncio daemon (/opt/jarvis-arc/reactor.py) running on 127.0.0.1:7474 - systemd service (jarvis-arc) auto-starts with MySQL dependency - arc_jobs + arc_status MySQL tables for async job queue - api/endpoints/arc.php: PHP bridge to daemon (status, job_create, job_get, jobs, purge) - api.php: added arc route - index.html: ARC REACTOR status indicator in bottom bar with live polling - admin/index.php: ARC REACTOR nav section + full job management panel - Built-in job handlers: ping, echo, shell (whitelist-gated) - Foundation for Phase 2 (Intel Protocol) and beyond Co-Authored-By: Claude Sonnet 4.6 --- api/endpoints/arc.php | 108 ++++++++++++++++++++++++++++++++++++ public_html/admin/index.php | 89 +++++++++++++++++++++++++++++ public_html/api.php | 3 + public_html/index.html | 61 ++++++++++++++++++++ 4 files changed, 261 insertions(+) create mode 100644 api/endpoints/arc.php diff --git a/api/endpoints/arc.php b/api/endpoints/arc.php new file mode 100644 index 0000000..9afe6fc --- /dev/null +++ b/api/endpoints/arc.php @@ -0,0 +1,108 @@ + true, + CURLOPT_TIMEOUT => ARC_TIMEOUT, + CURLOPT_CONNECTTIMEOUT => 3, + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + ]); + if ($method === 'POST') { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body)); + } elseif ($method === 'DELETE') { + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); + } + $raw = curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $err = curl_error($ch); + curl_close($ch); + + if ($err || $raw === false) { + return ['error' => 'Arc Reactor unreachable: ' . $err, 'online' => false]; + } + $decoded = json_decode($raw, true); + return $decoded ?? ['error' => 'Invalid response from Arc Reactor', 'raw' => substr($raw, 0, 200)]; +} + +// ── ROUTING ─────────────────────────────────────────────────────────────────── +// arc action comes from query string or POST body (not the URL path segment) +global $data; +$action = $_GET['action'] ?? $data['action'] ?? ''; + +switch ($action) { + + // GET /api/arc?action=status + case 'status': + $result = arc_request('GET', '/status'); + if (!isset($result['online'])) $result['online'] = false; + echo json_encode($result); + break; + + // POST /api/arc — create a job + // body: { action: "job_create", type: "ping", payload: {}, priority: 5 } + case 'job_create': + $type = $data['type'] ?? ''; + $payload = $data['payload'] ?? []; + $priority = (int)($data['priority'] ?? 5); + if (!$type) { http_response_code(400); echo json_encode(['error' => 'Missing job type']); break; } + $result = arc_request('POST', '/job', [ + 'type' => $type, + 'payload' => $payload, + 'priority' => $priority, + 'created_by' => 'jarvis_ui', + ]); + echo json_encode($result); + break; + + // GET /api/arc?action=job_get&id=123 + case 'job_get': + $id = (int)($data['id'] ?? $_GET['id'] ?? 0); + if (!$id) { http_response_code(400); echo json_encode(['error' => 'Missing job id']); break; } + echo json_encode(arc_request('GET', "/job/{$id}")); + break; + + // GET /api/arc?action=jobs&status=done&limit=20 + case 'jobs': + $status = $_GET['status'] ?? $data['status'] ?? ''; + $limit = (int)($_GET['limit'] ?? $data['limit'] ?? 50); + $qs = http_build_query(array_filter(['status' => $status, 'limit' => $limit])); + echo json_encode(arc_request('GET', '/jobs' . ($qs ? "?{$qs}" : ''))); + break; + + // DELETE /api/arc?action=job_cancel&id=123 + case 'job_cancel': + $id = (int)($data['id'] ?? $_GET['id'] ?? 0); + if (!$id) { http_response_code(400); echo json_encode(['error' => 'Missing job id']); break; } + echo json_encode(arc_request('DELETE', "/job/{$id}")); + break; + + // DELETE /api/arc?action=purge + case 'purge': + echo json_encode(arc_request('DELETE', '/jobs/purge')); + break; + + // Quick ping test + case 'ping': + $result = arc_request('POST', '/job', [ + 'type' => 'ping', + 'payload' => [], + 'priority' => 9, + 'created_by' => 'jarvis_ping', + ]); + echo json_encode($result); + break; + + default: + http_response_code(404); + echo json_encode(['error' => "Unknown arc action: {$action}"]); +} diff --git a/public_html/admin/index.php b/public_html/admin/index.php index 3b13a0d..c44b353 100644 --- a/public_html/admin/index.php +++ b/public_html/admin/index.php @@ -459,6 +459,45 @@ if ($action) { $r = runSync(); j(['ok'=>true,'results'=>$r]); + + // ── ARC REACTOR ────────────────────────────────────────────────────── + case 'arc_status': + $ch = curl_init('http://127.0.0.1:7474/status'); + curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5, CURLOPT_CONNECTTIMEOUT=>3]); + $raw = curl_exec($ch); $err = curl_error($ch); curl_close($ch); + if ($err || !$raw) j(['online'=>false, 'error'=>$err ?: 'unreachable']); + j(json_decode($raw, true) ?: ['online'=>false, 'error'=>'bad response']); + + case 'arc_jobs': + $status = $_GET['status'] ?? ''; + $limit = (int)($_GET['limit'] ?? 100); + $url = 'http://127.0.0.1:7474/jobs?' . http_build_query(array_filter(['status'=>$status,'limit'=>$limit])); + $ch = curl_init($url); + curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw, true) ?: []); + + case 'arc_job_get': + $id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id'); + $ch = curl_init('http://127.0.0.1:7474/job/' . $id); + curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw, true) ?: ['error'=>'not found']); + + case 'arc_ping': + $ch = curl_init('http://127.0.0.1:7474/job'); + curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5, CURLOPT_POST=>true, + CURLOPT_POSTFIELDS=>json_encode(['type'=>'ping','payload'=>[],'priority'=>9,'created_by'=>'admin']), + CURLOPT_HTTPHEADER=>['Content-Type: application/json']]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw, true) ?: ['error'=>'failed']); + + case 'arc_purge': + $ch = curl_init('http://127.0.0.1:7474/jobs/purge'); + curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5, CURLOPT_CUSTOMREQUEST=>'DELETE']); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw, true) ?: ['ok'=>true]); + case 'users_list': j(JarvisDB::query('SELECT id,username,display_name,last_seen,created_at FROM users ORDER BY username')); @@ -695,6 +734,8 @@ select.filter-sel:focus{border-color:var(--cyan)} + + @@ -943,6 +984,54 @@ select.filter-sel:focus{border-color:var(--cyan)}
SCANNING...
+ + +
+
⚡ ARC REACTOR — CORE DAEMON
+
+
+
STATUS
+
CHECKING...
+
+
+
VERSION
+
+
+
+
JOBS DONE
+
+
+
+
FAILED
+
+
+
+
HEARTBEAT
+
+
+
+
CAPABILITIES
+
+
+
+ +
+ + + + +
+ +
INITIALIZING...
+
+ diff --git a/public_html/api.php b/public_html/api.php index 2143f85..54ddff2 100644 --- a/public_html/api.php +++ b/public_html/api.php @@ -99,6 +99,9 @@ switch ($endpoint) { case "planner": require __DIR__ . '/../api/endpoints/planner.php'; break; + case "arc": + require __DIR__ . "/../api/endpoints/arc.php"; + break; case "calendar": require __DIR__ . '/../api/endpoints/calendar_sync.php'; break; diff --git a/public_html/index.html b/public_html/index.html index a36ed9b..9a11825 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -1121,6 +1121,11 @@ body::after{ AGENTS -- +
+
+ ARC REACTOR OFFLINE +
+
JARVIS v2.0 · SECURITY LEVEL ALPHA · UPDATED --:--:-- @@ -2290,6 +2295,7 @@ function showApp(name, greeting, silent = false) { loadNetwork(); loadHA(); checkAgentStatus(); + checkArcStatus().catch(() => {}); loadAgents(); loadAlerts(); loadWeather(); @@ -2489,6 +2495,10 @@ async function refreshAll() { loadPlannerSummary().catch(() => {}), ]); } + // Refresh Arc Reactor status every 6th tick (~60s) + if (_refreshTick % 6 === 0) { + checkArcStatus().catch(() => {}); + } // Refresh weather + news every 18th tick (~3 min) if (_refreshTick % 18 === 0) { Promise.all([ @@ -3436,6 +3446,57 @@ async function checkAgentStatus() { } } +// ── ARC REACTOR STATUS ──────────────────────────────────────────────── +let _arcOnline = false; +let _arcJobs = { queued: 0, running: 0, done: 0, failed: 0 }; + +async function checkArcStatus() { + const dot = document.getElementById('bb-arc-dot'); + const sta = document.getElementById('bb-arc-status'); + if (!dot || !sta) return; + try { + const d = await api('arc?action=status'); + if (d && d.online) { + _arcOnline = true; + dot.className = 'bb-dot online'; + const active = (d.active_jobs || 0) + (d.queued_jobs || 0); + sta.textContent = active > 0 ? active + ' JOB' + (active !== 1 ? 'S' : '') : 'ONLINE'; + _arcJobs = { queued: d.queued_jobs||0, running: d.running_jobs||0, + done: d.jobs_done||0, failed: d.jobs_failed||0 }; + } else { + _arcOnline = false; + dot.className = 'bb-dot offline'; + sta.textContent = 'OFFLINE'; + } + } catch(e) { + _arcOnline = false; + dot.className = 'bb-dot offline'; + sta.textContent = 'OFFLINE'; + } +} + +// Submit a job to the Arc Reactor and return job_id +async function arcSubmitJob(type, payload, priority) { + payload = payload || {}; + priority = priority || 5; + const d = await api('arc', { action: 'job_create', type: type, payload: payload, priority: priority }); + return d.job_id || null; +} + +// Poll a job until done or failed (max 120s), calling onProgress each tick +async function arcWaitJob(jobId, onProgress) { + var start = Date.now(); + while (Date.now() - start < 120000) { + const d = await api('arc?action=job_get&id=' + jobId); + if (onProgress) onProgress(d); + if (d.status === 'done') return d; + if (d.status === 'failed') throw new Error(d.error || 'Job failed'); + await new Promise(function(r){ setTimeout(r, 1500); }); + } + throw new Error('Arc Reactor job timed out'); +} + + async function loadAgents() { const [listData, metricsData] = await Promise.all([ api('agent/list'),