diff --git a/AppImage/components/network-metrics.tsx b/AppImage/components/network-metrics.tsx index 8a86ece5..74cafd3e 100644 --- a/AppImage/components/network-metrics.tsx +++ b/AppImage/components/network-metrics.tsx @@ -142,8 +142,8 @@ export function NetworkMetrics() { error, isLoading, } = useSWR("/api/network", fetcher, { - refreshInterval: 53000, - revalidateOnFocus: false, + refreshInterval: 15000, + revalidateOnFocus: true, revalidateOnReconnect: true, }) diff --git a/AppImage/components/storage-overview.tsx b/AppImage/components/storage-overview.tsx index 17bb0f7c..ab2b3bc1 100644 --- a/AppImage/components/storage-overview.tsx +++ b/AppImage/components/storage-overview.tsx @@ -149,7 +149,7 @@ export function StorageOverview() { useEffect(() => { fetchStorageData() - const interval = setInterval(fetchStorageData, 60000) + const interval = setInterval(fetchStorageData, 30000) return () => clearInterval(interval) }, []) diff --git a/AppImage/components/virtual-machines.tsx b/AppImage/components/virtual-machines.tsx index faccabeb..3441c211 100644 --- a/AppImage/components/virtual-machines.tsx +++ b/AppImage/components/virtual-machines.tsx @@ -423,6 +423,36 @@ export function VirtualMachines() { } }, []) + // Keep the open modal's VM in sync with the 5s poll of /api/vms so CPU/RAM/I-O + // values don't stay frozen at click-time while the user has the modal open. + useEffect(() => { + if (!selectedVM || !vmData) return + const updated = vmData.find((v) => v.vmid === selectedVM.vmid) + if (!updated) return + // Avoid unnecessary setState when no field changed (reference-equal shortcut first). + if (updated === selectedVM) return + setSelectedVM(updated) + }, [vmData]) + + // Faster per-VM live status poll that only runs while the modal is open. + // SWR disables polling when the key is null, so this is truly scoped to the modal. + const { data: liveVMStatus } = useSWR( + selectedVM ? `/api/vms/${selectedVM.vmid}/status` : null, + fetcher, + { + refreshInterval: 2500, + revalidateOnFocus: true, + revalidateOnReconnect: true, + dedupingInterval: 1000, + }, + ) + + useEffect(() => { + if (!liveVMStatus || !selectedVM) return + if (liveVMStatus.vmid !== selectedVM.vmid) return + setSelectedVM((prev) => (prev ? { ...prev, ...liveVMStatus } : prev)) + }, [liveVMStatus]) + const handleVMClick = async (vm: VMData) => { setSelectedVM(vm) setCurrentView("main") diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py index ed6a088c..1264b932 100644 --- a/AppImage/scripts/flask_server.py +++ b/AppImage/scripts/flask_server.py @@ -8046,6 +8046,55 @@ def api_vms(): """Get virtual machine information""" return jsonify(get_proxmox_vms()) + +@app.route('/api/vms//status', methods=['GET']) +@require_auth +def api_vm_status(vmid): + """Lightweight per-VM live status: cpu, mem, disk, I/O counters, uptime. + + Designed to be polled every 2-3s from the detail modal while it's open. + Single pvesh call (~200-400ms local socket); returns the same shape as + /api/vms entries so the frontend can swap in-place. + """ + try: + local_node = get_proxmox_node_name() + + result = subprocess.run( + ['pvesh', 'get', f'/nodes/{local_node}/qemu/{vmid}/status/current', '--output-format', 'json'], + capture_output=True, text=True, timeout=10 + ) + vm_type = 'qemu' + if result.returncode != 0: + result = subprocess.run( + ['pvesh', 'get', f'/nodes/{local_node}/lxc/{vmid}/status/current', '--output-format', 'json'], + capture_output=True, text=True, timeout=10 + ) + vm_type = 'lxc' + + if result.returncode != 0: + return jsonify({'error': f'VM/LXC {vmid} not found'}), 404 + + data = json.loads(result.stdout) + return jsonify({ + 'vmid': vmid, + 'name': data.get('name', f'VM-{vmid}'), + 'status': data.get('status', 'unknown'), + 'type': vm_type if vm_type == 'lxc' else 'qemu', + 'cpu': data.get('cpu', 0), + 'mem': data.get('mem', 0), + 'maxmem': data.get('maxmem', 0), + 'disk': data.get('disk', 0), + 'maxdisk': data.get('maxdisk', 0), + 'uptime': data.get('uptime', 0), + 'netin': data.get('netin', 0), + 'netout': data.get('netout', 0), + 'diskread': data.get('diskread', 0), + 'diskwrite': data.get('diskwrite', 0), + }) + except Exception as e: + return jsonify({'error': str(e)}), 500 + + @app.route('/api/vms//metrics', methods=['GET']) @require_auth def api_vm_metrics(vmid): @@ -9064,9 +9113,10 @@ def api_prometheus(): metrics = [] timestamp = int(datetime.now().timestamp() * 1000) node = socket.gethostname() - - # Get system data - cpu_usage = psutil.cpu_percent(interval=0.5) + + # Non-blocking: returns %CPU since the last psutil call (sampler keeps state primed). + # Avoids 500ms worker block on each Prometheus scrape. + cpu_usage = psutil.cpu_percent(interval=0) memory = psutil.virtual_memory() load_avg = os.getloadavg() uptime_seconds = time.time() - psutil.boot_time()