diff --git a/AppImage/components/network-metrics.tsx b/AppImage/components/network-metrics.tsx index 24ded84..f0003ec 100644 --- a/AppImage/components/network-metrics.tsx +++ b/AppImage/components/network-metrics.tsx @@ -1,19 +1,29 @@ "use client" -import { useState, useEffect } from "react" +import { useState } from "react" import { Card, CardContent, CardHeader, CardTitle } from "./ui/card" import { Badge } from "./ui/badge" import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog" import { Wifi, Globe, Shield, Activity, Network, Router, AlertCircle, Zap } from "lucide-react" +import useSWR from "swr" interface NetworkData { interfaces: NetworkInterface[] + vm_lxc_interfaces?: NetworkInterface[] traffic: { bytes_sent: number bytes_recv: number packets_sent?: number packets_recv?: number + packet_loss_in?: number + packet_loss_out?: number + dropin?: number + dropout?: number } + active_count?: number + total_count?: number + vm_lxc_active_count?: number + vm_lxc_total_count?: number } interface NetworkInterface { @@ -40,6 +50,12 @@ interface NetworkInterface { bond_slaves?: string[] bond_active_slave?: string | null bridge_members?: string[] + packet_loss_in?: number + packet_loss_out?: number + vmid?: number + vm_name?: string + vm_type?: string + vm_status?: string } const getInterfaceTypeBadge = (type: string) => { @@ -52,6 +68,8 @@ const getInterfaceTypeBadge = (type: string) => { 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 "vm_lxc": + return { color: "bg-orange-500/10 text-orange-500 border-orange-500/20", label: "Virtual" } case "virtual": return { color: "bg-orange-500/10 text-orange-500 border-orange-500/20", label: "Virtual" } default: @@ -59,6 +77,15 @@ const getInterfaceTypeBadge = (type: string) => { } } +const getVMTypeBadge = (vmType: string | undefined) => { + if (vmType === "lxc") { + return { color: "bg-cyan-500/10 text-cyan-500 border-cyan-500/20", label: "LXC" } + } else if (vmType === "vm") { + return { color: "bg-purple-500/10 text-purple-500 border-purple-500/20", label: "VM" } + } + 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 @@ -73,57 +100,36 @@ const formatSpeed = (speed: number): string => { return `${speed} Mbps` } -const fetchNetworkData = async (): Promise => { - try { - console.log("[v0] Fetching network data from Flask server...") - const response = await fetch("/api/network", { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - signal: AbortSignal.timeout(5000), - }) +const fetcher = async (url: string): Promise => { + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + signal: AbortSignal.timeout(5000), + }) - if (!response.ok) { - throw new Error(`Flask server responded with status: ${response.status}`) - } - - const data = await response.json() - console.log("[v0] Successfully fetched network data from Flask:", data) - return data - } catch (error) { - console.error("[v0] Failed to fetch network data from Flask server:", error) - return null + if (!response.ok) { + throw new Error(`Flask server responded with status: ${response.status}`) } + + return response.json() } export function NetworkMetrics() { - const [networkData, setNetworkData] = useState(null) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) + const { + data: networkData, + error, + isLoading, + } = useSWR("/api/network", fetcher, { + refreshInterval: 30000, // Refresh every 30 seconds + revalidateOnFocus: false, + revalidateOnReconnect: true, + }) + const [selectedInterface, setSelectedInterface] = useState(null) - useEffect(() => { - const fetchData = async () => { - setLoading(true) - setError(null) - const result = await fetchNetworkData() - - if (!result) { - setError("Flask server not available. Please ensure the server is running.") - } else { - setNetworkData(result) - } - - setLoading(false) - } - - fetchData() - const interval = setInterval(fetchData, 30000) - return () => clearInterval(interval) - }, []) - - if (loading) { + if (isLoading) { return (
@@ -143,7 +149,8 @@ export function NetworkMetrics() {
Flask Server Not Available
- {error || "Unable to connect to the Flask server. Please ensure the server is running and try again."} + {error?.message || + "Unable to connect to the Flask server. Please ensure the server is running and try again."}
@@ -153,23 +160,24 @@ export function NetworkMetrics() { ) } - const trafficInMB = (networkData.traffic.bytes_recv / (1024 * 1024)).toFixed(1) - const trafficOutMB = (networkData.traffic.bytes_sent / (1024 * 1024)).toFixed(1) + const trafficInGB = (networkData.traffic.bytes_recv / 1024 ** 3).toFixed(2) + const trafficOutGB = (networkData.traffic.bytes_sent / 1024 ** 3).toFixed(2) + const packetsRecvK = networkData.traffic.packets_recv ? (networkData.traffic.packets_recv / 1000).toFixed(0) : "0" return (
{/* Network Overview Cards */} -
+
Network Traffic -
{trafficInMB} MB
-
- ↓ {trafficInMB} MB - ↑ {trafficOutMB} MB +
{trafficInGB} GB
+
+ ↓ {trafficInGB} GB + ↑ {trafficOutGB} GB

Total data transferred

@@ -181,15 +189,13 @@ export function NetworkMetrics() { -
- {networkData.interfaces.filter((i) => i.status === "up").length} -
+
{networkData.active_count ?? 0}
Online
-

{networkData.interfaces.length} total interfaces

+

{networkData.total_count ?? 0} total interfaces

@@ -210,22 +216,23 @@ export function NetworkMetrics() { - - - - Packets - + + Packets + -
- {networkData.traffic.packets_recv ? (networkData.traffic.packets_recv / 1000).toFixed(0) : "N/A"}K -
+
{packetsRecvK}K
Received
-

Total packets received

+ {networkData.traffic.packet_loss_in !== undefined && networkData.traffic.packet_loss_in > 0 && ( +

Loss: {networkData.traffic.packet_loss_in}%

+ )} + {(!networkData.traffic.packet_loss_in || networkData.traffic.packet_loss_in === 0) && ( +

No packet loss

+ )}
@@ -249,10 +256,10 @@ export function NetworkMetrics() { className="flex flex-col gap-3 p-4 rounded-lg border border-border bg-card/50 hover:bg-card/80 transition-colors cursor-pointer" onClick={() => setSelectedInterface(interface_)} > - {/* First row: Icon, Name, Type Badge */} -
+ {/* First row: Icon, Name, Type Badge, Status */} +
-
+
{interface_.name}
{typeBadge.label} @@ -262,48 +269,48 @@ export function NetworkMetrics() { variant="outline" className={ interface_.status === "up" - ? "bg-green-500/10 text-green-500 border-green-500/20" - : "bg-red-500/10 text-red-500 border-red-500/20" + ? "bg-green-500/10 text-green-500 border-green-500/20 ml-auto" + : "bg-red-500/10 text-red-500 border-red-500/20 ml-auto" } > {interface_.status.toUpperCase()}
- {/* Second row: Details */} -
-
-
-
IP Address
-
- {interface_.addresses.length > 0 ? interface_.addresses[0].ip : "N/A"} -
+ {/* Second row: Details - Responsive layout */} +
+
+
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}
-
- )}
+ +
+
Speed
+
+ + {formatSpeed(interface_.speed)} +
+
+ +
+
Traffic
+
+ ↓ {formatBytes(interface_.bytes_recv)} + {" / "} + ↑ {formatBytes(interface_.bytes_sent)} +
+
+ + {interface_.mac_address && ( +
+
MAC
+
+ {interface_.mac_address} +
+
+ )}
) @@ -312,6 +319,93 @@ export function NetworkMetrics() { + {networkData.vm_lxc_interfaces && networkData.vm_lxc_interfaces.length > 0 && ( + + + + + VM & LXC Network Interfaces + + {networkData.vm_lxc_active_count ?? 0} / {networkData.vm_lxc_total_count ?? 0} Active + + + + +
+ {networkData.vm_lxc_interfaces.map((interface_, index) => { + const vmTypeBadge = getVMTypeBadge(interface_.vm_type) + + return ( +
setSelectedInterface(interface_)} + > + {/* First row: Icon, Name, VM/LXC Badge, VM Name, Status */} +
+ +
+
{interface_.name}
+ + {vmTypeBadge.label} + + {interface_.vm_name && ( +
→ {interface_.vm_name}
+ )} +
+ + {interface_.status.toUpperCase()} + +
+ + {/* Second row: Details - Responsive layout */} +
+
+
VMID
+
{interface_.vmid ?? "N/A"}
+
+ +
+
Speed
+
+ + {formatSpeed(interface_.speed)} +
+
+ +
+
Traffic
+
+ ↓ {formatBytes(interface_.bytes_recv)} + {" / "} + ↑ {formatBytes(interface_.bytes_sent)} +
+
+ + {interface_.mac_address && ( +
+
MAC
+
+ {interface_.mac_address} +
+
+ )} +
+
+ ) + })} +
+
+
+ )} + {/* Interface Details Modal */} setSelectedInterface(null)}> @@ -425,6 +519,26 @@ export function NetworkMetrics() {
Drops Out
{selectedInterface.drops_out || 0}
+ {selectedInterface.packet_loss_in !== undefined && ( +
+
Packet Loss In
+
1 ? "text-red-500" : "text-green-500"}`} + > + {selectedInterface.packet_loss_in}% +
+
+ )} + {selectedInterface.packet_loss_out !== undefined && ( +
+
Packet Loss Out
+
1 ? "text-red-500" : "text-green-500"}`} + > + {selectedInterface.packet_loss_out}% +
+
+ )}
diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py index 9de02a4..0cbb0cc 100644 --- a/AppImage/scripts/flask_server.py +++ b/AppImage/scripts/flask_server.py @@ -15,10 +15,56 @@ import os import time import socket from datetime import datetime, timedelta +import re # Added for regex matching app = Flask(__name__) CORS(app) # Enable CORS for Next.js frontend +def extract_vmid_from_interface(interface_name): + """Extract VMID from virtual interface name (veth100i0 -> 100, tap105i0 -> 105)""" + try: + match = re.match(r'(veth|tap)(\d+)i\d+', interface_name) + if match: + vmid = int(match.group(2)) + interface_type = 'lxc' if match.group(1) == 'veth' else 'vm' + return vmid, interface_type + return None, None + except Exception as e: + print(f"[v0] Error extracting VMID from {interface_name}: {e}") + return None, None + +def get_vm_lxc_names(): + """Get VM and LXC names from Proxmox API""" + vm_lxc_map = {} + + try: + result = subprocess.run(['pvesh', 'get', '/cluster/resources', '--type', 'vm', '--output-format', 'json'], + capture_output=True, text=True, timeout=10) + + if result.returncode == 0: + resources = json.loads(result.stdout) + for resource in resources: + vmid = resource.get('vmid') + name = resource.get('name', f'VM-{vmid}') + vm_type = resource.get('type', 'unknown') # 'qemu' or 'lxc' + status = resource.get('status', 'unknown') + + if vmid: + vm_lxc_map[vmid] = { + 'name': name, + 'type': 'lxc' if vm_type == 'lxc' else 'vm', + 'status': status + } + print(f"[v0] Found {vm_type} {vmid}: {name} ({status})") + else: + print(f"[v0] pvesh command failed: {result.stderr}") + except FileNotFoundError: + print("[v0] pvesh command not found - Proxmox not installed") + except Exception as e: + print(f"[v0] Error getting VM/LXC names: {e}") + + return vm_lxc_map + @app.route('/') def serve_dashboard(): """Serve the main dashboard page from Next.js build""" @@ -873,35 +919,38 @@ def get_proxmox_storage(): def get_interface_type(interface_name): """Detect the type of network interface""" try: + # Skip loopback + if interface_name == 'lo': + return 'skip' + + if interface_name.startswith(('veth', 'tap')): + return 'vm_lxc' + + # Skip other virtual interfaces + if interface_name.startswith(('tun', 'vnet', 'docker', 'virbr')): + return 'skip' + # 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')): + # Check if it's a bridge (but not virbr which we skip above) + if interface_name.startswith(('vmbr', 'br')): 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')): + if interface_name.startswith(('enp', 'eth', 'wlan', 'wlp', 'eno', 'ens')): 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' + # Default to skip for unknown types + return 'skip' except Exception as e: print(f"[v0] Error detecting interface type for {interface_name}: {e}") - return 'unknown' + return 'skip' def get_bond_info(bond_name): """Get detailed information about a bonding interface""" @@ -952,13 +1001,16 @@ def get_bridge_info(bridge_name): return bridge_info def get_network_info(): - """Get network interface information - Enhanced with type detection, speed, MAC, bonds, bridges""" + """Get network interface information - Enhanced with VM/LXC interface separation""" try: network_data = { 'interfaces': [], - 'traffic': {'bytes_sent': 0, 'bytes_recv': 0} + 'vm_lxc_interfaces': [], # Added separate list for VM/LXC interfaces + 'traffic': {'bytes_sent': 0, 'bytes_recv': 0, 'packets_sent': 0, 'packets_recv': 0} } + vm_lxc_map = get_vm_lxc_names() + # Get network interfaces net_if_addrs = psutil.net_if_addrs() net_if_stats = psutil.net_if_stats() @@ -969,32 +1021,55 @@ def get_network_info(): print(f"[v0] Error getting per-NIC stats: {e}") net_io_per_nic = {} + active_count = 0 + total_count = 0 + vm_lxc_active_count = 0 + vm_lxc_total_count = 0 + for interface_name, interface_addresses in net_if_addrs.items(): - # Skip loopback - if interface_name == 'lo': - continue - 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')): + if interface_type == 'skip': + print(f"[v0] Skipping interface: {interface_name} (type: {interface_type})") continue stats = net_if_stats.get(interface_name) if not stats: continue + + if interface_type == 'vm_lxc': + vm_lxc_total_count += 1 + if stats.isup: + vm_lxc_active_count += 1 + else: + total_count += 1 + if stats.isup: + active_count += 1 interface_info = { 'name': interface_name, - 'type': interface_type, # Added type + 'type': interface_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 + 'speed': stats.speed if stats.speed > 0 else 0, + 'duplex': 'full' if stats.duplex == 2 else 'half' if stats.duplex == 1 else 'unknown', + 'mtu': stats.mtu, 'addresses': [], - 'mac_address': None, # Added MAC address + 'mac_address': None, } + if interface_type == 'vm_lxc': + vmid, vm_type = extract_vmid_from_interface(interface_name) + if vmid and vmid in vm_lxc_map: + interface_info['vmid'] = vmid + interface_info['vm_name'] = vm_lxc_map[vmid]['name'] + interface_info['vm_type'] = vm_lxc_map[vmid]['type'] + interface_info['vm_status'] = vm_lxc_map[vmid]['status'] + elif vmid: + interface_info['vmid'] = vmid + interface_info['vm_name'] = f'{"LXC" if vm_type == "lxc" else "VM"} {vmid}' + interface_info['vm_type'] = vm_type + interface_info['vm_status'] = 'unknown' + for address in interface_addresses: if address.family == 2: # IPv4 interface_info['addresses'].append({ @@ -1014,6 +1089,19 @@ def get_network_info(): interface_info['errors_out'] = io_stats.errout interface_info['drops_in'] = io_stats.dropin interface_info['drops_out'] = io_stats.dropout + + total_packets_in = io_stats.packets_recv + io_stats.dropin + total_packets_out = io_stats.packets_sent + io_stats.dropout + + if total_packets_in > 0: + interface_info['packet_loss_in'] = round((io_stats.dropin / total_packets_in) * 100, 2) + else: + interface_info['packet_loss_in'] = 0 + + if total_packets_out > 0: + interface_info['packet_loss_out'] = round((io_stats.dropout / total_packets_out) * 100, 2) + else: + interface_info['packet_loss_out'] = 0 if interface_type == 'bond': bond_info = get_bond_info(interface_name) @@ -1025,7 +1113,18 @@ def get_network_info(): bridge_info = get_bridge_info(interface_name) interface_info['bridge_members'] = bridge_info['members'] - network_data['interfaces'].append(interface_info) + if interface_type == 'vm_lxc': + network_data['vm_lxc_interfaces'].append(interface_info) + else: + network_data['interfaces'].append(interface_info) + + network_data['active_count'] = active_count + network_data['total_count'] = total_count + network_data['vm_lxc_active_count'] = vm_lxc_active_count + network_data['vm_lxc_total_count'] = vm_lxc_total_count + + print(f"[v0] Physical interfaces: {active_count} active out of {total_count} total") + print(f"[v0] VM/LXC interfaces: {vm_lxc_active_count} active out of {vm_lxc_total_count} total") # Get network I/O statistics (global) net_io = psutil.net_io_counters() @@ -1033,9 +1132,26 @@ def get_network_info(): 'bytes_sent': net_io.bytes_sent, 'bytes_recv': net_io.bytes_recv, 'packets_sent': net_io.packets_sent, - 'packets_recv': net_io.packets_recv + 'packets_recv': net_io.packets_recv, + 'errin': net_io.errin, + 'errout': net_io.errout, + 'dropin': net_io.dropin, + 'dropout': net_io.dropout } + total_packets_in = net_io.packets_recv + net_io.dropin + total_packets_out = net_io.packets_sent + net_io.dropout + + if total_packets_in > 0: + network_data['traffic']['packet_loss_in'] = round((net_io.dropin / total_packets_in) * 100, 2) + else: + network_data['traffic']['packet_loss_in'] = 0 + + if total_packets_out > 0: + network_data['traffic']['packet_loss_out'] = round((net_io.dropout / total_packets_out) * 100, 2) + else: + network_data['traffic']['packet_loss_out'] = 0 + return network_data except Exception as e: print(f"Error getting network info: {e}") @@ -1044,7 +1160,12 @@ def get_network_info(): return { 'error': f'Unable to access network information: {str(e)}', 'interfaces': [], - 'traffic': {'bytes_sent': 0, 'bytes_recv': 0, 'packets_sent': 0, 'packets_recv': 0} + 'vm_lxc_interfaces': [], + 'traffic': {'bytes_sent': 0, 'bytes_recv': 0, 'packets_sent': 0, 'packets_recv': 0}, + 'active_count': 0, + 'total_count': 0, + 'vm_lxc_active_count': 0, + 'vm_lxc_total_count': 0 } def get_proxmox_vms():