diff --git a/api/endpoints/stats_cache.php b/api/endpoints/stats_cache.php
index f536708..f8c37b4 100644
--- a/api/endpoints/stats_cache.php
+++ b/api/endpoints/stats_cache.php
@@ -39,39 +39,70 @@ if (PROXMOX_HOST !== '10.48.200.X' && PROXMOX_TOKEN_VAL !== 'YOUR_TOKEN_VALUE_HE
$pveBase = 'https://orbisne.fortiddns.com:' . PROXMOX_PORT . '/api2/json';
$pveAuth = ['Authorization: PVEAPIToken=' . PROXMOX_USER . '!' . PROXMOX_TOKEN_ID . '=' . PROXMOX_TOKEN_VAL];
+ // Cluster resources API — returns all VMs/CTs from ALL nodes (pve + pve2)
+ $clusterRaw = curlGet("$pveBase/cluster/resources?type=vm", $pveAuth);
$nodeStatusRaw = curlGet("$pveBase/nodes/" . PROXMOX_NODE . "/status", $pveAuth);
- $vmsRaw = curlGet("$pveBase/nodes/" . PROXMOX_NODE . "/qemu", $pveAuth);
- $lxcRaw = curlGet("$pveBase/nodes/" . PROXMOX_NODE . "/lxc", $pveAuth);
+ $nodeStatus = $nodeStatusRaw ? (json_decode($nodeStatusRaw, true)['data'] ?? null) : null;
- $nodeStatus = $nodeStatusRaw ? (json_decode($nodeStatusRaw, true)['data'] ?? null) : null;
- $vms = $vmsRaw ? (json_decode($vmsRaw, true)['data'] ?? []) : [];
- $lxcs = $lxcRaw ? (json_decode($lxcRaw, true)['data'] ?? []) : [];
+ $allResources = $clusterRaw ? (json_decode($clusterRaw, true)['data'] ?? []) : [];
- $vmDetails = [];
- foreach ($vms as $vm) {
- $vmDetails[] = [
- 'vmid' => $vm['vmid'],
- 'name' => $vm['name'] ?? 'VM-' . $vm['vmid'],
- 'status' => $vm['status'] ?? 'unknown',
- 'cpu' => round(($vm['cpu'] ?? 0) * 100, 1),
- 'mem_mb' => round(($vm['mem'] ?? 0) / 1048576),
- 'maxmem_mb' => round(($vm['maxmem'] ?? 0) / 1048576),
- 'disk_gb' => round(($vm['disk'] ?? 0) / 1073741824, 1),
- 'uptime' => $vm['uptime'] ?? 0,
- 'netin' => $vm['netin'] ?? 0,
- 'netout' => $vm['netout'] ?? 0,
- ];
+ function fmtUptime(int $sec): string {
+ $d = intdiv($sec, 86400); $h = intdiv($sec % 86400, 3600); $m = intdiv($sec % 3600, 60);
+ return ($d > 0 ? "{$d}d " : '') . "{$h}h {$m}m";
}
- $lxcDetails = [];
- foreach ($lxcs as $lxc) {
- $lxcDetails[] = [
- 'vmid' => $lxc['vmid'],
- 'name' => $lxc['name'] ?? 'CT-' . $lxc['vmid'],
- 'status' => $lxc['status'] ?? 'unknown',
- 'cpu' => round(($lxc['cpu'] ?? 0) * 100, 1),
- 'mem_mb' => round(($lxc['mem'] ?? 0) / 1048576),
- 'maxmem_mb' => round(($lxc['maxmem'] ?? 0) / 1048576),
- 'type' => 'lxc',
+
+ function fmtBytes(int $b): string {
+ if ($b >= 1073741824) return round($b/1073741824, 1) . ' GB';
+ if ($b >= 1048576) return round($b/1048576, 1) . ' MB';
+ return $b . ' B';
+ }
+
+ $vmDetails = []; $lxcDetails = [];
+ foreach ($allResources as $r) {
+ $memPct = ($r['maxmem'] ?? 0) > 0 ? round($r['mem'] / $r['maxmem'] * 100, 1) : 0;
+ $diskGb = round(($r['maxdisk'] ?? 0) / 1073741824, 1);
+ $cpuPct = round(($r['cpu'] ?? 0) * 100, 1);
+ $upSec = (int)($r['uptime'] ?? 0);
+ $entry = [
+ 'vmid' => $r['vmid'],
+ 'name' => $r['name'] ?? ($r['type'] === 'lxc' ? 'CT-' : 'VM-') . $r['vmid'],
+ 'node' => $r['node'] ?? 'pve',
+ 'type' => $r['type'] ?? 'qemu',
+ 'status' => $r['status'] ?? 'unknown',
+ 'cpu_pct' => $cpuPct,
+ 'cpus' => $r['maxcpu'] ?? 1,
+ 'mem_pct' => $memPct,
+ 'mem_used_mb' => round(($r['mem'] ?? 0) / 1048576),
+ 'mem_total_mb' => round(($r['maxmem'] ?? 0) / 1048576),
+ 'disk_gb' => $diskGb,
+ 'uptime_s' => $upSec,
+ 'uptime_human' => $upSec > 0 ? fmtUptime($upSec) : '—',
+ 'netin' => $r['netin'] ?? 0,
+ 'netout' => $r['netout'] ?? 0,
+ 'netin_fmt' => fmtBytes((int)($r['netin'] ?? 0)),
+ 'netout_fmt' => fmtBytes((int)($r['netout'] ?? 0)),
+ ];
+ if ($r['type'] === 'lxc') $lxcDetails[] = $entry;
+ else $vmDetails[] = $entry;
+ }
+
+ // Sort by node then vmid
+ usort($vmDetails, fn($a,$b) => $a['node'] <=> $b['node'] ?: $a['vmid'] <=> $b['vmid']);
+ usort($lxcDetails, fn($a,$b) => $a['node'] <=> $b['node'] ?: $a['vmid'] <=> $b['vmid']);
+
+ // Node summary for both nodes
+ $nodeInfo = [];
+ if ($nodeStatus) {
+ $ns = $nodeStatus;
+ $nodeInfo['pve'] = [
+ 'cpu_pct' => round(($ns['cpu'] ?? 0) * 100, 1),
+ 'mem_pct' => ($ns['memory']['total'] ?? 0) > 0
+ ? round($ns['memory']['used'] / $ns['memory']['total'] * 100, 1) : 0,
+ 'mem_used_gb' => round(($ns['memory']['used'] ?? 0) / 1073741824, 1),
+ 'mem_total_gb' => round(($ns['memory']['total'] ?? 0) / 1073741824, 1),
+ 'uptime' => fmtUptime((int)($ns['uptime'] ?? 0)),
+ 'disk_used_gb' => round(($ns['rootfs']['used'] ?? 0) / 1073741824, 1),
+ 'disk_total_gb'=> round(($ns['rootfs']['total'] ?? 0) / 1073741824, 1),
];
}
@@ -79,13 +110,14 @@ if (PROXMOX_HOST !== '10.48.200.X' && PROXMOX_TOKEN_VAL !== 'YOUR_TOKEN_VALUE_HE
'configured' => true,
'node' => PROXMOX_NODE,
'node_status' => $nodeStatus,
+ 'node_info' => $nodeInfo,
'vms' => $vmDetails,
'containers' => $lxcDetails,
'vm_count' => count($vmDetails),
'ct_count' => count($lxcDetails),
'cached_at' => date('c'),
]);
- echo '[cache] Proxmox: ' . count($vmDetails) . ' VMs, ' . count($lxcDetails) . " CTs cached\n";
+ echo '[cache] Proxmox: ' . count($vmDetails) . ' VMs, ' . count($lxcDetails) . " CTs cached (both nodes)\n";
}
// ── Home Assistant ────────────────────────────────────────────────────────
diff --git a/public_html/admin/index.php b/public_html/admin/index.php
index bc26038..327df95 100644
--- a/public_html/admin/index.php
+++ b/public_html/admin/index.php
@@ -285,10 +285,9 @@ if ($action) {
// ── 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'=>[],'ts'=>null]);
+ if (!$raw) j(['vms'=>[],'containers'=>[],'node_info'=>[],'ts'=>null]);
$pve = json_decode($raw['data'],true) ?? [];
- $vms = array_merge($pve['vms']??[], $pve['containers']??[]);
- j(['vms'=>$vms,'node_status'=>$pve['node_status']??null,'ts'=>$raw['ts']]);
+ j(['vms'=>$pve['vms']??[],'containers'=>$pve['containers']??[],'node_info'=>$pve['node_info']??[],'node_status'=>$pve['node_status']??null,'ts'=>$raw['ts']]);
// ── USERS ────────────────────────────────────────────────────────────
case 'users_list':
@@ -1207,26 +1206,61 @@ function newsCustomModal(id=0, title='', url='') {
async function loadVMs() {
document.getElementById('vms-tbl').innerHTML='
LOADING...
';
const data = await api('vms_list');
- const vms = data.vms||[];
- if (!vms.length) { document.getElementById('vms-tbl').innerHTML='NO VM DATA (Proxmox cache may be empty)
'; return; }
- const ns = data.node_status||{};
- let rows = vms.map(v => {
- const run = v.status==='running';
- const cpu = v.cpu_pct!=null?Math.round(v.cpu_pct)+'%':'—';
- const mem = v.mem_pct!=null?Math.round(v.mem_pct)+'%':'—';
- return `
- | ${v.vmid||'—'} |
- ${esc(v.name||'—')} |
- ${esc(v.type||'vm').toUpperCase()} |
- ${run?'RUNNING':''+esc(v.status).toUpperCase()+''} |
- ${cpu} | ${mem} |
- ${v.uptime_human||'—'} |
-
`;
- }).join('');
- document.getElementById('vms-tbl').innerHTML = `
- ${ns.cpu_pct!=null?`PVE NODE — CPU: ${Math.round(ns.cpu_pct)}% · RAM: ${Math.round(ns.mem_pct||0)}% · UPTIME: ${ns.uptime||'—'}
`:''}
- | VMID | NAME | TYPE | STATUS | CPU | MEM | UPTIME |
- ${rows}
`;
+ const vms = [...(data.vms||[]), ...(data.containers||[])];
+ if (!vms.length) { document.getElementById('vms-tbl').innerHTML='NO VM DATA — Proxmox cache empty, refreshes every 5 min
'; return; }
+
+ const ni = data.node_info||{};
+ function nodeBar(info) {
+ if (!info) return '';
+ const cc = info.cpu_pct>80?'var(--red)':info.cpu_pct>60?'var(--yellow)':'var(--green)';
+ const mc = info.mem_pct>80?'var(--red)':info.mem_pct>60?'var(--yellow)':'var(--cyan)';
+ return `CPU ${info.cpu_pct}% · `+
+ `RAM ${info.mem_used_gb}/${info.mem_total_gb}GB (${info.mem_pct}%) · `+
+ `Disk ${info.disk_used_gb}/${info.disk_total_gb}GB · Up ${info.uptime}`;
+ }
+
+ function meter(pct, warn=70, crit=85) {
+ if (pct == null) return '—';
+ const c = pct>=crit?'var(--red)':pct>=warn?'var(--yellow)':'var(--green)';
+ return ``;
+ }
+
+ // Group by node
+ const nodes = [...new Set(vms.map(v=>v.node||'pve'))].sort();
+ let html = '';
+ for (const node of nodes) {
+ const nodeVMs = vms.filter(v=>(v.node||'pve')===node);
+ const info = ni[node];
+ html += ``+
+ `${node.toUpperCase()} NODE${info?` — ${nodeBar(info)}`:''}
`;
+ html += nodeVMs.map(v => {
+ const run = v.status==='running';
+ const typeColor = v.type==='lxc'?'var(--orange)':'var(--cyan)';
+ const memLabel = v.mem_used_mb && v.mem_total_mb
+ ? `${Math.round(v.mem_used_mb/1024*10)/10}/${Math.round(v.mem_total_mb/1024*10)/10}GB`
+ : '—';
+ return `
+ | ${v.vmid} |
+ ${esc(v.name)} |
+ ${(v.type||'qemu').toUpperCase()} |
+ ${run?'RUNNING':''+esc(v.status||'stopped').toUpperCase()+''} |
+ ${meter(v.cpu_pct,50,80)} ${v.cpus||1}vCPU |
+ ${meter(v.mem_pct)} ${memLabel} |
+ ${v.disk_gb||'—'}GB |
+ ${run?(v.uptime_human||'—'):'—'} |
+ ↓${v.netin_fmt||'—'} ↑${v.netout_fmt||'—'} |
+
`;
+ }).join('');
+ }
+
+ document.getElementById('vms-tbl').innerHTML =
+ `| VMID | NAME | TYPE | STATUS | CPU | RAM | DISK | UPTIME | NETWORK |
+ ${html}
`;
}
// ── AUTO-LOGIN CHECK (PHP session) ───────────────────────────────────────────