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:
2026-06-12 01:44:59 +00:00
parent 4c67efe715
commit 0469b31829
11 changed files with 49 additions and 19 deletions
+1 -1
View File
@@ -291,7 +291,7 @@ def register(cfg: dict, state: dict) -> str:
result = api_post( result = api_post(
f"{cfg['jarvis_url']}/api/agent/register", f"{cfg['jarvis_url']}/api/agent/register",
{"hostname": hostname, "agent_type": agent_type, "ip_address": ip, {"hostname": hostname, "version": AGENT_VERSION, "agent_type": agent_type, "ip_address": ip,
"capabilities": capabilities, "agent_id": agent_id}, "capabilities": capabilities, "agent_id": agent_id},
headers={"X-Registration-Key": cfg["registration_key"]}, headers={"X-Registration-Key": cfg["registration_key"]},
ssl_verify=ssl_verify, ssl_verify=ssl_verify,
+1 -1
View File
@@ -241,7 +241,7 @@ def register(cfg: dict, state: dict) -> str:
result = api_post( result = api_post(
f"{cfg['jarvis_url']}/api/agent/register", f"{cfg['jarvis_url']}/api/agent/register",
{"hostname": hostname, "agent_type": agent_type, "ip_address": ip, {"hostname": hostname, "version": AGENT_VERSION, "agent_type": agent_type, "ip_address": ip,
"capabilities": capabilities, "agent_id": agent_id}, "capabilities": capabilities, "agent_id": agent_id},
headers={"X-Registration-Key": cfg["registration_key"]}, headers={"X-Registration-Key": cfg["registration_key"]},
ssl_verify=ssl_verify, ssl_verify=ssl_verify,
+1
View File
@@ -136,6 +136,7 @@ def register(cfg: dict, state: dict) -> str:
f"{cfg['jarvis_url']}/api/agent/register", f"{cfg['jarvis_url']}/api/agent/register",
{ {
"hostname": hostname, "hostname": hostname,
"version": AGENT_VERSION,
"agent_type": agent_type, "agent_type": agent_type,
"ip_address": ip, "ip_address": ip,
"capabilities": capabilities, "capabilities": capabilities,
+6 -5
View File
@@ -79,23 +79,24 @@ switch ($agentAction) {
$ipAddress = $data['ip_address'] ?? ($_SERVER['REMOTE_ADDR'] ?? ''); $ipAddress = $data['ip_address'] ?? ($_SERVER['REMOTE_ADDR'] ?? '');
$capabilities = $data['capabilities'] ?? []; $capabilities = $data['capabilities'] ?? [];
$agentId = $data['agent_id'] ?? ($hostname . '_' . substr(md5($hostname . $ipAddress), 0, 8)); $agentId = $data['agent_id'] ?? ($hostname . '_' . substr(md5($hostname . $ipAddress), 0, 8));
$version = trim($data['version'] ?? '');
if (!$hostname) agent_error(400, 'hostname required'); if (!$hostname) agent_error(400, 'hostname required');
if (!in_array($agentType, ['linux', 'homeassistant', 'proxmox', 'windows'])) agent_error(400, 'Invalid agent_type'); if (!in_array($agentType, ['linux', 'homeassistant', 'proxmox', 'windows', 'macos'])) agent_error(400, 'Invalid agent_type');
// Upsert agent // Upsert agent
$existing = JarvisDB::query('SELECT api_key FROM registered_agents WHERE agent_id = ?', [$agentId]); $existing = JarvisDB::query('SELECT api_key FROM registered_agents WHERE agent_id = ?', [$agentId]);
if ($existing) { if ($existing) {
$apiKey = $existing[0]['api_key']; $apiKey = $existing[0]['api_key'];
JarvisDB::query( JarvisDB::query(
'UPDATE registered_agents SET hostname=?, agent_type=?, ip_address=?, capabilities=?, last_seen=NOW(), status="online" WHERE agent_id=?', 'UPDATE registered_agents SET hostname=?, agent_type=?, ip_address=?, capabilities=?, version=?, last_seen=NOW(), status="online" WHERE agent_id=?',
[$hostname, $agentType, $ipAddress, json_encode($capabilities), $agentId] [$hostname, $agentType, $ipAddress, json_encode($capabilities), $version ?: null, $agentId]
); );
} else { } else {
$apiKey = generate_api_key(); $apiKey = generate_api_key();
JarvisDB::query( JarvisDB::query(
'INSERT INTO registered_agents (agent_id, hostname, agent_type, ip_address, api_key, capabilities, last_seen, status) VALUES (?,?,?,?,?,?,NOW(),"online")', 'INSERT INTO registered_agents (agent_id, hostname, agent_type, ip_address, api_key, capabilities, version, last_seen, status) VALUES (?,?,?,?,?,?,?,NOW(),"online")',
[$agentId, $hostname, $agentType, $ipAddress, $apiKey, json_encode($capabilities)] [$agentId, $hostname, $agentType, $ipAddress, $apiKey, json_encode($capabilities), $version ?: null]
); );
} }
+34 -7
View File
@@ -467,9 +467,17 @@ if ($action) {
case 'workers_list': case 'workers_list':
$agents = JarvisDB::query( $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' 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'); $reactorRaw = @file_get_contents('http://127.0.0.1:7474/status');
$reactor = $reactorRaw ? json_decode($reactorRaw, true) : null; $reactor = $reactorRaw ? json_decode($reactorRaw, true) : null;
$arcStats = JarvisDB::query( $arcStats = JarvisDB::query(
@@ -506,7 +514,7 @@ if ($action) {
} }
$doLog = '/var/log/do-server-backup.log'; $doLog = '/var/log/do-server-backup.log';
if (file_exists($doLog)) $cronLast['do_server_backup'] = date('Y-m-d H:i:s', filemtime($doLog)); 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; break;
case 'worker_action': case 'worker_action':
@@ -1343,8 +1351,8 @@ select.filter-sel:focus{border-color:var(--cyan)}
</div> </div>
<div class="page-title" style="font-size:0.7rem;margin-top:0;border:none;padding-bottom:4px">FIELD AGENTS</div> <div class="page-title" style="font-size:0.7rem;margin-top:0;border:none;padding-bottom:4px">FIELD AGENTS</div>
<table><thead><tr> <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> <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="7" class="loading">LOADING...</td></tr></tbody></table> </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> <div class="page-title" style="font-size:0.7rem;margin-top:24px;border:none;padding-bottom:4px">CRON WORKERS</div>
<table><thead><tr> <table><thead><tr>
<th>WORKER</th><th>SCHEDULE</th><th>HOST</th><th>LAST RUN</th><th>ACTIONS</th> <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() { async function loadWorkers() {
const d=await api('workers_list'); const d=await api('workers_list');
if(!d||d.error) return; if(!d||d.error) return;
const latestVer = d.latest_versions || {};
// Field Agents // Field Agents
const agTbody=document.getElementById('workers-agents'); const agTbody=document.getElementById('workers-agents');
if(!d.agents||!d.agents.length){ 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 { } else {
agTbody.innerHTML=d.agents.map(ag=>{ agTbody.innerHTML=d.agents.map(ag=>{
const on=ag.status==='online'; 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>`; 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(''); }).join('');
const shotBtn=caps.includes('screenshot')?`<button onclick="workerAction('agent','${ag.hostname}','screenshot')" style="${wBtn('dim')}">&#9670; SHOT</button>`:''; const shotBtn=caps.includes('screenshot')?`<button onclick="workerAction('agent','${ag.hostname}','screenshot')" style="${wBtn('dim')}">&#9670; 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>${dot}<strong>${ag.hostname}</strong></td>
<td class="ts">${ag.agent_type||'linux'}</td> <td class="ts">${ag.agent_type||'linux'}</td>
<td class="ts">${ag.ip_address||'&mdash;'}</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>${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>${capHtml||'<span class="ts">—</span>'}</td>
<td class="ts">${wAgo(ag.last_seen)}</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>`; </tr>`;
}).join(''); }).join('');
} }
+1 -1
View File
@@ -291,7 +291,7 @@ def register(cfg: dict, state: dict) -> str:
result = api_post( result = api_post(
f"{cfg['jarvis_url']}/api/agent/register", f"{cfg['jarvis_url']}/api/agent/register",
{"hostname": hostname, "agent_type": agent_type, "ip_address": ip, {"hostname": hostname, "version": AGENT_VERSION, "agent_type": agent_type, "ip_address": ip,
"capabilities": capabilities, "agent_id": agent_id}, "capabilities": capabilities, "agent_id": agent_id},
headers={"X-Registration-Key": cfg["registration_key"]}, headers={"X-Registration-Key": cfg["registration_key"]},
ssl_verify=ssl_verify, ssl_verify=ssl_verify,
+1 -1
View File
@@ -1 +1 @@
d0aeaab1686f788c9d46ffeaac57a42e3e82b80c2d7fac2be443c05cf70c87be jarvis-agent-mac.py 6a0cb7a876f0d4ba36c5f7aaf6a80d6224b52d6e23ac32d6cd9f8c03ca9d8062 jarvis-agent-mac.py
+1 -1
View File
@@ -241,7 +241,7 @@ def register(cfg: dict, state: dict) -> str:
result = api_post( result = api_post(
f"{cfg['jarvis_url']}/api/agent/register", f"{cfg['jarvis_url']}/api/agent/register",
{"hostname": hostname, "agent_type": agent_type, "ip_address": ip, {"hostname": hostname, "version": AGENT_VERSION, "agent_type": agent_type, "ip_address": ip,
"capabilities": capabilities, "agent_id": agent_id}, "capabilities": capabilities, "agent_id": agent_id},
headers={"X-Registration-Key": cfg["registration_key"]}, headers={"X-Registration-Key": cfg["registration_key"]},
ssl_verify=ssl_verify, ssl_verify=ssl_verify,
@@ -1 +1 @@
feadcb033426838f0d9e87df933fc3cd6b09b0d74eea7dc697b29a12421a1f2d jarvis-agent-windows.py 1232a5ffa6dda93fca04878952d26e889fde5db479b9f23ea35111be2c3f77f5 jarvis-agent-windows.py
+1
View File
@@ -136,6 +136,7 @@ def register(cfg: dict, state: dict) -> str:
f"{cfg['jarvis_url']}/api/agent/register", f"{cfg['jarvis_url']}/api/agent/register",
{ {
"hostname": hostname, "hostname": hostname,
"version": AGENT_VERSION,
"agent_type": agent_type, "agent_type": agent_type,
"ip_address": ip, "ip_address": ip,
"capabilities": capabilities, "capabilities": capabilities,
+1 -1
View File
@@ -1 +1 @@
bccfeed43d7fbcd5f002656e866432b8aaa5035bc797f855727f50147f609427 jarvis-agent.py 1a9e8e24e5aee8f27a5900b6340373023ff2171e844e71e451eecdbf3b2b0f03 jarvis-agent.py