From dc03144773641fd7bd8a8f023ea905435b94dd04 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Sat, 4 Oct 2025 19:05:39 +0200 Subject: [PATCH] Update AppImage --- AppImage/components/network-metrics.tsx | 331 +++++++++++++++++++++--- AppImage/scripts/flask_server.py | 156 ++++++++++- 2 files changed, 438 insertions(+), 49 deletions(-) diff --git a/AppImage/components/network-metrics.tsx b/AppImage/components/network-metrics.tsx index 894cec3..24ded84 100644 --- a/AppImage/components/network-metrics.tsx +++ b/AppImage/components/network-metrics.tsx @@ -3,7 +3,8 @@ import { useState, useEffect } from "react" import { Card, CardContent, CardHeader, CardTitle } from "./ui/card" import { Badge } from "./ui/badge" -import { Wifi, Globe, Shield, Activity, Network, Router, AlertCircle } from "lucide-react" +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog" +import { Wifi, Globe, Shield, Activity, Network, Router, AlertCircle, Zap } from "lucide-react" interface NetworkData { interfaces: NetworkInterface[] @@ -17,11 +18,59 @@ interface NetworkData { interface NetworkInterface { name: string + type: string status: string + speed: number + duplex: string + mtu: number + mac_address: string | null addresses: Array<{ ip: string netmask: string }> + bytes_sent?: number + bytes_recv?: number + packets_sent?: number + packets_recv?: number + errors_in?: number + errors_out?: number + drops_in?: number + drops_out?: number + bond_mode?: string + bond_slaves?: string[] + bond_active_slave?: string | null + bridge_members?: string[] +} + +const getInterfaceTypeBadge = (type: string) => { + switch (type) { + case "physical": + return { color: "bg-blue-500/10 text-blue-500 border-blue-500/20", label: "Physical" } + case "bridge": + return { color: "bg-green-500/10 text-green-500 border-green-500/20", label: "Bridge" } + case "bond": + return { color: "bg-purple-500/10 text-purple-500 border-purple-500/20", label: "Bond" } + case "vlan": + return { color: "bg-cyan-500/10 text-cyan-500 border-cyan-500/20", label: "VLAN" } + case "virtual": + return { color: "bg-orange-500/10 text-orange-500 border-orange-500/20", label: "Virtual" } + default: + return { color: "bg-gray-500/10 text-gray-500 border-gray-500/20", label: "Unknown" } + } +} + +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]}` +} + +const formatSpeed = (speed: number): string => { + if (speed === 0) return "N/A" + if (speed >= 1000) return `${(speed / 1000).toFixed(1)} Gbps` + return `${speed} Mbps` } const fetchNetworkData = async (): Promise => { @@ -52,6 +101,7 @@ export function NetworkMetrics() { const [networkData, setNetworkData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + const [selectedInterface, setSelectedInterface] = useState(null) useEffect(() => { const fetchData = async () => { @@ -190,50 +240,255 @@ export function NetworkMetrics() {
- {networkData.interfaces.map((interface_, index) => ( -
-
- -
-
{interface_.name}
-
Network Interface
-
-
+ {networkData.interfaces.map((interface_, index) => { + const typeBadge = getInterfaceTypeBadge(interface_.type) -
-
-
IP Address
-
- {interface_.addresses.length > 0 ? interface_.addresses[0].ip : "N/A"} + return ( +
setSelectedInterface(interface_)} + > + {/* First row: Icon, Name, Type Badge */} +
+ +
+
{interface_.name}
+ + {typeBadge.label} + +
+ + {interface_.status.toUpperCase()} + +
+ + {/* Second row: Details */} +
+
+
+
IP Address
+
+ {interface_.addresses.length > 0 ? interface_.addresses[0].ip : "N/A"} +
+
+ +
+
Speed
+
+ + {formatSpeed(interface_.speed)} +
+
+ +
+
Traffic
+
+ ↓ {formatBytes(interface_.bytes_recv)} + {" / "} + ↑ {formatBytes(interface_.bytes_sent)} +
+
+ + {interface_.mac_address && ( +
+
MAC
+
{interface_.mac_address}
+
+ )}
- -
-
Netmask
-
- {interface_.addresses.length > 0 ? interface_.addresses[0].netmask : "N/A"} -
-
- - - {interface_.status.toUpperCase()} -
-
- ))} + ) + })}
+ + {/* Interface Details Modal */} + setSelectedInterface(null)}> + + + + + {selectedInterface?.name} - Interface Details + + + + {selectedInterface && ( +
+ {/* Basic Information */} +
+

Basic Information

+
+
+
Interface Name
+
{selectedInterface.name}
+
+
+
Type
+ + {getInterfaceTypeBadge(selectedInterface.type).label} + +
+
+
Status
+ + {selectedInterface.status.toUpperCase()} + +
+
+
Speed
+
{formatSpeed(selectedInterface.speed)}
+
+
+
Duplex
+
{selectedInterface.duplex}
+
+
+
MTU
+
{selectedInterface.mtu}
+
+ {selectedInterface.mac_address && ( +
+
MAC Address
+
{selectedInterface.mac_address}
+
+ )} +
+
+ + {/* IP Addresses */} + {selectedInterface.addresses.length > 0 && ( +
+

IP Addresses

+
+ {selectedInterface.addresses.map((addr, idx) => ( +
+
+
{addr.ip}
+
Netmask: {addr.netmask}
+
+
+ ))} +
+
+ )} + + {/* Traffic Statistics */} +
+

Traffic Statistics

+
+
+
Bytes Received
+
{formatBytes(selectedInterface.bytes_recv)}
+
+
+
Bytes Sent
+
{formatBytes(selectedInterface.bytes_sent)}
+
+
+
Packets Received
+
{selectedInterface.packets_recv?.toLocaleString() || "N/A"}
+
+
+
Packets Sent
+
{selectedInterface.packets_sent?.toLocaleString() || "N/A"}
+
+
+
Errors In
+
{selectedInterface.errors_in || 0}
+
+
+
Errors Out
+
{selectedInterface.errors_out || 0}
+
+
+
Drops In
+
{selectedInterface.drops_in || 0}
+
+
+
Drops Out
+
{selectedInterface.drops_out || 0}
+
+
+
+ + {/* Bond Information */} + {selectedInterface.type === "bond" && selectedInterface.bond_slaves && ( +
+

Bond Configuration

+
+
+
Bonding Mode
+
{selectedInterface.bond_mode || "Unknown"}
+
+ {selectedInterface.bond_active_slave && ( +
+
Active Slave
+
{selectedInterface.bond_active_slave}
+
+ )} +
+
Slave Interfaces
+
+ {selectedInterface.bond_slaves.map((slave, idx) => ( + + {slave} + + ))} +
+
+
+
+ )} + + {/* Bridge Information */} + {selectedInterface.type === "bridge" && selectedInterface.bridge_members && ( +
+

Bridge Configuration

+
+
Member Interfaces
+
+ {selectedInterface.bridge_members.length > 0 ? ( + selectedInterface.bridge_members.map((member, idx) => ( + + {member} + + )) + ) : ( +
No members
+ )} +
+
+
+ )} +
+ )} +
+
) } diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py index aded967..9de02a4 100644 --- a/AppImage/scripts/flask_server.py +++ b/AppImage/scripts/flask_server.py @@ -870,8 +870,89 @@ def get_proxmox_storage(): 'storage': [] } +def get_interface_type(interface_name): + """Detect the type of network interface""" + try: + # Check if it's a bond + if interface_name.startswith('bond'): + return 'bond' + + # Check if it's a bridge + if interface_name.startswith(('vmbr', 'br', 'virbr')): + return 'bridge' + + # Check if it's a VLAN (contains a dot) + if '.' in interface_name: + return 'vlan' + + # Check if it's NVMe (virtual) + if interface_name.startswith('nvme'): + return 'virtual' + + # Check if it's a physical interface + if interface_name.startswith(('enp', 'eth', 'wlan', 'wlp')): + return 'physical' + + # Check if it's virtual (veth, tap, tun, etc.) + if interface_name.startswith(('veth', 'tap', 'tun', 'vnet')): + return 'virtual' + + # Default to unknown + return 'unknown' + except Exception as e: + print(f"[v0] Error detecting interface type for {interface_name}: {e}") + return 'unknown' + +def get_bond_info(bond_name): + """Get detailed information about a bonding interface""" + bond_info = { + 'mode': 'unknown', + 'slaves': [], + 'active_slave': None + } + + try: + bond_file = f'/proc/net/bonding/{bond_name}' + if os.path.exists(bond_file): + with open(bond_file, 'r') as f: + content = f.read() + + # Parse bonding mode + for line in content.split('\n'): + if 'Bonding Mode:' in line: + bond_info['mode'] = line.split(':', 1)[1].strip() + elif 'Slave Interface:' in line: + slave_name = line.split(':', 1)[1].strip() + bond_info['slaves'].append(slave_name) + elif 'Currently Active Slave:' in line: + bond_info['active_slave'] = line.split(':', 1)[1].strip() + + print(f"[v0] Bond {bond_name} info: mode={bond_info['mode']}, slaves={bond_info['slaves']}") + except Exception as e: + print(f"[v0] Error reading bond info for {bond_name}: {e}") + + return bond_info + +def get_bridge_info(bridge_name): + """Get detailed information about a bridge interface""" + bridge_info = { + 'members': [] + } + + try: + # Try to read bridge members from /sys/class/net//brif/ + brif_path = f'/sys/class/net/{bridge_name}/brif' + if os.path.exists(brif_path): + members = os.listdir(brif_path) + bridge_info['members'] = members + print(f"[v0] Bridge {bridge_name} members: {members}") + except Exception as e: + print(f"[v0] Error reading bridge info for {bridge_name}: {e}") + + return bridge_info + def get_network_info(): - """Get network interface information""" + """Get network interface information - Enhanced with type detection, speed, MAC, bonds, bridges""" try: network_data = { 'interfaces': [], @@ -882,20 +963,36 @@ def get_network_info(): net_if_addrs = psutil.net_if_addrs() net_if_stats = psutil.net_if_stats() + try: + net_io_per_nic = psutil.net_io_counters(pernic=True) + except Exception as e: + print(f"[v0] Error getting per-NIC stats: {e}") + net_io_per_nic = {} + for interface_name, interface_addresses in net_if_addrs.items(): # Skip loopback if interface_name == 'lo': continue - # Skip virtual interfaces that are not bridges - # Keep: physical interfaces (enp*, eth*, wlan*) and bridges (vmbr*, br*) - if not (interface_name.startswith(('enp', 'eth', 'wlan', 'vmbr', 'br'))): + interface_type = get_interface_type(interface_name) + + # Skip unknown virtual interfaces + if interface_type == 'unknown' and not interface_name.startswith(('enp', 'eth', 'wlan', 'vmbr', 'br', 'bond')): + continue + + stats = net_if_stats.get(interface_name) + if not stats: continue interface_info = { 'name': interface_name, - 'status': 'up' if net_if_stats[interface_name].isup else 'down', - 'addresses': [] + 'type': interface_type, # Added type + 'status': 'up' if stats.isup else 'down', + 'speed': stats.speed if stats.speed > 0 else 0, # Added speed in Mbps + 'duplex': 'full' if stats.duplex == 2 else 'half' if stats.duplex == 1 else 'unknown', # Added duplex + 'mtu': stats.mtu, # Added MTU + 'addresses': [], + 'mac_address': None, # Added MAC address } for address in interface_addresses: @@ -904,10 +1001,33 @@ def get_network_info(): 'ip': address.address, 'netmask': address.netmask }) + elif address.family == 17: # AF_PACKET (MAC address on Linux) + interface_info['mac_address'] = address.address + + if interface_name in net_io_per_nic: + io_stats = net_io_per_nic[interface_name] + interface_info['bytes_sent'] = io_stats.bytes_sent + interface_info['bytes_recv'] = io_stats.bytes_recv + interface_info['packets_sent'] = io_stats.packets_sent + interface_info['packets_recv'] = io_stats.packets_recv + interface_info['errors_in'] = io_stats.errin + interface_info['errors_out'] = io_stats.errout + interface_info['drops_in'] = io_stats.dropin + interface_info['drops_out'] = io_stats.dropout + + if interface_type == 'bond': + bond_info = get_bond_info(interface_name) + interface_info['bond_mode'] = bond_info['mode'] + interface_info['bond_slaves'] = bond_info['slaves'] + interface_info['bond_active_slave'] = bond_info['active_slave'] + + if interface_type == 'bridge': + bridge_info = get_bridge_info(interface_name) + interface_info['bridge_members'] = bridge_info['members'] network_data['interfaces'].append(interface_info) - # Get network I/O statistics + # Get network I/O statistics (global) net_io = psutil.net_io_counters() network_data['traffic'] = { 'bytes_sent': net_io.bytes_sent, @@ -919,6 +1039,8 @@ def get_network_info(): return network_data except Exception as e: print(f"Error getting network info: {e}") + import traceback + traceback.print_exc() return { 'error': f'Unable to access network information: {str(e)}', 'interfaces': [], @@ -936,10 +1058,22 @@ def get_proxmox_vms(): vms = json.loads(result.stdout) return vms else: - return { - 'error': 'pvesh command not available or failed - Proxmox API not accessible', - 'vms': [] - } + # Handle LXC containers as well + result_lxc = subprocess.run(['pvesh', 'get', '/nodes/localhost/lxc', '--output-format', 'json'], + capture_output=True, text=True, timeout=10) + if result_lxc.returncode == 0: + lxc_vms = json.loads(result_lxc.stdout) + # Combine QEMU and LXC for a complete VM list + if 'vms' in locals(): # Check if vms were loaded from QEMU + vms.extend(lxc_vms) + else: + vms = lxc_vms + return vms + else: + return { + 'error': 'pvesh command not available or failed - Proxmox API not accessible for QEMU and LXC', + 'vms': [] + } except Exception as e: print(f"Error getting VM info: {e}") return {