mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
Agent version tracking — workers tab shows current vs latest version
- Add version column to registered_agents table - Agents send version on registration (Linux 3.1, Windows 3.0, macOS 3.0) - workers_list API returns latest_versions per platform - Workers tab: VERSION column with green check (up-to-date) or red (outdated) - Outdated agents highlight row and show blue UPDATE button - Up-to-date agents show dimmed UPDATE button - Update button dispatches update command immediately Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -467,9 +467,17 @@ if ($action) {
|
||||
|
||||
case 'workers_list':
|
||||
$agents = JarvisDB::query(
|
||||
'SELECT agent_id, hostname, agent_type, ip_address, status, capabilities, last_seen
|
||||
'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(
|
||||
@@ -506,7 +514,7 @@ if ($action) {
|
||||
}
|
||||
$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]);
|
||||
j(['agents'=>$agents,'reactor'=>$reactor,'arc_counts'=>$arcCounts,'cron_last'=>$cronLast,'latest_versions'=>$latestVersions]);
|
||||
break;
|
||||
|
||||
case 'worker_action':
|
||||
@@ -1343,8 +1351,8 @@ select.filter-sel:focus{border-color:var(--cyan)}
|
||||
</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>
|
||||
<th>HOSTNAME</th><th>TYPE</th><th>IP</th><th>STATUS</th><th>VERSION</th><th>CAPABILITIES</th><th>LAST SEEN</th><th>ACTIONS</th>
|
||||
</tr></thead><tbody id="workers-agents"><tr><td colspan="8" 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>
|
||||
@@ -2171,10 +2179,11 @@ async function workerAction(type,id,action) {
|
||||
async function loadWorkers() {
|
||||
const d=await api('workers_list');
|
||||
if(!d||d.error) return;
|
||||
const latestVer = d.latest_versions || {};
|
||||
// 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>';
|
||||
agTbody.innerHTML='<tr><td colspan="8" class="empty">NO AGENTS</td></tr>';
|
||||
} else {
|
||||
agTbody.innerHTML=d.agents.map(ag=>{
|
||||
const on=ag.status==='online';
|
||||
@@ -2185,14 +2194,32 @@ async function loadWorkers() {
|
||||
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')}">◆ SHOT</button>`:'';
|
||||
return `<tr>
|
||||
// Version column
|
||||
const curVer = ag.version || null;
|
||||
const latVer = latestVer[ag.agent_type] || null;
|
||||
let verHtml;
|
||||
if (!latVer) {
|
||||
verHtml = '<span class="ts">—</span>';
|
||||
} else if (!curVer) {
|
||||
verHtml = `<span style="color:var(--yellow);font-size:0.62rem;font-family:var(--mono)">? / ${latVer}</span>`;
|
||||
} else if (curVer === latVer) {
|
||||
verHtml = `<span style="color:var(--green);font-size:0.62rem;font-family:var(--mono)">v${curVer} ✓</span>`;
|
||||
} else {
|
||||
verHtml = `<span style="color:var(--red);font-size:0.62rem;font-family:var(--mono)">v${curVer}</span><span class="ts"> → v${latVer}</span>`;
|
||||
}
|
||||
const needsUpdate = latVer && curVer !== latVer;
|
||||
const updBtn = caps.includes('commands')
|
||||
? `<button onclick="workerAction('agent','${ag.agent_id}','update')" style="${wBtn(needsUpdate?'cyan':'dim')}">${needsUpdate?'⬆ UPDATE':'↻ UPDATE'}</button>`
|
||||
: '';
|
||||
return `<tr${needsUpdate?' style="background:rgba(0,212,255,0.04)"':''}>
|
||||
<td>${dot}<strong>${ag.hostname}</strong></td>
|
||||
<td class="ts">${ag.agent_type||'linux'}</td>
|
||||
<td class="ts">${ag.ip_address||'—'}</td>
|
||||
<td>${dot}${on?'<span style="color:var(--green)">ONLINE</span>':'<span style="color:var(--red)">OFFLINE</span>'}</td>
|
||||
<td>${verHtml}</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>
|
||||
<td>${updBtn}${shotBtn}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user