admin: Workers page — consolidated view of all JARVIS Agent Workers

Single tab showing field agents (capabilities, status, last seen, update/screenshot
actions), cron workers (schedule, last run, run-now button), and Arc Reactor daemon
(handler count, 24h job stats, restart button). wToast for action feedback.
This commit is contained in:
2026-06-11 21:31:50 +00:00
parent 6eb387899e
commit 950749323c
+193
View File
@@ -464,6 +464,86 @@ if ($action) {
// ── ARC REACTOR ──────────────────────────────────────────────────────
case 'workers_list':
$agents = JarvisDB::query(
'SELECT agent_id, hostname, agent_type, ip_address, status, capabilities, last_seen
FROM registered_agents ORDER BY status DESC, hostname ASC'
);
$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]);
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]);
@@ -1197,6 +1277,7 @@ select.filter-sel:focus{border-color:var(--cyan)}
<div class="nav-item" data-tab="backups" onclick="nav(this)">💾 BACKUPS</div>
<div class="nav-section">MANAGE</div>
<div class="nav-item" data-tab="agents" onclick="nav(this)">AGENTS</div>
<div class="nav-item" data-tab="workers" onclick="nav(this)">&#9881; WORKERS</div>
<div class="nav-item" data-tab="network" onclick="nav(this)">NETWORK</div>
<div class="nav-item" data-tab="alerts" onclick="nav(this)">ALERTS</div>
<div class="nav-section">KNOWLEDGE</div>
@@ -1244,6 +1325,24 @@ select.filter-sel:focus{border-color:var(--cyan)}
</div>
<!-- NETWORK -->
<div class="tab" id="tab-workers">
<div class="page-title">&#9881; JARVIS AGENT WORKERS
<button onclick="loadWorkers()" style="font-size:0.6rem;padding:4px 12px">&#8635; REFRESH</button>
</div>
<div class="page-title" style="font-size:0.7rem;margin-top:0;border:none;padding-bottom:4px">FIELD AGENTS</div>
<table><thead><tr>
<th>HOSTNAME</th><th>TYPE</th><th>IP</th><th>STATUS</th><th>CAPABILITIES</th><th>LAST SEEN</th><th>ACTIONS</th>
</tr></thead><tbody id="workers-agents"><tr><td colspan="7" class="loading">LOADING...</td></tr></tbody></table>
<div class="page-title" style="font-size:0.7rem;margin-top:24px;border:none;padding-bottom:4px">CRON WORKERS</div>
<table><thead><tr>
<th>WORKER</th><th>SCHEDULE</th><th>HOST</th><th>LAST RUN</th><th>ACTIONS</th>
</tr></thead><tbody id="workers-crons"></tbody></table>
<div class="page-title" style="font-size:0.7rem;margin-top:24px;border:none;padding-bottom:4px">DAEMONS</div>
<table><thead><tr>
<th>DAEMON</th><th>HOST</th><th>STATUS</th><th>INFO</th><th>ACTIONS</th>
</tr></thead><tbody id="workers-daemons"></tbody></table>
</div>
<div class="tab" id="tab-network">
<div class="page-title">NETWORK DEVICES <span id="net-title-count" style="color:var(--cyan);font-size:0.6rem;letter-spacing:2px"></span>
<div class="actions">
@@ -1988,6 +2087,7 @@ function loadTab(tab) {
backups: loadBackups,
dashboard: loadDashboard,
agents: loadAgents,
workers: loadWorkers,
network: ()=>{ loadNetwork(); _netAutoRefresh = setInterval(loadNetwork, 30000); },
alerts: loadAlerts,
facts: ()=>{ loadFactCategories(); loadFacts(); },
@@ -2016,6 +2116,99 @@ function loadTab(tab) {
function initApp() { loadDashboard(); setInterval(loadDashboard, 15000); }
// ── DASHBOARD ─────────────────────────────────────────────────────────────────
// ── WORKERS ───────────────────────────────────────────────────────────────────
const CRON_DEFS = [
{id:'facts_collector', label:'Facts Collector', schedule:'Every 3 min', host:'jarvis-do'},
{id:'stats_cache', label:'Stats Cache', schedule:'Every 5 min', host:'jarvis-do'},
{id:'calendar_sync', label:'Calendar Sync', schedule:'Every 15 min', host:'jarvis-do'},
{id:'jarvis_deploy', label:'Deploy Runner', schedule:'Every 1 min', host:'jarvis-do'},
{id:'jarvis_watchdog', label:'Watchdog', schedule:'Every 5 min', host:'jarvis-do'},
{id:'jarvis_backup', label:'JARVIS Backup', schedule:'Daily 2am', host:'jarvis-do', norun:true},
{id:'do_server_backup',label:'DO Server Backup', schedule:'Weekly Sun 4am', host:'jarvis-do', norun:true},
];
function wBtn(col) {
const c={cyan:'var(--cyan)',red:'var(--red)',green:'var(--green)',dim:'var(--dim)'}[col]||'var(--dim)';
return `background:none;border:1px solid ${c};color:${c};padding:3px 8px;font-family:var(--font);font-size:0.55rem;letter-spacing:1px;cursor:pointer;border-radius:2px;margin-right:3px`;
}
function wAgo(ts) {
if (!ts) return '<span style="color:var(--red)">UNKNOWN</span>';
const d=new Date(ts.replace(' ','T')+(ts.includes('T')?'':'Z'));
const s=Math.floor((Date.now()-d)/1000);
if(isNaN(s)||s<0) return ts;
if(s<60) return s+'s ago';
if(s<3600) return Math.floor(s/60)+'m ago';
if(s<86400) return Math.floor(s/3600)+'h ago';
return Math.floor(s/86400)+'d ago';
}
function wToast(msg,err=false) {
let t=document.getElementById('w-toast');
if(!t){t=document.createElement('div');t.id='w-toast';
t.style.cssText='position:fixed;bottom:24px;right:24px;padding:10px 18px;border-radius:4px;font-size:0.65rem;letter-spacing:1px;z-index:9999;transition:opacity 0.5s';
document.body.appendChild(t);}
t.style.background=err?'rgba(255,34,68,0.12)':'rgba(0,212,255,0.12)';
t.style.border=err?'1px solid var(--red)':'1px solid var(--cyan)';
t.style.color=err?'var(--red)':'var(--cyan)';
t.style.opacity='1';t.textContent=msg;
setTimeout(()=>{t.style.opacity='0';},3000);
}
async function workerAction(type,id,action) {
const res=await api('worker_action',{worker_type:type,worker_id:id,action});
if(res&&res.ok){wToast(res.msg||'Done');setTimeout(loadWorkers,2500);}
else wToast((res&&res.error)||'Action failed',true);
}
async function loadWorkers() {
const d=await api('workers_list');
if(!d||d.error) return;
// Field Agents
const agTbody=document.getElementById('workers-agents');
if(!d.agents||!d.agents.length){
agTbody.innerHTML='<tr><td colspan="7" class="empty">NO AGENTS</td></tr>';
} else {
agTbody.innerHTML=d.agents.map(ag=>{
const on=ag.status==='online';
const dot=`<span class="dot ${on?'dot-green':'dot-red'}" style="margin-right:6px;vertical-align:middle"></span>`;
const caps=JSON.parse(ag.capabilities||'[]');
const capHtml=caps.map(c=>{
const col=c==='screenshot'?'var(--cyan)':c==='proxmox'?'var(--orange)':c==='docker'?'var(--green)':c==='ollama'?'var(--yellow)':'rgba(200,230,255,0.3)';
return `<span style="font-size:0.48rem;padding:1px 4px;border:1px solid ${col};color:${col};border-radius:2px;margin-right:2px;white-space:nowrap">${c.toUpperCase()}</span>`;
}).join('');
const shotBtn=caps.includes('screenshot')?`<button onclick="workerAction('agent','${ag.hostname}','screenshot')" style="${wBtn('dim')}">&#9670; SHOT</button>`:'';
return `<tr>
<td>${dot}<strong>${ag.hostname}</strong></td>
<td class="ts">${ag.agent_type||'linux'}</td>
<td class="ts">${ag.ip_address||'&mdash;'}</td>
<td>${dot}${on?'<span style="color:var(--green)">ONLINE</span>':'<span style="color:var(--red)">OFFLINE</span>'}</td>
<td>${capHtml||'<span class="ts">—</span>'}</td>
<td class="ts">${wAgo(ag.last_seen)}</td>
<td><button onclick="workerAction('agent','${ag.agent_id}','update')" style="${wBtn('cyan')}">UPDATE</button>${shotBtn}</td>
</tr>`;
}).join('');
}
// Cron Workers
const cl=d.cron_last||{};
document.getElementById('workers-crons').innerHTML=CRON_DEFS.map(c=>{
const runBtn=!c.norun?`<button onclick="workerAction('cron','${c.id}','run')" style="${wBtn('cyan')}">&#9654; RUN</button>`:'<span class="ts">&mdash;</span>';
return `<tr>
<td><strong>${c.label}</strong></td>
<td class="ts">${c.schedule}</td>
<td class="ts">${c.host}</td>
<td class="ts">${wAgo(cl[c.id])}</td>
<td>${runBtn}</td>
</tr>`;
}).join('');
// Daemons
const r=d.reactor,ron=r&&!r.error;
const rdot=`<span class="dot ${ron?'dot-green':'dot-red'}" style="margin-right:6px;vertical-align:middle"></span>`;
const rinfo=ron?`${r.handlers||'?'} handlers &nbsp;&middot;&nbsp; ${(d.arc_counts||{}).done||0} jobs done (24h) &nbsp;&middot;&nbsp; ${(d.arc_counts||{}).failed||0} failed`:'Not responding on :7474';
document.getElementById('workers-daemons').innerHTML=`<tr>
<td>${rdot}<strong>Arc Reactor</strong></td>
<td class="ts">jarvis-do :7474</td>
<td>${rdot}${ron?'<span style="color:var(--green)">ONLINE</span>':'<span style="color:var(--red)">OFFLINE</span>'}</td>
<td class="ts">${rinfo}</td>
<td><button onclick="workerAction('daemon','arc_reactor','restart')" style="${wBtn('red')}">&#8635; RESTART</button></td>
</tr>`;
}
async function loadDashboard() {
const d = await api('dashboard');
const s = d.sys;