$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 '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]); // ── 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_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

ADMIN PORTAL
DASHBOARD
SCANNING...
AGENTS
NETWORK DEVICES
FILTER:  
SCANNING...
ALERTS
FILTER:
SCANNING...
KB FACTS
CATEGORY:  
SCANNING...
KB INTENTS
SCANNING...
HOME ASSISTANT ENTITIES
DOMAIN:    
SCANNING...
NEWS MANAGEMENT
PINNED / CUSTOM NEWS
SCANNING...
LIVE FEED (auto-refreshed)
SCANNING...
PROXMOX VMs
SCANNING...
BACKUPS
Daily automatic backup runs at 2:00 AM. Files + all databases. Last 7 days retained. Stored on server — download anytime.
SCANNING...
SITE HEALTH
SCANNING...
EMAIL INTELLIGENCE
TASKS
APPOINTMENTS
CALENDAR SYNC
iCloud CalDAV syncs automatically every 15 min. Add Google Calendar or ICS feeds below.
USERS
SCANNING...
⚡ ARC REACTOR — CORE DAEMON
STATUS
CHECKING...
VERSION
JOBS DONE
FAILED
HEARTBEAT
CAPABILITIES
INITIALIZING...
◈ VISION PROTOCOL — FIELD SCREENSHOTS
◈ GUARDIAN MODE
STATUS
CHECKING...
LAST SCAN
UNREAD
24H EVENTS
THRESHOLDS
LOADING...
◈ COMMS PROTOCOL — GMAIL TRIAGE
LOADING TRIAGE DATA...
◈ COMMS OUTBOX — SENT & QUEUED
LOADING OUTBOX...
◈ MISSION OPS — AUTOMATED WORKFLOWS
LOADING MISSIONS...
◈ MISSION DIRECTIVES — OBJECTIVES & KEY RESULTS
LOADING DIRECTIVES...