From 71af9345a5b5cc8320683f35d4ac4e6760d87ca0 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Sun, 19 Oct 2025 16:51:52 +0200 Subject: [PATCH] Update AppImage --- AppImage/components/metrics-dialog.tsx | 244 +++++++++++ AppImage/components/virtual-machines.tsx | 530 ++++++++++++----------- AppImage/scripts/flask_server.py | 49 +++ 3 files changed, 561 insertions(+), 262 deletions(-) create mode 100644 AppImage/components/metrics-dialog.tsx diff --git a/AppImage/components/metrics-dialog.tsx b/AppImage/components/metrics-dialog.tsx new file mode 100644 index 0000000..a12d254 --- /dev/null +++ b/AppImage/components/metrics-dialog.tsx @@ -0,0 +1,244 @@ +"use client" + +import { useState, useEffect } from "react" +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { ArrowLeft, Loader2 } from "lucide-react" +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts" + +interface MetricsDialogProps { + open: boolean + onClose: () => void + vmid: number + vmName: string + vmType: "qemu" | "lxc" + metricType: "cpu" | "memory" | "network" | "disk" +} + +const TIMEFRAME_OPTIONS = [ + { value: "hour", label: "1 Hora" }, + { value: "day", label: "24 Horas" }, + { value: "week", label: "7 Días" }, + { value: "month", label: "30 Días" }, + { value: "year", label: "1 Año" }, +] + +const METRIC_TITLES = { + cpu: "Uso de CPU", + memory: "Uso de Memoria", + network: "Tráfico de Red", + disk: "I/O de Disco", +} + +export function MetricsDialog({ open, onClose, vmid, vmName, vmType, metricType }: MetricsDialogProps) { + const [timeframe, setTimeframe] = useState("week") + const [data, setData] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + if (open) { + fetchMetrics() + } + }, [open, vmid, timeframe]) + + const fetchMetrics = async () => { + setLoading(true) + setError(null) + + try { + const response = await fetch(`http://localhost:8008/api/vms/${vmid}/metrics?timeframe=${timeframe}`) + + if (!response.ok) { + throw new Error("Failed to fetch metrics") + } + + const result = await response.json() + + // Transform data for charts + const transformedData = result.data.map((item: any) => ({ + time: new Date(item.time * 1000).toLocaleString("es-ES", { + month: "short", + day: "numeric", + hour: timeframe === "hour" ? "2-digit" : undefined, + minute: timeframe === "hour" ? "2-digit" : undefined, + }), + timestamp: item.time, + cpu: item.cpu ? (item.cpu * 100).toFixed(2) : 0, + memory: item.mem ? ((item.mem / item.maxmem) * 100).toFixed(2) : 0, + memoryMB: item.mem ? (item.mem / 1024 / 1024).toFixed(0) : 0, + maxMemoryMB: item.maxmem ? (item.maxmem / 1024 / 1024).toFixed(0) : 0, + netin: item.netin ? (item.netin / 1024 / 1024).toFixed(2) : 0, + netout: item.netout ? (item.netout / 1024 / 1024).toFixed(2) : 0, + diskread: item.diskread ? (item.diskread / 1024 / 1024).toFixed(2) : 0, + diskwrite: item.diskwrite ? (item.diskwrite / 1024 / 1024).toFixed(2) : 0, + })) + + setData(transformedData) + } catch (err) { + console.error("[v0] Error fetching metrics:", err) + setError("Error al cargar las métricas") + } finally { + setLoading(false) + } + } + + const renderChart = () => { + if (loading) { + return ( +
+ +
+ ) + } + + if (error) { + return ( +
+

{error}

+
+ ) + } + + if (data.length === 0) { + return ( +
+

No hay datos disponibles

+
+ ) + } + + switch (metricType) { + case "cpu": + return ( + + + + + + + + + + + ) + + case "memory": + return ( + + + + + + + + + + + ) + + case "network": + return ( + + + + + + + + + + + + ) + + case "disk": + return ( + + + + + + + + + + + + ) + } + } + + return ( + + + {/* Fixed Header */} + +
+
+ +
+ + {METRIC_TITLES[metricType]} - {vmName} + +

+ VMID: {vmid} • Tipo: {vmType.toUpperCase()} +

+
+
+ +
+
+ + {/* Scrollable Content */} +
{renderChart()}
+
+
+ ) +} diff --git a/AppImage/components/virtual-machines.tsx b/AppImage/components/virtual-machines.tsx index b0a820d..c052002 100644 --- a/AppImage/components/virtual-machines.tsx +++ b/AppImage/components/virtual-machines.tsx @@ -678,8 +678,8 @@ export function VirtualMachines() { setVMDetails(null) }} > - - + +
@@ -699,288 +699,294 @@ export function VirtualMachines() { -
- {selectedVM && ( - <> -
-

- Basic Information -

-
-
-
Name
-
{selectedVM.name}
-
-
-
VMID
-
{selectedVM.vmid}
-
-
-
CPU Usage
-
- {(selectedVM.cpu * 100).toFixed(1)}% +
+
+ {selectedVM && ( + <> +
+

+ Basic Information +

+
+
+
Name
+
{selectedVM.name}
- -
-
-
Memory
-
- {(selectedVM.mem / 1024 ** 3).toFixed(1)} / {(selectedVM.maxmem / 1024 ** 3).toFixed(1)} GB +
+
VMID
+
{selectedVM.vmid}
- -
-
-
Disk
-
- {(selectedVM.disk / 1024 ** 3).toFixed(1)} / {(selectedVM.maxdisk / 1024 ** 3).toFixed(1)} GB -
- -
-
-
Uptime
-
{formatUptime(selectedVM.uptime)}
-
-
-
Disk I/O
-
-
- ↓ {formatBytes(selectedVM.diskread)} +
+
CPU Usage
+
+ {(selectedVM.cpu * 100).toFixed(1)}%
-
- ↑ {formatBytes(selectedVM.diskwrite)} + +
+
+
Memory
+
+ {(selectedVM.mem / 1024 ** 3).toFixed(1)} / {(selectedVM.maxmem / 1024 ** 3).toFixed(1)} GB +
+ +
+
+
Disk
+
+ {(selectedVM.disk / 1024 ** 3).toFixed(1)} / {(selectedVM.maxdisk / 1024 ** 3).toFixed(1)} GB +
+ +
+
+
Uptime
+
{formatUptime(selectedVM.uptime)}
+
+
+
Disk I/O
+
+
+ ↓ {formatBytes(selectedVM.diskread)} +
+
+ ↑ {formatBytes(selectedVM.diskwrite)} +
-
-
-
Network I/O
-
-
- ↓ {formatBytes(selectedVM.netin)} -
-
- ↑ {formatBytes(selectedVM.netout)} +
+
Network I/O
+
+
+ ↓ {formatBytes(selectedVM.netin)} +
+
+ ↑ {formatBytes(selectedVM.netout)} +
-
- {detailsLoading ? ( -
Loading configuration...
- ) : vmDetails?.config ? ( - <> -
-

- Resources -

-
- {vmDetails.config.cores && ( -
-
CPU Cores
-
{vmDetails.config.cores}
-
- )} - {vmDetails.config.sockets && ( -
-
CPU Sockets
-
{vmDetails.config.sockets}
-
- )} - {vmDetails.config.memory && ( -
-
Memory
-
{vmDetails.config.memory} MB
-
- )} - {vmDetails.config.swap && ( -
-
Swap
-
{vmDetails.config.swap} MB
-
- )} - {vmDetails.config.rootfs && ( -
-
Root Filesystem
-
- {vmDetails.config.rootfs} + {detailsLoading ? ( +
Loading configuration...
+ ) : vmDetails?.config ? ( + <> +
+

+ Resources +

+
+ {vmDetails.config.cores && ( +
+
CPU Cores
+
{vmDetails.config.cores}
-
- )} - {Object.keys(vmDetails.config) - .filter((key) => key.match(/^(scsi|sata|ide|virtio)\d+$/)) - .map((diskKey) => ( -
-
- {diskKey.toUpperCase().replace(/(\d+)/, " $1")} -
+ )} + {vmDetails.config.sockets && ( +
+
CPU Sockets
+
{vmDetails.config.sockets}
+
+ )} + {vmDetails.config.memory && ( +
+
Memory
+
{vmDetails.config.memory} MB
+
+ )} + {vmDetails.config.swap && ( +
+
Swap
+
{vmDetails.config.swap} MB
+
+ )} + {vmDetails.config.rootfs && ( +
+
Root Filesystem
- {vmDetails.config[diskKey]} + {vmDetails.config.rootfs}
- ))} + )} + {Object.keys(vmDetails.config) + .filter((key) => key.match(/^(scsi|sata|ide|virtio)\d+$/)) + .map((diskKey) => ( +
+
+ {diskKey.toUpperCase().replace(/(\d+)/, " $1")} +
+
+ {vmDetails.config[diskKey]} +
+
+ ))} +
-
-
-

- Network -

-
- {Object.keys(vmDetails.config) - .filter((key) => key.match(/^net\d+$/)) - .map((netKey) => ( -
-
- Network Interface {netKey.replace("net", "")} -
-
- {vmDetails.config[netKey]} +
+

+ Network +

+
+ {Object.keys(vmDetails.config) + .filter((key) => key.match(/^net\d+$/)) + .map((netKey) => ( +
+
+ Network Interface {netKey.replace("net", "")} +
+
+ {vmDetails.config[netKey]} +
+ ))} + {vmDetails.config.nameserver && ( +
+
DNS Nameserver
+
{vmDetails.config.nameserver}
- ))} - {vmDetails.config.nameserver && ( -
-
DNS Nameserver
-
{vmDetails.config.nameserver}
-
- )} - {vmDetails.config.searchdomain && ( -
-
Search Domain
-
{vmDetails.config.searchdomain}
-
- )} - {vmDetails.config.hostname && ( -
-
Hostname
-
{vmDetails.config.hostname}
-
- )} + )} + {vmDetails.config.searchdomain && ( +
+
Search Domain
+
{vmDetails.config.searchdomain}
+
+ )} + {vmDetails.config.hostname && ( +
+
Hostname
+
{vmDetails.config.hostname}
+
+ )} +
-
-
-

- Options -

-
- {vmDetails.config.onboot !== undefined && ( -
-
Start on Boot
- - {vmDetails.config.onboot ? "Yes" : "No"} - -
- )} - {vmDetails.config.unprivileged !== undefined && ( -
-
Unprivileged
- - {vmDetails.config.unprivileged ? "Yes" : "No"} - -
- )} - {vmDetails.config.ostype && ( -
-
OS Type
-
{vmDetails.config.ostype}
-
- )} - {vmDetails.config.arch && ( -
-
Architecture
-
{vmDetails.config.arch}
-
- )} - {vmDetails.config.boot && ( -
-
Boot Order
-
{vmDetails.config.boot}
-
- )} - {vmDetails.config.features && ( -
-
Features
-
{vmDetails.config.features}
-
- )} +
+

+ Options +

+
+ {vmDetails.config.onboot !== undefined && ( +
+
Start on Boot
+ + {vmDetails.config.onboot ? "Yes" : "No"} + +
+ )} + {vmDetails.config.unprivileged !== undefined && ( +
+
Unprivileged
+ + {vmDetails.config.unprivileged ? "Yes" : "No"} + +
+ )} + {vmDetails.config.ostype && ( +
+
OS Type
+
{vmDetails.config.ostype}
+
+ )} + {vmDetails.config.arch && ( +
+
Architecture
+
{vmDetails.config.arch}
+
+ )} + {vmDetails.config.boot && ( +
+
Boot Order
+
{vmDetails.config.boot}
+
+ )} + {vmDetails.config.features && ( +
+
Features
+
{vmDetails.config.features}
+
+ )} +
-
- - ) : null} + + ) : null} + + )} +
+
-
-

- Control Actions -

-
- - - - -
-
- - )} +
+

+ Control Actions +

+
+ + + + + +
diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py index b6f0276..9714503 100644 --- a/AppImage/scripts/flask_server.py +++ b/AppImage/scripts/flask_server.py @@ -3752,6 +3752,54 @@ def api_vms(): """Get virtual machine information""" return jsonify(get_proxmox_vms()) +@app.route('/api/vms//metrics', methods=['GET']) +def api_vm_metrics(vmid): + """Get historical metrics (RRD data) for a specific VM/LXC""" + try: + timeframe = request.args.get('timeframe', 'week') # hour, day, week, month, year + + # Validate timeframe + valid_timeframes = ['hour', 'day', 'week', 'month', 'year'] + if timeframe not in valid_timeframes: + return jsonify({'error': f'Invalid timeframe. Must be one of: {", ".join(valid_timeframes)}'}), 400 + + # Get local node name + local_node = socket.gethostname() + + # First, determine if it's a qemu VM or lxc container + 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: + # Try LXC + result = subprocess.run(['pvesh', 'get', f'/nodes/{local_node}/lxc/{vmid}/status/current', '--output-format', 'json'], + capture_output=True, text=True, timeout=10) + if result.returncode == 0: + vm_type = 'lxc' + else: + return jsonify({'error': f'VM/LXC {vmid} not found'}), 404 + + # Get RRD data + rrd_result = subprocess.run(['pvesh', 'get', f'/nodes/{local_node}/{vm_type}/{vmid}/rrddata', + '--timeframe', timeframe, '--output-format', 'json'], + capture_output=True, text=True, timeout=10) + + if rrd_result.returncode == 0: + rrd_data = json.loads(rrd_result.stdout) + return jsonify({ + 'vmid': vmid, + 'type': vm_type, + 'timeframe': timeframe, + 'data': rrd_data + }) + else: + return jsonify({'error': f'Failed to get RRD data: {rrd_result.stderr}'}), 500 + + except Exception as e: + print(f"Error getting VM metrics: {e}") + return jsonify({'error': str(e)}), 500 + @app.route('/api/logs', methods=['GET']) def api_logs(): """Get system logs""" @@ -4619,6 +4667,7 @@ def api_info(): '/api/proxmox-storage', '/api/network', '/api/vms', + '/api/vms//metrics', # Added endpoint for RRD data '/api/logs', '/api/health', '/api/hardware',