mirror of
https://github.com/myronblair/jarvis
synced 2026-07-28 16:52:46 -05:00
feat: Phase 1 — Arc Reactor Core Daemon
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* JARVIS Arc Reactor Bridge
|
||||||
|
* Proxies job requests to the Arc Reactor daemon at 127.0.0.1:7474
|
||||||
|
*/
|
||||||
|
|
||||||
|
define('ARC_REACTOR_URL', 'http://127.0.0.1:7474');
|
||||||
|
define('ARC_TIMEOUT', 8);
|
||||||
|
|
||||||
|
function arc_request(string $method, string $path, array $body = []): array {
|
||||||
|
$url = ARC_REACTOR_URL . $path;
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_RETURNTRANSFER => 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}"]);
|
||||||
|
}
|
||||||
@@ -459,6 +459,45 @@ if ($action) {
|
|||||||
$r = runSync();
|
$r = runSync();
|
||||||
j(['ok'=>true,'results'=>$r]);
|
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':
|
case 'users_list':
|
||||||
j(JarvisDB::query('SELECT id,username,display_name,last_seen,created_at FROM users ORDER BY username'));
|
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)}
|
|||||||
<div class="nav-item" data-tab="tasks" onclick="nav(this)">📋 TASKS</div>
|
<div class="nav-item" data-tab="tasks" onclick="nav(this)">📋 TASKS</div>
|
||||||
<div class="nav-item" data-tab="appointments" onclick="nav(this)">📅 APPOINTMENTS</div>
|
<div class="nav-item" data-tab="appointments" onclick="nav(this)">📅 APPOINTMENTS</div>
|
||||||
<div class="nav-item" data-tab="calendar" onclick="nav(this)">🗓 CALENDAR SYNC</div>
|
<div class="nav-item" data-tab="calendar" onclick="nav(this)">🗓 CALENDAR SYNC</div>
|
||||||
|
<div class="nav-section">ARC REACTOR</div>
|
||||||
|
<div class="nav-item" data-tab="arc" onclick="nav(this)">⚡ ARC REACTOR</div>
|
||||||
<div class="nav-section">INFO</div>
|
<div class="nav-section">INFO</div>
|
||||||
<div class="nav-item" data-tab="sites" onclick="nav(this)">SITES</div>
|
<div class="nav-item" data-tab="sites" onclick="nav(this)">SITES</div>
|
||||||
<div class="nav-item" data-tab="users" onclick="nav(this)">USERS</div>
|
<div class="nav-item" data-tab="users" onclick="nav(this)">USERS</div>
|
||||||
@@ -943,6 +984,54 @@ select.filter-sel:focus{border-color:var(--cyan)}
|
|||||||
<div class="tbl-wrap" id="users-tbl"><div class="loading">SCANNING...</div></div>
|
<div class="tbl-wrap" id="users-tbl"><div class="loading">SCANNING...</div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ARC REACTOR -->
|
||||||
|
<div class="tab" id="tab-arc">
|
||||||
|
<div class="page-title">⚡ ARC REACTOR — CORE DAEMON</div>
|
||||||
|
<div id="arc-status-bar" style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap">
|
||||||
|
<div class="stat-box" style="background:rgba(0,212,255,0.06);border:1px solid var(--border);padding:12px 20px;border-radius:4px;min-width:130px">
|
||||||
|
<div style="font-size:0.55rem;letter-spacing:2px;color:var(--dim);margin-bottom:4px">STATUS</div>
|
||||||
|
<div id="arc-status-val" style="font-size:1.1rem;font-family:var(--mono);color:var(--green)">CHECKING...</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-box" style="background:rgba(0,212,255,0.06);border:1px solid var(--border);padding:12px 20px;border-radius:4px;min-width:130px">
|
||||||
|
<div style="font-size:0.55rem;letter-spacing:2px;color:var(--dim);margin-bottom:4px">VERSION</div>
|
||||||
|
<div id="arc-version-val" style="font-size:1.1rem;font-family:var(--mono)">—</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-box" style="background:rgba(0,212,255,0.06);border:1px solid var(--border);padding:12px 20px;border-radius:4px;min-width:130px">
|
||||||
|
<div style="font-size:0.55rem;letter-spacing:2px;color:var(--dim);margin-bottom:4px">JOBS DONE</div>
|
||||||
|
<div id="arc-done-val" style="font-size:1.1rem;font-family:var(--mono)">—</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-box" style="background:rgba(0,212,255,0.06);border:1px solid var(--border);padding:12px 20px;border-radius:4px;min-width:130px">
|
||||||
|
<div style="font-size:0.55rem;letter-spacing:2px;color:var(--dim);margin-bottom:4px">FAILED</div>
|
||||||
|
<div id="arc-fail-val" style="font-size:1.1rem;font-family:var(--mono)">—</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-box" style="background:rgba(0,212,255,0.06);border:1px solid var(--border);padding:12px 20px;border-radius:4px;min-width:130px">
|
||||||
|
<div style="font-size:0.55rem;letter-spacing:2px;color:var(--dim);margin-bottom:4px">HEARTBEAT</div>
|
||||||
|
<div id="arc-hb-val" style="font-size:0.75rem;font-family:var(--mono)">—</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-box" style="background:rgba(0,212,255,0.06);border:1px solid var(--border);padding:12px 20px;border-radius:4px;min-width:130px">
|
||||||
|
<div style="font-size:0.55rem;letter-spacing:2px;color:var(--dim);margin-bottom:4px">CAPABILITIES</div>
|
||||||
|
<div id="arc-caps-val" style="font-size:0.65rem;font-family:var(--mono)">—</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;gap:10px;margin-bottom:16px;align-items:center;flex-wrap:wrap">
|
||||||
|
<button class="btn btn-sm" onclick="loadArc()">↻ REFRESH</button>
|
||||||
|
<button class="btn btn-sm btn-green" onclick="arcTestPing()">PING TEST</button>
|
||||||
|
<button class="btn btn-sm" onclick="arcPurge()" style="margin-left:auto;opacity:0.7">PURGE OLD JOBS</button>
|
||||||
|
<select id="arc-job-filter" class="inp" style="width:auto;padding:4px 8px;font-size:0.65rem" onchange="loadArc()">
|
||||||
|
<option value="">ALL JOBS</option>
|
||||||
|
<option value="queued">QUEUED</option>
|
||||||
|
<option value="running">RUNNING</option>
|
||||||
|
<option value="done">DONE</option>
|
||||||
|
<option value="failed">FAILED</option>
|
||||||
|
<option value="cancelled">CANCELLED</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tbl-wrap" id="arc-jobs-tbl"><div class="loading">INITIALIZING...</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div><!-- /content -->
|
</div><!-- /content -->
|
||||||
</div><!-- /main -->
|
</div><!-- /main -->
|
||||||
</div><!-- /app -->
|
</div><!-- /app -->
|
||||||
|
|||||||
@@ -99,6 +99,9 @@ switch ($endpoint) {
|
|||||||
case "planner":
|
case "planner":
|
||||||
require __DIR__ . '/../api/endpoints/planner.php';
|
require __DIR__ . '/../api/endpoints/planner.php';
|
||||||
break;
|
break;
|
||||||
|
case "arc":
|
||||||
|
require __DIR__ . "/../api/endpoints/arc.php";
|
||||||
|
break;
|
||||||
case "calendar":
|
case "calendar":
|
||||||
require __DIR__ . '/../api/endpoints/calendar_sync.php';
|
require __DIR__ . '/../api/endpoints/calendar_sync.php';
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1121,6 +1121,11 @@ body::after{
|
|||||||
<span>AGENTS</span> <span id="bb-agent-status">--</span>
|
<span>AGENTS</span> <span id="bb-agent-status">--</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="bb-item">
|
||||||
|
<div class="bb-dot" id="bb-arc-dot"></div>
|
||||||
|
<span>ARC REACTOR</span> <span id="bb-arc-status">OFFLINE</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="ekgWrap" style="margin-left:auto"><canvas id="ekgCanvas"></canvas></div>
|
<div id="ekgWrap" style="margin-left:auto"><canvas id="ekgCanvas"></canvas></div>
|
||||||
<div style="font-size:0.65rem;flex-shrink:0">
|
<div style="font-size:0.65rem;flex-shrink:0">
|
||||||
JARVIS v2.0 · SECURITY LEVEL ALPHA · UPDATED <span id="last-refresh">--:--:--</span>
|
JARVIS v2.0 · SECURITY LEVEL ALPHA · UPDATED <span id="last-refresh">--:--:--</span>
|
||||||
@@ -2290,6 +2295,7 @@ function showApp(name, greeting, silent = false) {
|
|||||||
loadNetwork();
|
loadNetwork();
|
||||||
loadHA();
|
loadHA();
|
||||||
checkAgentStatus();
|
checkAgentStatus();
|
||||||
|
checkArcStatus().catch(() => {});
|
||||||
loadAgents();
|
loadAgents();
|
||||||
loadAlerts();
|
loadAlerts();
|
||||||
loadWeather();
|
loadWeather();
|
||||||
@@ -2489,6 +2495,10 @@ async function refreshAll() {
|
|||||||
loadPlannerSummary().catch(() => {}),
|
loadPlannerSummary().catch(() => {}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
// Refresh Arc Reactor status every 6th tick (~60s)
|
||||||
|
if (_refreshTick % 6 === 0) {
|
||||||
|
checkArcStatus().catch(() => {});
|
||||||
|
}
|
||||||
// Refresh weather + news every 18th tick (~3 min)
|
// Refresh weather + news every 18th tick (~3 min)
|
||||||
if (_refreshTick % 18 === 0) {
|
if (_refreshTick % 18 === 0) {
|
||||||
Promise.all([
|
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() {
|
async function loadAgents() {
|
||||||
const [listData, metricsData] = await Promise.all([
|
const [listData, metricsData] = await Promise.all([
|
||||||
api('agent/list'),
|
api('agent/list'),
|
||||||
|
|||||||
Reference in New Issue
Block a user