From cbd63f1a1e7be236a7ac971f80340d0d9d0455fc Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Sat, 30 May 2026 04:40:34 +0000 Subject: [PATCH] Proxmox VMs: full resources from cluster API (both nodes), CPU/RAM/disk/uptime/network per VM --- api/endpoints/stats_cache.php | 92 +++++++++++++++++++++++------------ public_html/admin/index.php | 80 +++++++++++++++++++++--------- 2 files changed, 119 insertions(+), 53 deletions(-) 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||'—'}
`:''} - - ${rows}
VMIDNAMETYPESTATUSCPUMEMUPTIME
`; + 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 `
+
+
+
+ ${pct}% +
`; + } + + // 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 = + ` + ${html}
VMIDNAMETYPESTATUSCPURAMDISKUPTIMENETWORK
`; } // ── AUTO-LOGIN CHECK (PHP session) ───────────────────────────────────────────