$msg]); }
function self_upsert_device(array $d): void {
JarvisDB::execute(
'INSERT INTO network_devices (ip,mac,hostname,status,last_seen) VALUES (?,?,?,"online",NOW())
ON DUPLICATE KEY UPDATE mac=VALUES(mac), hostname=COALESCE(VALUES(hostname),hostname), status="online", last_seen=NOW()',
[$d['ip'], $d['mac'], $d['hostname'] ?? $d['vendor']]
);
if (!empty($d['vendor'])) {
JarvisDB::execute('UPDATE network_devices SET device_type=? WHERE ip=? AND (device_type IS NULL OR device_type="")', [$d['vendor'], $d['ip']]);
}
}
// ── BACKEND API ───────────────────────────────────────────────────────────────
$action = $_GET['action'] ?? $_POST['action'] ?? '';
if ($action) {
// Login doesn't require session
if ($action === 'login') {
$u = trim($_POST['username'] ?? '');
$p = $_POST['password'] ?? '';
$row = JarvisDB::single('SELECT * FROM users WHERE username = ?', [$u]);
if ($row && password_verify($p, $row['password_hash'])) {
$_SESSION['admin_user'] = $row['username'];
$_SESSION['admin_name'] = $row['display_name'];
j(['ok' => true, 'name' => $row['display_name']]);
}
bad('Invalid credentials', 401);
}
if ($action === 'logout') { session_destroy(); j(['ok' => true]); }
if (!loggedIn()) bad('Not authenticated', 401);
switch ($action) {
// ── DASHBOARD ─────────────────────────────────────────────────────────
case 'dashboard':
$mi = [];
foreach (file('/proc/meminfo') as $l) {
[$k,$v] = explode(':', $l, 2) + [null,null];
if ($k) $mi[trim($k)] = (int)trim($v);
}
$mt = $mi['MemTotal'] ?? 0; $mf = $mi['MemAvailable'] ?? 0;
$up = (int)explode(' ', file_get_contents('/proc/uptime'))[0];
$la = explode(' ', file_get_contents('/proc/loadavg'));
$disk = trim(shell_exec("df / | tail -1 | awk '{print $5}'") ?? '');
j([
'sys' => [
'mem_pct' => $mt > 0 ? round(($mt-$mf)/$mt*100,1) : 0,
'mem_used_mb' => round(($mt-$mf)/1024),
'mem_total_mb' => round($mt/1024),
'uptime_s' => $up,
'load_1m' => (float)$la[0],
'disk_pct' => $disk,
],
'agents' => JarvisDB::single('SELECT COUNT(*) total, SUM(status="online") online FROM registered_agents'),
'alerts' => JarvisDB::single('SELECT COUNT(*) total, SUM(resolved=0) active FROM alerts'),
'devices' => JarvisDB::single('SELECT COUNT(*) total, SUM(status="online") online FROM network_devices WHERE alias IS NOT NULL'),
'facts' => JarvisDB::single('SELECT COUNT(*) total FROM kb_facts'),
'intents' => JarvisDB::single('SELECT COUNT(*) total, SUM(active=1) active FROM kb_intents'),
]);
// ── AGENTS ───────────────────────────────────────────────────────────
case 'agents_list':
$agents = JarvisDB::query('SELECT agent_id, hostname, agent_type, ip_address, status, last_seen, created_at FROM registered_agents ORDER BY status="online" DESC, hostname');
$metrics = JarvisDB::query(
"SELECT agent_id,
ROUND(JSON_EXTRACT(metric_data,'$.cpu_percent'),1) AS cpu_pct,
ROUND(JSON_EXTRACT(metric_data,'$.memory.percent'),1) AS mem_pct,
ROUND(JSON_EXTRACT(metric_data,'$.disk[0].percent'),1) AS disk_pct
FROM agent_metrics
WHERE metric_type='system' AND recorded_at > DATE_SUB(NOW(), INTERVAL 5 MINUTE)
GROUP BY agent_id ORDER BY recorded_at DESC"
);
$mm = array_column($metrics, null, 'agent_id');
foreach ($agents as &$a) $a['metrics'] = $mm[$a['agent_id']] ?? null;
j($agents);
case 'agents_delete':
$id = $_POST['agent_id'] ?? ''; if (!$id) bad('Missing agent_id');
JarvisDB::execute('DELETE FROM registered_agents WHERE agent_id=?', [$id]);
JarvisDB::execute('DELETE FROM agent_metrics WHERE agent_id=?', [$id]);
JarvisDB::execute('DELETE FROM agent_commands WHERE agent_id=?', [$id]);
j(['ok' => true]);
// ── NETWORK ──────────────────────────────────────────────────────────
case 'network_list':
j(JarvisDB::query('SELECT id,ip,mac,hostname,alias,device_type,status,last_seen FROM network_devices ORDER BY status="online" DESC, COALESCE(alias,hostname,ip)'));
case 'network_scan':
// Queue shell command to PVE1 agent — it runs jarvis-netscan.sh and pushes results back
$pve1 = JarvisDB::single('SELECT agent_id FROM registered_agents WHERE ip_address="10.48.200.90" AND status="online" LIMIT 1');
if (!$pve1) {
$pve1 = JarvisDB::single('SELECT agent_id FROM registered_agents WHERE hostname LIKE "%pve%" AND status="online" LIMIT 1');
}
if ($pve1) {
JarvisDB::execute(
'INSERT INTO agent_commands (agent_id, command_type, command_data, status) VALUES (?,?,?,?)',
[$pve1['agent_id'], 'shell', json_encode(['command'=>'/usr/local/bin/jarvis-netscan.sh','allowed'=>true]), 'pending']
);
j(['ok' => true, 'queued' => true, 'note' => 'Scan command sent to PVE1 agent — results in ~40 seconds']);
} else {
j(['ok' => false, 'note' => 'PVE1 agent offline — scan will run automatically via cron in < 3 minutes']);
}
case 'network_save':
$id = (int)($_POST['id'] ?? 0);
$ip = trim($_POST['ip'] ?? ''); $alias = trim($_POST['alias'] ?? '');
$type = trim($_POST['device_type'] ?? 'device');
if (!$ip || !$alias) bad('IP and alias required');
if ($id) {
JarvisDB::execute('UPDATE network_devices SET ip=?,alias=?,device_type=? WHERE id=?', [$ip,$alias,$type,$id]);
} else {
JarvisDB::execute('INSERT INTO network_devices (ip,alias,device_type,status) VALUES (?,?,?,"unknown") ON DUPLICATE KEY UPDATE alias=?,device_type=?', [$ip,$alias,$type,$alias,$type]);
}
j(['ok' => true]);
case 'network_delete':
$id = (int)($_POST['id'] ?? 0); if (!$id) bad('Missing id');
JarvisDB::execute('DELETE FROM network_devices WHERE id=?', [$id]);
j(['ok' => true]);
case 'network_ping':
$ip = trim($_POST['ip'] ?? ''); if (!$ip) bad('Missing IP');
$out = shell_exec('ping -c 2 -W 2 '.escapeshellarg($ip).' 2>/dev/null');
$alive = $out && (strpos($out,'2 received')!==false || strpos($out,'1 received')!==false);
$lat = null;
if ($alive && preg_match('/time=([\d.]+)/', $out, $m)) $lat = (float)$m[1];
j(['alive'=>$alive,'latency_ms'=>$lat]);
// ── ALERTS ───────────────────────────────────────────────────────────
case 'alerts_list':
$f = $_GET['filter'] ?? 'all';
$w = $f === 'active' ? 'WHERE resolved=0' : ($f === 'resolved' ? 'WHERE resolved=1' : '');
j(JarvisDB::query("SELECT id,alert_type,title,message,severity,resolved,created_at,resolved_at,source_key FROM alerts $w ORDER BY created_at DESC LIMIT 300"));
case 'alerts_resolve':
$id = (int)($_POST['id'] ?? 0); if (!$id) bad('Missing id');
JarvisDB::execute('UPDATE alerts SET resolved=1,resolved_at=NOW() WHERE id=?', [$id]);
j(['ok' => true]);
case 'alerts_resolve_all':
JarvisDB::execute('UPDATE alerts SET resolved=1,resolved_at=NOW() WHERE resolved=0');
j(['ok' => true]);
case 'alerts_delete':
$id = (int)($_POST['id'] ?? 0); if (!$id) bad('Missing id');
JarvisDB::execute('DELETE FROM alerts WHERE id=?', [$id]);
j(['ok' => true]);
case 'alerts_purge_resolved':
JarvisDB::execute('DELETE FROM alerts WHERE resolved=1');
j(['ok' => true]);
case 'alerts_save':
$id = (int)($_POST['id'] ?? 0);
$t = trim($_POST['title'] ?? ''); if (!$t) bad('Title required');
$typ = trim($_POST['alert_type'] ?? 'manual');
$msg = trim($_POST['message'] ?? '');
$sev = trim($_POST['severity'] ?? 'info');
if ($id) {
JarvisDB::execute('UPDATE alerts SET alert_type=?,title=?,message=?,severity=? WHERE id=?', [$typ,$t,$msg,$sev,$id]);
} else {
JarvisDB::execute('INSERT INTO alerts (alert_type,title,message,severity,resolved) VALUES (?,?,?,?,0)', [$typ,$t,$msg,$sev]);
}
j(['ok' => true]);
// ── KB FACTS ─────────────────────────────────────────────────────────
case 'facts_categories':
j(JarvisDB::query('SELECT category, COUNT(*) cnt FROM kb_facts GROUP BY category ORDER BY cnt DESC'));
case 'facts_list':
$cat = $_GET['category'] ?? '';
if ($cat === '__all__') {
j(JarvisDB::query('SELECT id,category,fact_key,fact_value,host,updated_at FROM kb_facts ORDER BY category,fact_key LIMIT 1000'));
}
j(JarvisDB::query('SELECT id,category,fact_key,fact_value,host,updated_at FROM kb_facts WHERE category=? ORDER BY fact_key', [$cat]));
case 'facts_save':
$id = (int)($_POST['id'] ?? 0);
$cat = trim($_POST['category'] ?? ''); $key = trim($_POST['fact_key'] ?? ''); $val = trim($_POST['fact_value'] ?? '');
if (!$cat||!$key) bad('Category and key required');
if ($id) {
JarvisDB::execute('UPDATE kb_facts SET category=?,fact_key=?,fact_value=? WHERE id=?', [$cat,$key,$val,$id]);
} else {
JarvisDB::execute('INSERT INTO kb_facts (category,fact_key,fact_value) VALUES (?,?,?)', [$cat,$key,$val]);
}
j(['ok' => true]);
case 'facts_delete':
$id = (int)($_POST['id'] ?? 0); if (!$id) bad('Missing id');
JarvisDB::execute('DELETE FROM kb_facts WHERE id=?', [$id]);
j(['ok' => true]);
// ── KB INTENTS ───────────────────────────────────────────────────────
case 'intents_list':
j(JarvisDB::query('SELECT id,intent_name,pattern,response_template,action_type,priority,active FROM kb_intents ORDER BY priority DESC,intent_name'));
case 'intents_save':
$id = (int)($_POST['id'] ?? 0);
$name = trim($_POST['intent_name'] ?? ''); $pat = trim($_POST['pattern'] ?? '');
$resp = trim($_POST['response_template'] ?? '');
$typ = trim($_POST['action_type'] ?? 'response');
$pri = (int)($_POST['priority'] ?? 5); $act = (int)($_POST['active'] ?? 1);
if (!$name||!$pat) bad('Name and pattern required');
if ($id) {
JarvisDB::execute('UPDATE kb_intents SET intent_name=?,pattern=?,response_template=?,action_type=?,priority=?,active=? WHERE id=?', [$name,$pat,$resp,$typ,$pri,$act,$id]);
} else {
JarvisDB::execute('INSERT INTO kb_intents (intent_name,pattern,response_template,action_type,priority,active) VALUES (?,?,?,?,?,?)', [$name,$pat,$resp,$typ,$pri,$act]);
}
j(['ok' => true]);
case 'intents_delete':
$id = (int)($_POST['id'] ?? 0); if (!$id) bad('Missing id');
JarvisDB::execute('DELETE FROM kb_intents WHERE id=?', [$id]);
j(['ok' => true]);
case 'intents_toggle':
$id = (int)($_POST['id'] ?? 0); if (!$id) bad('Missing id');
JarvisDB::execute('UPDATE kb_intents SET active=NOT active WHERE id=?', [$id]);
j(['ok' => true]);
// ── SITES ────────────────────────────────────────────────────────────
case 'sites_list':
j(JarvisDB::query("SELECT fact_key,fact_value,updated_at FROM kb_facts WHERE category='sites' ORDER BY fact_key"));
// ── HOME ASSISTANT ENTITIES ───────────────────────────────────────────
case 'ha_list':
// Read from ha_entities table (real-time pushes from jarvis_agent custom component)
$domain = $_GET['domain'] ?? '';
$search = strtolower(trim($_GET['search'] ?? ''));
$skipDomains = ['sensor','binary_sensor','button','update','select','number',
'device_tracker','event','image','person','zone','tts','conversation',
'assist_satellite','input_button'];
$skipKeywords = ['pre_release','_record','_ftp_','_push_','_hub_ringtone',
'_siren_on','_email_on','_manual_record','_infrared_',
'do_not_disturb','matter_server','zerotier','mariadb',
'spotify_connect','file_editor','ssh_web','uptime_kuma',
'folding_home','music_assistant','get_hacs','mealie',
'mosquitto','social_to','esphome_device','motion_detection',
'front_yard_record','down_hill_record','camera1_record',
'back_yard_record','nvr_','assist_microphone','cec_scanner'];
$where = "state NOT IN ('unavailable','unknown')";
$params = [];
if ($domain) { $where .= " AND domain=?"; $params[] = $domain; }
$rows = JarvisDB::query(
"SELECT entity_id, entity_name name, domain, state, updated_at
FROM ha_entities WHERE $where ORDER BY domain, entity_name LIMIT 500",
$params
) ?? [];
$all = []; $domains = [];
foreach ($rows as $e) {
$dom = $e['domain'];
if (in_array($dom, $skipDomains)) continue;
$skip = false;
if ($dom === 'switch') {
foreach ($skipKeywords as $kw) {
if (strpos($e['entity_id'], $kw) !== false) { $skip = true; break; }
}
}
if ($skip) continue;
if ($search && strpos(strtolower($e['name']??''), $search) === false) continue;
$all[] = $e;
$domains[$dom] = true;
}
j(['entities'=>$all,'domains'=>array_keys($domains),'total'=>count($all),'ts'=>time()]);
case 'ha_toggle':
$eid = trim($_POST['entity_id'] ?? ''); if (!$eid) bad('Missing entity_id');
$state = trim($_POST['state'] ?? '');
if (!defined('HA_URL')||!defined('HA_TOKEN')) bad('HA not configured');
$domain = explode('.',$eid)[0];
$svc = match($domain) {
'light','switch','input_boolean','fan' => ($state==='on'?'turn_off':'turn_on'),
default => ($state==='on'?'turn_off':'turn_on')
};
$ch = curl_init(HA_URL.'/api/services/'.$domain.'/'.$svc);
curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_POST=>true,
CURLOPT_HTTPHEADER=>['Authorization: Bearer '.HA_TOKEN,'Content-Type: application/json'],
CURLOPT_POSTFIELDS=>json_encode(['entity_id'=>$eid]),CURLOPT_TIMEOUT=>8]);
$res = curl_exec($ch); $code = curl_getinfo($ch,CURLINFO_HTTP_CODE); curl_close($ch);
j(['ok'=>$code<300,'code'=>$code]);
// ── NEWS ─────────────────────────────────────────────────────────────
case 'news_list':
$cached = JarvisDB::single("SELECT data,UNIX_TIMESTAMP(updated_at) ts FROM api_cache WHERE cache_key='news'");
$news = $cached ? (json_decode($cached['data'],true)??[]) : [];
$custom = JarvisDB::query("SELECT id,fact_key title,fact_value url,updated_at FROM kb_facts WHERE category='custom_news' ORDER BY id DESC");
j(['news'=>$news,'custom'=>$custom,'cache_age'=>$cached?time()-(int)$cached['ts']:null]);
case 'news_custom_save':
$id = (int)($_POST['id']??0);
$t = trim($_POST['title']??''); if(!$t) bad('Title required');
$url = trim($_POST['url']??'');
if($id) {
JarvisDB::execute('UPDATE kb_facts SET fact_key=?,fact_value=? WHERE id=? AND category="custom_news"',[$t,$url,$id]);
} else {
JarvisDB::execute('INSERT INTO kb_facts (category,fact_key,fact_value) VALUES ("custom_news",?,?)',[$t,$url]);
}
j(['ok'=>true]);
case 'news_custom_delete':
$id=(int)($_POST['id']??0); if(!$id) bad('Missing id');
JarvisDB::execute('DELETE FROM kb_facts WHERE id=? AND category="custom_news"',[$id]);
j(['ok'=>true]);
// ── PROXMOX VMs ───────────────────────────────────────────────────────
case 'vms_list':
$raw = JarvisDB::single("SELECT data,UNIX_TIMESTAMP(updated_at) ts FROM api_cache WHERE cache_key='proxmox'");
if (!$raw) j(['vms'=>[],'containers'=>[],'node_info'=>[],'ts'=>null]);
$pve = json_decode($raw['data'],true) ?? [];
j(['vms'=>$pve['vms']??[],'containers'=>$pve['containers']??[],'node_info'=>$pve['node_info']??[],'node_status'=>$pve['node_status']??null,'ts'=>$raw['ts']]);
// ── USERS ────────────────────────────────────────────────────────────
case 'email_inbox':
// Call via server's own IP — REMOTE_ADDR matches JARVIS_IP so auth bypass applies
$acct = $_GET['account'] ?? 'all';
$force = !empty($_GET['force']) ? '&force=1' : '';
$ch = curl_init('https://165.22.1.228/api/email?account=' . $acct . $force);
curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>25,
CURLOPT_SSL_VERIFYPEER=>false,CURLOPT_SSL_VERIFYHOST=>false,
CURLOPT_HTTPHEADER=>['Host: jarvis.orbishosting.com']]);
$r = curl_exec($ch); $code = curl_getinfo($ch,CURLINFO_HTTP_CODE); curl_close($ch);
if($code===200 && $r) j(json_decode($r,true));
else j(['error'=>'Email fetch failed (HTTP '.$code.')']);
case 'email_action_items':
$rows = JarvisDB::query("SELECT * FROM email_actions WHERE dismissed=0 AND task_id IS NULL AND appointment_id IS NULL ORDER BY received_at DESC LIMIT 100") ?? [];
j(['action_items'=>$rows]);
case 'email_create_task':
$id=(int)($_POST['id']??0); if(!$id) bad('No id');
$ea=JarvisDB::single('SELECT * FROM email_actions WHERE id=?',[$id]); if(!$ea) bad('Not found');
$title=trim($_POST['title']??$ea['suggested_title']);
$due=trim($_POST['due_date']??$ea['suggested_date']??'');
$notes="From: {$ea['from_name']} <{$ea['from_email']}>\nSubject: {$ea['subject']}";
$tid=JarvisDB::insert('INSERT INTO tasks(title,notes,category,priority,due_date)VALUES(?,?,?,?,?)',
[$title,$notes,'work','normal',$due?:null]);
JarvisDB::execute('UPDATE email_actions SET task_id=?,dismissed=1 WHERE id=?',[$tid,$id]);
j(['ok'=>true,'task_id'=>$tid]);
case 'email_create_appt':
$id=(int)($_POST['id']??0); if(!$id) bad('No id');
$ea=JarvisDB::single('SELECT * FROM email_actions WHERE id=?',[$id]); if(!$ea) bad('Not found');
$title=trim($_POST['title']??$ea['suggested_title']);
$start=trim($_POST['start_at']??'');
if(!$start) $start=($ea['suggested_date']??date('Y-m-d')).' 09:00:00';
$aid=JarvisDB::insert('INSERT INTO appointments(title,description,category,start_at)VALUES(?,?,?,?)',
[$title,"From: {$ea['from_name']} <{$ea['from_email']}>",'work',$start]);
JarvisDB::execute('UPDATE email_actions SET appointment_id=?,dismissed=1 WHERE id=?',[$aid,$id]);
j(['ok'=>true,'appointment_id'=>$aid]);
case 'email_dismiss':
$id=(int)($_POST['id']??0);
if($id) JarvisDB::execute('UPDATE email_actions SET dismissed=1 WHERE id=?',[$id]);
j(['ok'=>true]);
case 'task_list':
$status = trim($_GET['status'] ?? '');
$category = trim($_GET['category'] ?? '');
$where = '1=1'; $params = [];
if ($status) { $where .= ' AND status=?'; $params[] = $status; }
if ($category) { $where .= ' AND category=?'; $params[] = $category; }
else if (!$status) { $where .= " AND status NOT IN ('done','cancelled')"; }
$rows = JarvisDB::query("SELECT * FROM tasks WHERE {$where} ORDER BY FIELD(priority,'urgent','high','normal','low'),due_date ASC,created_at DESC LIMIT 200",$params) ?? [];
j(['tasks'=>$rows]);
case 'task_save':
$id=$_POST['id']??0; $title=trim($_POST['title']??'');
$notes=trim($_POST['notes']??''); $cat=$_POST['category']??'personal';
$pri=$_POST['priority']??'normal'; $stat=$_POST['status']??'pending';
$due=!empty($_POST['due_date'])?$_POST['due_date']:null;
$dtime=!empty($_POST['due_time'])?$_POST['due_time']:null;
if(!$title) bad('Title required');
if($id){
JarvisDB::execute('UPDATE tasks SET title=?,notes=?,category=?,priority=?,status=?,due_date=?,due_time=?,updated_at=NOW() WHERE id=?',[$title,$notes,$cat,$pri,$stat,$due,$dtime,$id]);
j(['ok'=>true,'id'=>(int)$id]);
} else {
$newId=JarvisDB::insert('INSERT INTO tasks(title,notes,category,priority,status,due_date,due_time)VALUES(?,?,?,?,?,?,?)',[$title,$notes,$cat,$pri,$stat,$due,$dtime]);
j(['ok'=>true,'id'=>$newId]);
}
case 'task_done':
$id=(int)($_POST['id']??0); if(!$id) bad('No id');
JarvisDB::execute("UPDATE tasks SET status='done',completed_at=NOW() WHERE id=?",[$id]);
j(['ok'=>true]);
case 'task_delete':
$id=(int)($_POST['id']??0); if(!$id) bad('No id');
JarvisDB::execute('DELETE FROM tasks WHERE id=?',[$id]);
j(['ok'=>true]);
case 'appt_list':
$from=$_GET['from']??date('Y-m-d'); $to=$_GET['to']??date('Y-m-d',strtotime('+90 days'));
$rows=JarvisDB::query("SELECT * FROM appointments WHERE DATE(start_at) BETWEEN ? AND ? ORDER BY start_at ASC LIMIT 200",[$from,$to]) ?? [];
j(['appointments'=>$rows]);
case 'appt_save':
$id=$_POST['id']??0; $title=trim($_POST['title']??''); $desc=trim($_POST['description']??'');
$cat=$_POST['category']??'personal'; $loc=trim($_POST['location']??'');
$all_day=(int)($_POST['all_day']??0); $rem=(int)($_POST['reminder_min']??30);
$start=trim($_POST['start_at']??''); $end=trim($_POST['end_at']??'');
if(!$title||!$start) bad('Title and start required');
$ts=strtotime($start); if(!$ts) bad('Invalid start datetime');
$startDt=date('Y-m-d H:i:s',$ts);
$endDt=($end&&strtotime($end))?date('Y-m-d H:i:s',strtotime($end)):null;
if($id){
JarvisDB::execute('UPDATE appointments SET title=?,description=?,category=?,start_at=?,end_at=?,location=?,all_day=?,reminder_min=?,alerted=0,updated_at=NOW() WHERE id=?',[$title,$desc,$cat,$startDt,$endDt,$loc,$all_day,$rem,$id]);
j(['ok'=>true,'id'=>(int)$id]);
} else {
$newId=JarvisDB::insert('INSERT INTO appointments(title,description,category,start_at,end_at,location,all_day,reminder_min)VALUES(?,?,?,?,?,?,?,?)',[$title,$desc,$cat,$startDt,$endDt,$loc,$all_day,$rem]);
j(['ok'=>true,'id'=>$newId]);
}
case 'appt_delete':
$id=(int)($_POST['id']??0); if(!$id) bad('No id');
JarvisDB::execute('DELETE FROM appointments WHERE id=?',[$id]);
j(['ok'=>true]);
case 'cal_feeds_list':
j(JarvisDB::query("SELECT * FROM calendar_feeds ORDER BY source,name") ?? []);
case 'cal_feed_save':
$id = (int)($_POST['id'] ?? 0);
$name = trim($_POST['name'] ?? '');
$source = $_POST['source'] ?? 'ics';
$ics = trim($_POST['ics_url'] ?? '');
$user = trim($_POST['username'] ?? '');
$pass = trim($_POST['password'] ?? '');
$active = (int)($_POST['active'] ?? 1);
if (!$name) bad('Name required');
if ($id) {
JarvisDB::execute("UPDATE calendar_feeds SET name=?,source=?,ics_url=?,username=?,password=?,active=? WHERE id=?",
[$name,$source,$ics,$user,$pass,$active,$id]);
j(['ok'=>true,'id'=>$id]);
} else {
$nid = JarvisDB::insert("INSERT INTO calendar_feeds(name,source,ics_url,username,password,active) VALUES(?,?,?,?,?,?)",
[$name,$source,$ics,$user,$pass,$active]);
j(['ok'=>true,'id'=>$nid]);
}
case 'cal_feed_delete':
$id = (int)($_POST['id'] ?? 0); if (!$id) bad('No id');
JarvisDB::execute("DELETE FROM calendar_feeds WHERE id=?", [$id]);
j(['ok'=>true]);
case 'cal_sync_now':
if (!class_exists('JarvisDB')) require_once __DIR__ . '/../../api/lib/db.php';
require_once __DIR__ . '/../../api/endpoints/calendar_sync.php';
$r = runSync();
j(['ok'=>true,'results'=>$r]);
// ── ARC REACTOR ──────────────────────────────────────────────────────
case 'workers_list':
$agents = JarvisDB::query(
'SELECT agent_id, hostname, agent_type, ip_address, status, capabilities, version, last_seen
FROM registered_agents ORDER BY status DESC, hostname ASC'
);
// Latest available versions per platform
$latestVersions = [
'linux' => '3.1',
'proxmox' => '3.1',
'windows' => '3.0',
'macos' => '3.0',
'homeassistant' => null,
];
$reactorRaw = @file_get_contents('http://127.0.0.1:7474/status');
$reactor = $reactorRaw ? json_decode($reactorRaw, true) : null;
$arcStats = JarvisDB::query(
'SELECT status, COUNT(*) as cnt FROM arc_jobs
WHERE created_at > DATE_SUB(NOW(), INTERVAL 24 HOUR) GROUP BY status'
);
$arcCounts = [];
foreach ($arcStats as $r) $arcCounts[$r['status']] = (int)$r['cnt'];
$cronLast = [];
$cronLog = '/home/jarvis.orbishosting.com/logs/cron.log';
if (file_exists($cronLog)) {
$lines = array_filter(explode("\n", shell_exec("grep -a 'facts\\|stats\\|calendar' " . escapeshellarg($cronLog) . " | tail -60")));
foreach ($lines as $line) {
if (preg_match('/^\\[(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\].*facts/i', $line, $m)) $cronLast['facts_collector'] = $m[1];
if (preg_match('/^\\[(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\].*stats/i', $line, $m)) $cronLast['stats_cache'] = $m[1];
if (preg_match('/^\\[(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\].*calendar/i', $line, $m)) $cronLast['calendar_sync'] = $m[1];
}
}
if (empty($cronLast['stats_cache'])) {
$row = JarvisDB::query('SELECT MAX(updated_at) as t FROM api_cache WHERE cache_key IN ("weather","news")');
if (!empty($row[0]['t'])) $cronLast['stats_cache'] = $row[0]['t'];
}
$deployLog = '/home/jarvis.orbishosting.com/logs/deploy.log';
if (file_exists($deployLog)) {
$last = shell_exec("grep -a '\\[' " . escapeshellarg($deployLog) . " | tail -1");
if (preg_match('/^\\[(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\]/', trim($last), $m)) $cronLast['jarvis_deploy'] = $m[1];
}
$wdLog = '/home/jarvis.orbishosting.com/logs/watchdog.log';
if (file_exists($wdLog)) $cronLast['jarvis_watchdog'] = date('Y-m-d H:i:s', filemtime($wdLog));
$bkLog = '/var/backups/jarvis/backup.log';
if (file_exists($bkLog)) {
$last = shell_exec("grep -a '\\[' " . escapeshellarg($bkLog) . " | tail -1");
if (preg_match('/^\\[(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\]/', trim($last), $m)) $cronLast['jarvis_backup'] = $m[1];
}
$doLog = '/var/log/do-server-backup.log';
if (file_exists($doLog)) $cronLast['do_server_backup'] = date('Y-m-d H:i:s', filemtime($doLog));
j(['agents'=>$agents,'reactor'=>$reactor,'arc_counts'=>$arcCounts,'cron_last'=>$cronLast,'latest_versions'=>$latestVersions]);
break;
case 'worker_action':
$wType = $data['worker_type'] ?? '';
$wId = $data['worker_id'] ?? '';
$wAction = $data['action'] ?? '';
if ($wType === 'agent' && $wAction === 'update') {
JarvisDB::execute('INSERT INTO agent_commands (agent_id,command_type,command_data,status) VALUES (?,?,?,?)',
[$wId,'update','{}','pending']);
j(['ok'=>true,'msg'=>'Update dispatched to '.$wId]);
} elseif ($wType === 'agent' && $wAction === 'screenshot') {
$ch = curl_init('http://127.0.0.1:7474/job');
curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_POST=>true,
CURLOPT_POSTFIELDS=>json_encode(['type'=>'screenshot','payload'=>['agent'=>$wId,'analyze'=>false],'priority'=>8,'created_by'=>'admin:workers']),
CURLOPT_HTTPHEADER=>['Content-Type: application/json'],CURLOPT_TIMEOUT=>5]);
j(json_decode(curl_exec($ch),true)?:['error'=>'reactor unreachable']);
} elseif ($wType === 'cron' && $wAction === 'run') {
$scripts = [
'facts_collector'=>[true, '/home/jarvis.orbishosting.com/api/endpoints/facts_collector.php'],
'stats_cache' =>[true, '/home/jarvis.orbishosting.com/api/endpoints/stats_cache.php'],
'calendar_sync' =>[true, '/home/jarvis.orbishosting.com/api/endpoints/calendar_sync.php'],
'jarvis_deploy' =>[false,'/usr/local/bin/jarvis-deploy.sh'],
'jarvis_watchdog'=>[false,'/usr/local/bin/jarvis-watchdog.sh'],
];
if (isset($scripts[$wId])) {
[$isPhp,$path] = $scripts[$wId];
$cmd = $isPhp
? '/usr/local/lsws/lsphp85/bin/lsphp '.escapeshellarg($path).' >> /home/jarvis.orbishosting.com/logs/cron.log 2>&1 &'
: escapeshellcmd($path).' >> /home/jarvis.orbishosting.com/logs/deploy.log 2>&1 &';
shell_exec($cmd);
j(['ok'=>true,'msg'=>ucwords(str_replace('_',' ',$wId)).' triggered']);
} else { bad('Unknown cron worker'); }
} elseif ($wType === 'daemon' && $wId === 'arc_reactor' && $wAction === 'restart') {
shell_exec('pkill -f reactor.py 2>/dev/null; sleep 1; cd /opt/jarvis-arc && source venv/bin/activate && nohup python3 reactor.py >> /home/jarvis.orbishosting.com/logs/arc_reactor.log 2>&1 &');
j(['ok'=>true,'msg'=>'Arc Reactor restarting']);
} else { bad('Invalid worker action'); }
break;
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]);
// ── GMAIL TRIAGE ──────────────────────────────────────────────────────
case 'triage_list':
$limit = min((int)($_GET['limit'] ?? 100), 200);
$filter = $_GET['filter'] ?? 'priority';
if ($filter === 'urgent') {
$rows = JarvisDB::query(
"SELECT * FROM email_triage WHERE action_taken != 'dismissed' AND category = 'urgent' ORDER BY priority DESC, created_at DESC LIMIT ?",
[$limit]
);
} elseif ($filter === 'action') {
$rows = JarvisDB::query(
"SELECT * FROM email_triage WHERE action_taken != 'dismissed' AND category IN ('urgent','action','reply','meeting') ORDER BY priority DESC, created_at DESC LIMIT ?",
[$limit]
);
} elseif ($filter === 'priority') {
$rows = JarvisDB::query(
"SELECT * FROM email_triage WHERE action_taken != 'dismissed' AND category IN ('urgent','action','reply','meeting') AND priority >= 5 ORDER BY priority DESC, created_at DESC LIMIT ?",
[$limit]
);
} else {
$rows = JarvisDB::query(
"SELECT * FROM email_triage ORDER BY priority DESC, created_at DESC LIMIT ?",
[$limit]
);
}
$counts = JarvisDB::single("SELECT COUNT(*) AS total,
SUM(category='urgent') AS urgent, SUM(category='action') AS action,
SUM(category='reply') AS reply, SUM(category='meeting') AS meeting,
SUM(action_taken='none') AS pending
FROM email_triage WHERE action_taken != 'dismissed'");
j(['items' => $rows ?: [], 'counts' => $counts]);
case 'triage_action':
$id = (int)($_GET['id'] ?? $_POST['id'] ?? 0); if (!$id) bad('Missing id');
$act = $_POST['action'] ?? $_GET['action_val'] ?? 'dismissed';
$allowed = ['dismissed','replied','done','snoozed'];
if (!in_array($act, $allowed)) bad('Invalid action');
JarvisDB::execute("UPDATE email_triage SET action_taken = ? WHERE id = ?", [$act, $id]);
j(['ok' => true]);
case 'triage_run':
$account = $_GET['account'] ?? 'gmail';
$maxEmails = (int)($_GET['max'] ?? 25);
$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'=>'gmail_triage','payload'=>['account'=>$account,'max_emails'=>$maxEmails,'provider'=>'claude'],'priority'=>7,'created_by'=>'admin']),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: ['error' => 'Arc Reactor unreachable']);
// ── OUTBOX ────────────────────────────────────────────────────────────
case 'outbox_list':
$limit = min((int)($_GET['limit'] ?? 50), 200);
$status = $_GET['status'] ?? '';
$qs = http_build_query(array_filter(['limit' => $limit, 'status' => $status]));
$ch = curl_init('http://127.0.0.1:7474/comms/sent' . ($qs ? "?{$qs}" : ''));
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: []);
case 'outbox_delete':
$id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id');
$ch = curl_init('http://127.0.0.1:7474/comms/sent/' . $id);
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) ?: ['error'=>'failed']);
case 'send_reply':
$id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing triage id');
$content = $_GET['content'] ?? '';
$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'=>'send_email','payload'=>['triage_id'=>$id,'content'=>$content],'priority'=>8,'created_by'=>'admin']),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: ['error' => 'Arc Reactor unreachable']);
case 'compose_email':
$to = $_GET['to'] ?? ''; if (!$to) bad('Missing recipient');
$subject = $_GET['subject'] ?? '';
$body = $_GET['body'] ?? '';
$account = $_GET['account'] ?? 'gmail';
$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'=>'compose_email','payload'=>['recipient'=>$to,'subject'=>$subject,'instructions'=>$body,'account'=>$account,'auto_send'=>false],'priority'=>7,'created_by'=>'admin']),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: ['error' => 'Arc Reactor unreachable']);
// ── DIRECTIVES ───────────────────────────────────────────────────────
case 'directive_list':
$status = $_GET['status'] ?? 'active';
$category = $_GET['category'] ?? '';
$where = '1=1'; $params = [];
if ($status && $status !== 'all') { $where .= ' AND d.status=?'; $params[] = $status; }
if ($category) { $where .= ' AND d.category=?'; $params[] = $category; }
$rows = JarvisDB::query(
"SELECT d.*,
COUNT(kr.id) AS kr_count,
COALESCE(SUM(kr.current_value),0) AS kr_current_sum,
COALESCE(SUM(kr.target_value),0) AS kr_target_sum,
(SELECT COUNT(*) FROM directive_links dl WHERE dl.directive_id=d.id) AS link_count
FROM directives d
LEFT JOIN directive_key_results kr ON kr.directive_id=d.id
WHERE {$where}
GROUP BY d.id
ORDER BY d.priority DESC, d.target_date ASC, d.created_at DESC",
$params
) ?: [];
foreach ($rows as &$r) {
$r['progress'] = ($r['kr_target_sum'] > 0)
? (float)round($r['kr_current_sum'] / $r['kr_target_sum'] * 100, 1)
: 0;
}
j(['directives' => $rows]);
case 'directive_get':
$id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id');
$d = JarvisDB::single("SELECT * FROM directives WHERE id=?", [$id]);
if (!$d) bad('Not found', 404);
$krs = JarvisDB::query("SELECT * FROM directive_key_results WHERE directive_id=? ORDER BY id", [$id]) ?: [];
$links = JarvisDB::query(
"SELECT dl.*, COALESCE(t.title,a.title) AS linked_title
FROM directive_links dl
LEFT JOIN tasks t ON dl.link_type='task' AND t.id=dl.link_id
LEFT JOIN appointments a ON dl.link_type='appointment' AND a.id=dl.link_id
WHERE dl.directive_id=? ORDER BY dl.created_at DESC",
[$id]
) ?: [];
$cur = array_sum(array_column($krs,'current_value'));
$tgt = array_sum(array_column($krs,'target_value'));
$d['progress'] = $tgt > 0 ? round($cur/$tgt*100,1) : 0;
$d['key_results'] = $krs;
$d['links'] = $links;
j($d);
case 'directive_save':
$id = (int)($_GET['id'] ?? 0);
$body = file_get_contents('php://input');
$data_in = json_decode($body, true) ?: [];
$title = trim($data_in['title'] ?? '');
$description = trim($data_in['description'] ?? '');
$category = $data_in['category'] ?? 'work';
$status = $data_in['status'] ?? 'active';
$priority = (int)($data_in['priority'] ?? 5);
$target_date = $data_in['target_date'] ?? null;
$krs = $data_in['key_results'] ?? [];
if (!$title) bad('Title required');
if ($id) {
JarvisDB::execute(
"UPDATE directives SET title=?,description=?,category=?,status=?,priority=?,target_date=?,updated_at=NOW() WHERE id=?",
[$title,$description,$category,$status,$priority,$target_date?:null,$id]
);
} else {
$id = JarvisDB::insert(
"INSERT INTO directives (title,description,category,status,priority,target_date) VALUES (?,?,?,?,?,?)",
[$title,$description,$category,$status,$priority,$target_date?:null]
);
}
if (is_array($krs)) {
JarvisDB::execute("DELETE FROM directive_key_results WHERE directive_id=?", [$id]);
foreach ($krs as $kr) {
$krt = trim($kr['title'] ?? ''); if (!$krt) continue;
JarvisDB::execute(
"INSERT INTO directive_key_results (directive_id,title,current_value,target_value,unit) VALUES (?,?,?,?,?)",
[$id,$krt,(float)($kr['current_value']??0),(float)($kr['target_value']??100),$kr['unit']??'%']
);
}
}
j(['ok' => true, 'id' => $id]);
case 'directive_delete':
$id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id');
JarvisDB::execute("DELETE FROM directive_key_results WHERE directive_id=?", [$id]);
JarvisDB::execute("DELETE FROM directive_links WHERE directive_id=?", [$id]);
JarvisDB::execute("DELETE FROM directives WHERE id=?", [$id]);
j(['ok' => true]);
case 'arc_action':
$body = file_get_contents('php://input');
$d = json_decode($body, true) ?: [];
$type = $d['action'] === 'job_create' ? ($d['type'] ?? '') : '';
$payload = $d['payload'] ?? [];
$pri = (int)($d['priority'] ?? 5);
if (!$type) bad('Missing type');
$ch = curl_init('http://127.0.0.1:7474/job');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>10, CURLOPT_POST=>true,
CURLOPT_POSTFIELDS => json_encode(['type'=>$type,'payload'=>$payload,'priority'=>$pri,'created_by'=>'admin']),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: ['error'=>'Arc Reactor unreachable']);
// ── MISSION OPS ──────────────────────────────────────────────────────
case 'mission_list':
$ch = curl_init('http://127.0.0.1:7474/missions');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: []);
case 'mission_get':
$id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id');
$ch = curl_init('http://127.0.0.1:7474/missions/' . $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 'mission_runs':
$id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id');
$limit = (int)($_GET['limit'] ?? 20);
$ch = curl_init("http://127.0.0.1:7474/missions/{$id}/runs?limit={$limit}");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: []);
case 'mission_save': // create or update
$id = (int)($_GET['id'] ?? 0);
$url = $id ? "http://127.0.0.1:7474/missions/{$id}" : 'http://127.0.0.1:7474/missions';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>10,
CURLOPT_CUSTOMREQUEST => $id ? 'PUT' : 'POST',
CURLOPT_POSTFIELDS => file_get_contents('php://input'),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: ['error'=>'Arc Reactor unreachable']);
case 'mission_delete':
$id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id');
$ch = curl_init('http://127.0.0.1:7474/missions/' . $id);
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 'mission_run':
$id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id');
$ch = curl_init("http://127.0.0.1:7474/missions/{$id}/run");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>120, CURLOPT_POST=>true,
CURLOPT_POSTFIELDS => json_encode(['trigger_source'=>'admin']),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: ['error'=>'Arc Reactor unreachable or timeout']);
case 'mission_toggle':
$id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id');
$enabled = (int)($_GET['enabled'] ?? 0);
$ch = curl_init('http://127.0.0.1:7474/missions/' . $id);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5, CURLOPT_CUSTOMREQUEST=>'PUT',
CURLOPT_POSTFIELDS => json_encode(['enabled'=>$enabled]),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: ['ok'=>true]);
// ── CLEARANCE PROTOCOL ───────────────────────────────────────────────
case 'clearance_pending':
$ch = curl_init('http://127.0.0.1:7474/clearance/pending');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: []);
case 'clearance_history':
$limit = min((int)($_GET['limit'] ?? 50), 200);
$ch = curl_init('http://127.0.0.1:7474/clearance/history?limit=' . $limit);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: []);
case 'clearance_approve':
$id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id');
$body = json_decode(file_get_contents('php://input'), true) ?: [];
$decidedBy = $body['decided_by'] ?? 'admin';
$ch = curl_init('http://127.0.0.1:7474/clearance/' . $id . '/approve');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>10, CURLOPT_POST=>true,
CURLOPT_POSTFIELDS => json_encode(['decided_by'=>$decidedBy]),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: ['ok'=>true]);
case 'clearance_deny':
$id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id');
$body = json_decode(file_get_contents('php://input'), true) ?: [];
$decidedBy = $body['decided_by'] ?? 'admin';
$note = $body['note'] ?? '';
$ch = curl_init('http://127.0.0.1:7474/clearance/' . $id . '/deny');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5, CURLOPT_POST=>true,
CURLOPT_POSTFIELDS => json_encode(['decided_by'=>$decidedBy,'note'=>$note]),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: ['ok'=>true]);
case 'clearance_rules':
$ch = curl_init('http://127.0.0.1:7474/clearance/rules');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: []);
case 'clearance_rule_update':
$id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id');
$body = json_decode(file_get_contents('php://input'), true) ?: [];
$ch = curl_init('http://127.0.0.1:7474/clearance/rules/' . $id);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5, CURLOPT_CUSTOMREQUEST=>'PUT',
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: ['ok'=>true]);
case 'clearance_rule_create':
$body = json_decode(file_get_contents('php://input'), true) ?: [];
$ch = curl_init('http://127.0.0.1:7474/clearance/rules');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5, CURLOPT_POST=>true,
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: ['ok'=>true]);
// ── MEMORY CORE ──────────────────────────────────────────────────────
case 'memory_list':
$limit = min((int)($_GET['limit'] ?? 200), 500);
$category = $_GET['category'] ?? '';
$search = $_GET['search'] ?? '';
$qs = http_build_query(array_filter(['limit'=>$limit,'category'=>$category,'search'=>$search]));
$ch = curl_init('http://127.0.0.1:7474/memory/facts' . ($qs ? '?'.$qs : ''));
curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>5]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw,true) ?: []);
case 'memory_stats':
$ch = curl_init('http://127.0.0.1:7474/memory/stats');
curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>5]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw,true) ?: ['total'=>0,'by_category'=>[]]);
case 'memory_store':
$body = json_decode(file_get_contents('php://input'),true) ?: [];
$ch = curl_init('http://127.0.0.1:7474/memory/facts');
curl_setopt_array($ch,[
CURLOPT_RETURNTRANSFER=>true, CURLOPT_POST=>true, CURLOPT_TIMEOUT=>5,
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw,true) ?: ['ok'=>true]);
case 'memory_delete':
$id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id');
$ch = curl_init('http://127.0.0.1:7474/memory/facts/' . $id);
curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_CUSTOMREQUEST=>'DELETE',CURLOPT_TIMEOUT=>5]);
curl_exec($ch); curl_close($ch);
j(['ok'=>true]);
case 'memory_clear':
$category = $_GET['category'] ?? '';
$url = 'http://127.0.0.1:7474/memory/facts' . ($category ? '?category=' . urlencode($category) : '');
$ch = curl_init($url);
curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_CUSTOMREQUEST=>'DELETE',CURLOPT_TIMEOUT=>5]);
curl_exec($ch); curl_close($ch);
j(['ok'=>true]);
// ── VISION PROTOCOL ──────────────────────────────────────────────────
case 'vision_list':
$limit = min((int)($_GET['limit'] ?? 30), 100);
$agent = $_GET['agent'] ?? '';
$url = 'http://127.0.0.1:7474/screenshots?' . http_build_query(array_filter(['limit'=>$limit,'agent'=>$agent]));
$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 'vision_get':
$id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id');
$ch = curl_init('http://127.0.0.1:7474/screenshots/' . $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 'vision_delete':
$id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id');
$ch = curl_init('http://127.0.0.1:7474/screenshots/' . $id);
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 'vision_screenshot':
$agent = trim($_GET['agent'] ?? ''); if (!$agent) bad('Missing agent');
$analyze = ($_GET['analyze'] ?? '1') !== '0';
$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'=>'screenshot','payload'=>['agent'=>$agent,'analyze'=>$analyze],'priority'=>8,'created_by'=>'admin']),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: ['error'=>'Arc Reactor unreachable']);
case 'vision_analyze':
$id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id');
$ch = curl_init('http://127.0.0.1:7474/job');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_POST=>true, CURLOPT_TIMEOUT=>5,
CURLOPT_POSTFIELDS=>json_encode(['type'=>'vision','payload'=>['screenshot_id'=>$id,'provider'=>'claude'],'priority'=>8,'created_by'=>'admin']),
CURLOPT_HTTPHEADER=>['Content-Type: application/json']]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: ['error'=>'Arc Reactor unreachable']);
break;
case 'vision_purge':
$ch = curl_init('http://127.0.0.1:7474/screenshots/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]);
// ── GUARDIAN MODE ─────────────────────────────────────────────────
case 'guardian_status':
$ch = curl_init('http://127.0.0.1:7474/guardian/status');
curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>5]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw,true) ?: ['error'=>'unreachable']);
case 'guardian_events':
$limit = (int)($_GET['limit'] ?? 50);
$severity = $_GET['severity'] ?? '';
$url = 'http://127.0.0.1:7474/guardian/events?' . http_build_query(array_filter(['limit'=>$limit,'severity'=>$severity]));
$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 'guardian_ack':
$id = (int)($_GET['id'] ?? $_POST['id'] ?? 0);
if ($id) {
$ch = curl_init('http://127.0.0.1:7474/guardian/events/'.$id.'/ack');
} else {
$ch = curl_init('http://127.0.0.1:7474/guardian/events/ack_all');
}
curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>5,CURLOPT_POST=>true,CURLOPT_POSTFIELDS=>'']);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw,true) ?: ['ok'=>true]);
case 'guardian_sitrep':
$detail = $_GET['detail'] ?? 'full';
$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'=>'sitrep','payload'=>['detail'=>$detail,'provider'=>'claude'],'priority'=>9,'created_by'=>'admin']),
CURLOPT_HTTPHEADER=>['Content-Type: application/json'],
]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw,true) ?: ['error'=>'Arc Reactor unreachable']);
case 'guardian_config_set':
$key = $_POST['key'] ?? $_GET['key'] ?? '';
$val = $_POST['value'] ?? $_GET['value'] ?? '';
if (!$key) bad('Missing key');
$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'=>'guardian_config','payload'=>['action'=>'set','key'=>$key,'value'=>$val],'priority'=>9,'created_by'=>'admin']),
CURLOPT_HTTPHEADER=>['Content-Type: application/json'],
]);
$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'));
case 'users_save':
$id = (int)($_POST['id'] ?? 0); if (!$id) bad('Missing id');
$dn = trim($_POST['display_name'] ?? '');
$pw = trim($_POST['password'] ?? '');
if ($pw) {
JarvisDB::execute('UPDATE users SET display_name=?,password_hash=? WHERE id=?', [$dn, password_hash($pw, PASSWORD_BCRYPT), $id]);
} else {
JarvisDB::execute('UPDATE users SET display_name=? WHERE id=?', [$dn, $id]);
}
j(['ok' => true]);
// ── BACKUPS ───────────────────────────────────────────────────────────
case 'backups_list':
$dir = '/var/backups/jarvis';
$lock = "$dir/backup.lock";
$log = "$dir/backup.log";
$running = file_exists($lock) && (time() - filemtime($lock)) < 3600;
$files = [];
foreach (glob("$dir/jarvis_backup_*.tar.gz") ?: [] as $f) {
$files[] = [
'file' => basename($f),
'size' => filesize($f),
'size_mb' => round(filesize($f)/1048576, 1),
'date' => date('Y-m-d H:i:s', filemtime($f)),
];
}
usort($files, fn($a,$b) => strcmp($b['date'], $a['date']));
$lastLog = $log && file_exists($log) ? trim(shell_exec("tail -3 " . escapeshellarg($log))) : '';
j(['running' => $running, 'files' => $files, 'last_log' => $lastLog]);
case 'backup_trigger':
$lock = '/var/backups/jarvis/backup.lock';
if (file_exists($lock) && (time() - filemtime($lock)) < 3600) {
j(['ok' => false, 'message' => 'Backup already running']);
}
shell_exec('nohup /usr/local/bin/jarvis-backup.sh > /dev/null 2>&1 &');
sleep(1);
j(['ok' => true, 'message' => 'Backup started']);
case 'backup_download':
$file = basename($_GET['file'] ?? '');
if (!preg_match('/^jarvis_backup_[\d_-]+\.tar\.gz$/', $file)) bad('Invalid filename');
$path = '/var/backups/jarvis/' . $file;
if (!file_exists($path)) bad('File not found', 404);
header('Content-Type: application/gzip');
header('Content-Disposition: attachment; filename="' . $file . '"');
header('Content-Length: ' . filesize($path));
header('X-Accel-Buffering: no');
ob_end_clean();
readfile($path);
exit;
default: bad('Unknown action');
}
}
?>
JARVIS ADMIN
JARVIS
ADMIN PORTAL
⚙ JARVIS AGENT WORKERS
FIELD AGENTS
| HOSTNAME | TYPE | IP | STATUS | VERSION | CAPABILITIES | LAST SEEN | ACTIONS |
| LOADING... |
CRON WORKERS
| WORKER | SCHEDULE | HOST | LAST RUN | ACTIONS |
DAEMONS
| DAEMON | HOST | STATUS | INFO | ACTIONS |
NETWORK DEVICES
FILTER:
ALERTS
FILTER:
KB FACTS
CATEGORY:
KB INTENTS
HOME ASSISTANT ENTITIES
DOMAIN:
NEWS MANAGEMENT
LIVE FEED (auto-refreshed)
BACKUPS
Daily automatic backup runs at 2:00 AM. Files + all databases. Last 7 days retained. Stored on server — download anytime.
EMAIL INTELLIGENCE
TASKS
APPOINTMENTS
CALENDAR SYNC
iCloud CalDAV syncs automatically every 15 min. Add Google Calendar or ICS feeds below.
⚡ ARC REACTOR — CORE DAEMON
◈ VISION PROTOCOL — FIELD SCREENSHOTS
◈ GUARDIAN MODE
◈ COMMS PROTOCOL — GMAIL TRIAGE
◈ COMMS OUTBOX — SENT & QUEUED
◈ MISSION OPS — AUTOMATED WORKFLOWS
◈ MISSION BUILDER
TRIGGER
◈ MISSION DIRECTIVES — OBJECTIVES & KEY RESULTS
◈ DIRECTIVE EDITOR
STATUS
CATEGORY
KEY RESULTS
🔒 CLEARANCE PROTOCOL
CLEARANCE RULES
ADD CUSTOM RULE
RISK LEVEL
REQUIRE APPROVAL
◈ MEMORY CORE — KNOWLEDGE GRAPH
CATEGORY