mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-10-11 12:26:18 +00:00
Update AppImage
This commit is contained in:
@@ -4,18 +4,23 @@ import { useState, useEffect } from "react"
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
|
||||||
import { Badge } from "./ui/badge"
|
import { Badge } from "./ui/badge"
|
||||||
import { Progress } from "./ui/progress"
|
import { Progress } from "./ui/progress"
|
||||||
import { Server, Play, Square, Monitor, Cpu, MemoryStick, AlertCircle } from "lucide-react"
|
import { Server, Play, Square, Monitor, Cpu, MemoryStick, AlertCircle, HardDrive, Network } from "lucide-react"
|
||||||
|
|
||||||
interface VMData {
|
interface VMData {
|
||||||
vmid: number
|
vmid: number
|
||||||
name: string
|
name: string
|
||||||
status: string
|
status: string
|
||||||
|
type: string // Added type field to distinguish VM from LXC
|
||||||
cpu: number
|
cpu: number
|
||||||
mem: number
|
mem: number
|
||||||
maxmem: number
|
maxmem: number
|
||||||
disk: number
|
disk: number
|
||||||
maxdisk: number
|
maxdisk: number
|
||||||
uptime: number
|
uptime: number
|
||||||
|
netin?: number // Added network in
|
||||||
|
netout?: number // Added network out
|
||||||
|
diskread?: number // Added disk read
|
||||||
|
diskwrite?: number // Added disk write
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchVMData = async (): Promise<VMData[]> => {
|
const fetchVMData = async (): Promise<VMData[]> => {
|
||||||
@@ -42,6 +47,14 @@ const fetchVMData = async (): Promise<VMData[]> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatBytes = (bytes: number | undefined): string => {
|
||||||
|
if (!bytes || bytes === 0) return "0 B"
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ["B", "KB", "MB", "GB", "TB"]
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`
|
||||||
|
}
|
||||||
|
|
||||||
export function VirtualMachines() {
|
export function VirtualMachines() {
|
||||||
const [vmData, setVmData] = useState<VMData[]>([])
|
const [vmData, setVmData] = useState<VMData[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
@@ -123,6 +136,13 @@ export function VirtualMachines() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getTypeBadge = (type: string) => {
|
||||||
|
if (type === "lxc") {
|
||||||
|
return { color: "bg-cyan-500/10 text-cyan-500 border-cyan-500/20", label: "LXC" }
|
||||||
|
}
|
||||||
|
return { color: "bg-purple-500/10 text-purple-500 border-purple-500/20", label: "VM" }
|
||||||
|
}
|
||||||
|
|
||||||
const formatUptime = (seconds: number) => {
|
const formatUptime = (seconds: number) => {
|
||||||
const days = Math.floor(seconds / 86400)
|
const days = Math.floor(seconds / 86400)
|
||||||
const hours = Math.floor((seconds % 86400) / 3600)
|
const hours = Math.floor((seconds % 86400) / 3600)
|
||||||
@@ -159,7 +179,7 @@ export function VirtualMachines() {
|
|||||||
<Cpu className="h-4 w-4 text-muted-foreground" />
|
<Cpu className="h-4 w-4 text-muted-foreground" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold text-foreground">{(totalCPU * 100).toFixed(0)}%</div>
|
<div className="text-2xl font-bold text-green-500">{(totalCPU * 100).toFixed(0)}%</div>
|
||||||
<p className="text-xs text-muted-foreground mt-2">Allocated CPU usage</p>
|
<p className="text-xs text-muted-foreground mt-2">Allocated CPU usage</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -170,7 +190,7 @@ export function VirtualMachines() {
|
|||||||
<MemoryStick className="h-4 w-4 text-muted-foreground" />
|
<MemoryStick className="h-4 w-4 text-muted-foreground" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold text-foreground">{(totalMemory / 1024 ** 3).toFixed(1)} GB</div>
|
<div className="text-2xl font-bold text-blue-500">{(totalMemory / 1024 ** 3).toFixed(1)} GB</div>
|
||||||
<p className="text-xs text-muted-foreground mt-2">Allocated RAM</p>
|
<p className="text-xs text-muted-foreground mt-2">Allocated RAM</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -181,7 +201,7 @@ export function VirtualMachines() {
|
|||||||
<Monitor className="h-4 w-4 text-muted-foreground" />
|
<Monitor className="h-4 w-4 text-muted-foreground" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold text-foreground">
|
<div className="text-2xl font-bold text-green-500">
|
||||||
{runningVMs > 0 ? ((totalCPU / runningVMs) * 100).toFixed(0) : 0}%
|
{runningVMs > 0 ? ((totalCPU / runningVMs) * 100).toFixed(0) : 0}%
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mt-2">Average resource utilization</p>
|
<p className="text-xs text-muted-foreground mt-2">Average resource utilization</p>
|
||||||
@@ -194,7 +214,7 @@ export function VirtualMachines() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-foreground flex items-center">
|
<CardTitle className="text-foreground flex items-center">
|
||||||
<Server className="h-5 w-5 mr-2" />
|
<Server className="h-5 w-5 mr-2" />
|
||||||
Virtual Machines
|
Virtual Machines & Containers
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -207,6 +227,7 @@ export function VirtualMachines() {
|
|||||||
const memPercent = vm.maxmem > 0 ? ((vm.mem / vm.maxmem) * 100).toFixed(1) : "0"
|
const memPercent = vm.maxmem > 0 ? ((vm.mem / vm.maxmem) * 100).toFixed(1) : "0"
|
||||||
const memGB = (vm.mem / 1024 ** 3).toFixed(1)
|
const memGB = (vm.mem / 1024 ** 3).toFixed(1)
|
||||||
const maxMemGB = (vm.maxmem / 1024 ** 3).toFixed(1)
|
const maxMemGB = (vm.maxmem / 1024 ** 3).toFixed(1)
|
||||||
|
const typeBadge = getTypeBadge(vm.type)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={vm.vmid} className="p-6 rounded-lg border border-border bg-card/50">
|
<div key={vm.vmid} className="p-6 rounded-lg border border-border bg-card/50">
|
||||||
@@ -216,11 +237,8 @@ export function VirtualMachines() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="font-semibold text-foreground text-lg flex items-center">
|
<div className="font-semibold text-foreground text-lg flex items-center">
|
||||||
{vm.name}
|
{vm.name}
|
||||||
<Badge
|
<Badge variant="outline" className={`ml-2 text-xs ${typeBadge.color}`}>
|
||||||
variant="outline"
|
{typeBadge.label}
|
||||||
className="ml-2 text-xs bg-purple-500/10 text-purple-500 border-purple-500/20"
|
|
||||||
>
|
|
||||||
VM
|
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-muted-foreground">ID: {vm.vmid}</div>
|
<div className="text-sm text-muted-foreground">ID: {vm.vmid}</div>
|
||||||
@@ -235,25 +253,53 @@ export function VirtualMachines() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-muted-foreground mb-2">CPU Usage</div>
|
<div className="text-sm text-muted-foreground mb-2">CPU Usage</div>
|
||||||
<div className="text-lg font-semibold text-foreground mb-1">{cpuPercent}%</div>
|
<div className="text-lg font-semibold text-green-500 mb-1">{cpuPercent}%</div>
|
||||||
<Progress value={Number.parseFloat(cpuPercent)} className="h-2" />
|
<Progress value={Number.parseFloat(cpuPercent)} className="h-2 [&>div]:bg-green-500" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-muted-foreground mb-2">Memory Usage</div>
|
<div className="text-sm text-muted-foreground mb-2">Memory Usage</div>
|
||||||
<div className="text-lg font-semibold text-foreground mb-1">
|
<div className="text-lg font-semibold text-blue-500 mb-1">
|
||||||
{memGB} GB / {maxMemGB} GB
|
{memGB} / {maxMemGB} GB
|
||||||
</div>
|
</div>
|
||||||
<Progress value={Number.parseFloat(memPercent)} className="h-2" />
|
<Progress value={Number.parseFloat(memPercent)} className="h-2 [&>div]:bg-blue-500" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-muted-foreground mb-2">Uptime</div>
|
<div className="text-sm text-muted-foreground mb-2">Disk I/O</div>
|
||||||
<div className="text-lg font-semibold text-foreground">{formatUptime(vm.uptime)}</div>
|
<div className="text-sm font-semibold text-foreground">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<HardDrive className="h-3 w-3 text-green-500" />
|
||||||
|
<span className="text-green-500">↓ {formatBytes(vm.diskread)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 mt-1">
|
||||||
|
<HardDrive className="h-3 w-3 text-blue-500" />
|
||||||
|
<span className="text-blue-500">↑ {formatBytes(vm.diskwrite)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-muted-foreground mb-2">Network I/O</div>
|
||||||
|
<div className="text-sm font-semibold text-foreground">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Network className="h-3 w-3 text-green-500" />
|
||||||
|
<span className="text-green-500">↓ {formatBytes(vm.netin)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 mt-1">
|
||||||
|
<Network className="h-3 w-3 text-blue-500" />
|
||||||
|
<span className="text-blue-500">↑ {formatBytes(vm.netout)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 pt-4 border-t border-border">
|
||||||
|
<div className="text-sm text-muted-foreground">Uptime</div>
|
||||||
|
<div className="text-lg font-semibold text-foreground">{formatUptime(vm.uptime)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@@ -986,7 +986,8 @@ def get_bridge_info(bridge_name):
|
|||||||
"""Get detailed information about a bridge interface"""
|
"""Get detailed information about a bridge interface"""
|
||||||
bridge_info = {
|
bridge_info = {
|
||||||
'members': [],
|
'members': [],
|
||||||
'physical_interface': None
|
'physical_interface': None,
|
||||||
|
'physical_duplex': 'unknown' # Added physical_duplex field
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -1000,6 +1001,17 @@ def get_bridge_info(bridge_name):
|
|||||||
if member.startswith(('enp', 'eth', 'eno', 'ens', 'wlan', 'wlp')):
|
if member.startswith(('enp', 'eth', 'eno', 'ens', 'wlan', 'wlp')):
|
||||||
bridge_info['physical_interface'] = member
|
bridge_info['physical_interface'] = member
|
||||||
print(f"[v0] Bridge {bridge_name} physical interface: {member}")
|
print(f"[v0] Bridge {bridge_name} physical interface: {member}")
|
||||||
|
|
||||||
|
# Get duplex from physical interface
|
||||||
|
try:
|
||||||
|
net_if_stats = psutil.net_if_stats()
|
||||||
|
if member in net_if_stats:
|
||||||
|
stats = net_if_stats[member]
|
||||||
|
bridge_info['physical_duplex'] = 'full' if stats.duplex == 2 else 'half' if stats.duplex == 1 else 'unknown'
|
||||||
|
print(f"[v0] Physical interface {member} duplex: {bridge_info['physical_duplex']}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[v0] Error getting duplex for {member}: {e}")
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
print(f"[v0] Bridge {bridge_name} members: {members}")
|
print(f"[v0] Bridge {bridge_name} members: {members}")
|
||||||
@@ -1129,6 +1141,10 @@ def get_network_info():
|
|||||||
bridge_info = get_bridge_info(interface_name)
|
bridge_info = get_bridge_info(interface_name)
|
||||||
interface_info['bridge_members'] = bridge_info['members']
|
interface_info['bridge_members'] = bridge_info['members']
|
||||||
interface_info['bridge_physical_interface'] = bridge_info['physical_interface']
|
interface_info['bridge_physical_interface'] = bridge_info['physical_interface']
|
||||||
|
interface_info['bridge_physical_duplex'] = bridge_info['physical_duplex']
|
||||||
|
# Override bridge duplex with physical interface duplex
|
||||||
|
if bridge_info['physical_duplex'] != 'unknown':
|
||||||
|
interface_info['duplex'] = bridge_info['physical_duplex']
|
||||||
|
|
||||||
if interface_type == 'vm_lxc':
|
if interface_type == 'vm_lxc':
|
||||||
network_data['vm_lxc_interfaces'].append(interface_info)
|
network_data['vm_lxc_interfaces'].append(interface_info)
|
||||||
@@ -1204,32 +1220,49 @@ def get_network_info():
|
|||||||
}
|
}
|
||||||
|
|
||||||
def get_proxmox_vms():
|
def get_proxmox_vms():
|
||||||
"""Get Proxmox VM information (requires pvesh command)"""
|
"""Get Proxmox VM and LXC information (requires pvesh command)"""
|
||||||
try:
|
try:
|
||||||
# Try to get VM list using pvesh command
|
all_vms = []
|
||||||
result = subprocess.run(['pvesh', 'get', '/nodes/localhost/qemu', '--output-format', 'json'],
|
|
||||||
capture_output=True, text=True, timeout=10)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
try:
|
||||||
vms = json.loads(result.stdout)
|
result = subprocess.run(['pvesh', 'get', '/cluster/resources', '--type', 'vm', '--output-format', 'json'],
|
||||||
return vms
|
capture_output=True, text=True, timeout=10)
|
||||||
else:
|
|
||||||
# Handle LXC containers as well
|
if result.returncode == 0:
|
||||||
result_lxc = subprocess.run(['pvesh', 'get', '/nodes/localhost/lxc', '--output-format', 'json'],
|
resources = json.loads(result.stdout)
|
||||||
capture_output=True, text=True, timeout=10)
|
for resource in resources:
|
||||||
if result_lxc.returncode == 0:
|
vm_data = {
|
||||||
lxc_vms = json.loads(result_lxc.stdout)
|
'vmid': resource.get('vmid'),
|
||||||
# Combine QEMU and LXC for a complete VM list
|
'name': resource.get('name', f"VM-{resource.get('vmid')}"),
|
||||||
if 'vms' in locals(): # Check if vms were loaded from QEMU
|
'status': resource.get('status', 'unknown'),
|
||||||
vms.extend(lxc_vms)
|
'type': 'lxc' if resource.get('type') == 'lxc' else 'qemu',
|
||||||
else:
|
'cpu': resource.get('cpu', 0),
|
||||||
vms = lxc_vms
|
'mem': resource.get('mem', 0),
|
||||||
return vms
|
'maxmem': resource.get('maxmem', 0),
|
||||||
|
'disk': resource.get('disk', 0),
|
||||||
|
'maxdisk': resource.get('maxdisk', 0),
|
||||||
|
'uptime': resource.get('uptime', 0),
|
||||||
|
'netin': resource.get('netin', 0),
|
||||||
|
'netout': resource.get('netout', 0),
|
||||||
|
'diskread': resource.get('diskread', 0),
|
||||||
|
'diskwrite': resource.get('diskwrite', 0)
|
||||||
|
}
|
||||||
|
all_vms.append(vm_data)
|
||||||
|
print(f"[v0] Found {vm_data['type']}: {vm_data['name']} (VMID: {vm_data['vmid']}, Status: {vm_data['status']})")
|
||||||
|
|
||||||
|
return all_vms
|
||||||
else:
|
else:
|
||||||
|
print(f"[v0] pvesh command failed: {result.stderr}")
|
||||||
return {
|
return {
|
||||||
'error': 'pvesh command not available or failed - Proxmox API not accessible for QEMU and LXC',
|
'error': 'pvesh command not available or failed',
|
||||||
'vms': []
|
'vms': []
|
||||||
}
|
}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[v0] Error getting VM/LXC info: {e}")
|
||||||
|
return {
|
||||||
|
'error': f'Unable to access VM information: {str(e)}',
|
||||||
|
'vms': []
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error getting VM info: {e}")
|
print(f"Error getting VM info: {e}")
|
||||||
return {
|
return {
|
||||||
|
Reference in New Issue
Block a user