"use client" 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, Activity, Network, Router, AlertCircle, Zap } from "lucide-react" import useSWR from "swr" import { NetworkTrafficChart } from "./network-traffic-chart" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" interface NetworkData { interfaces: NetworkInterface[] physical_interfaces?: NetworkInterface[] bridge_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 errin?: number errout?: number } active_count?: number total_count?: number physical_active_count?: number physical_total_count?: number bridge_active_count?: number bridge_total_count?: number vm_lxc_active_count?: number vm_lxc_total_count?: number } 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[] bridge_physical_interface?: string bridge_bond_slaves?: string[] packet_loss_in?: number packet_loss_out?: number vmid?: number vm_name?: string vm_type?: string vm_status?: 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 "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: return { color: "bg-gray-500/10 text-gray-500 border-gray-500/20", label: "Unknown" } } } 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 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 formatStorage = (bytes: number): string => { if (bytes === 0) return "0 B" const k = 1024 const sizes = ["B", "KB", "MB", "GB", "TB", "PB"] const i = Math.floor(Math.log(bytes) / Math.log(k)) const value = bytes / Math.pow(k, i) // Use 1 decimal place for values >= 10, 2 decimal places for values < 10 const decimals = value >= 10 ? 1 : 2 return `${value.toFixed(decimals)} ${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 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}`) } return response.json() } export function NetworkMetrics() { const { data: networkData, error, isLoading, } = useSWR("/api/network", fetcher, { refreshInterval: 60000, // Refresh every 60 seconds revalidateOnFocus: false, revalidateOnReconnect: true, }) const [selectedInterface, setSelectedInterface] = useState(null) const [timeframe, setTimeframe] = useState<"hour" | "day" | "week" | "month" | "year">("day") const [modalTimeframe, setModalTimeframe] = useState<"hour" | "day" | "week" | "month" | "year">("day") const [networkTotals, setNetworkTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 }) const [interfaceTotals, setInterfaceTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 }) const { data: modalNetworkData } = useSWR(selectedInterface ? "/api/network" : null, fetcher, { refreshInterval: 15000, // Refresh every 15 seconds when modal is open revalidateOnFocus: false, revalidateOnReconnect: true, }) const { data: interfaceHistoricalData } = useSWR(`/api/node/metrics?timeframe=${timeframe}`, fetcher, { refreshInterval: 30000, revalidateOnFocus: false, }) if (isLoading) { return (
Loading network data...
) } if (error || !networkData) { return (
Flask Server Not Available
{error?.message || "Unable to connect to the Flask server. Please ensure the server is running and try again."}
) } const trafficInFormatted = formatStorage(networkTotals.received * 1024 * 1024 * 1024) // Convert GB to bytes const trafficOutFormatted = formatStorage(networkTotals.sent * 1024 * 1024 * 1024) const packetsRecvK = networkData.traffic.packets_recv ? (networkData.traffic.packets_recv / 1000).toFixed(0) : "0" const totalErrors = (networkData.traffic.errin || 0) + (networkData.traffic.errout || 0) const packetLossIn = networkData.traffic.packet_loss_in || 0 const packetLossOut = networkData.traffic.packet_loss_out || 0 const avgPacketLoss = ((packetLossIn + packetLossOut) / 2).toFixed(2) // Determine health status let healthStatus = "Healthy" let healthColor = "bg-green-500/10 text-green-500 border-green-500/20" if (Number.parseFloat(avgPacketLoss) > 5 || totalErrors > 1000) { healthStatus = "Critical" healthColor = "bg-red-500/10 text-red-500 border-red-500/20" } else if (Number.parseFloat(avgPacketLoss) >= 1 || totalErrors >= 100) { healthStatus = "Warning" healthColor = "bg-yellow-500/10 text-yellow-500 border-yellow-500/20" } const allInterfaces = [ ...(networkData.physical_interfaces || []), ...(networkData.bridge_interfaces || []), ...(networkData.vm_lxc_interfaces || []), ] const vmLxcInterfaces = (networkData.vm_lxc_interfaces || []).sort((a, b) => { const vmidA = a.vmid ?? Number.MAX_SAFE_INTEGER const vmidB = b.vmid ?? Number.MAX_SAFE_INTEGER return vmidA - vmidB }) const topInterface = vmLxcInterfaces.length > 0 ? vmLxcInterfaces.reduce((top, iface) => { const ifaceTraffic = (iface.bytes_recv || 0) + (iface.bytes_sent || 0) const topTraffic = (top.bytes_recv || 0) + (top.bytes_sent || 0) return ifaceTraffic > topTraffic ? iface : top }, vmLxcInterfaces[0]) : { name: "No VM/LXC", type: "unknown", bytes_recv: 0, bytes_sent: 0, vm_name: "N/A" } const topInterfaceTraffic = (topInterface.bytes_recv || 0) + (topInterface.bytes_sent || 0) const getTimeframeLabel = () => { switch (timeframe) { case "hour": return "1 Hour" case "day": return "24 Hours" case "week": return "7 Days" case "month": return "30 Days" case "year": return "1 Year" default: return "24 Hours" } } return (
{/* Network Overview Cards */}
Network Traffic
Received: ↓ {trafficInFormatted}
Sent: ↑ {trafficOutFormatted}
Active Interfaces
{(networkData.physical_active_count ?? 0) + (networkData.bridge_active_count ?? 0)}
Physical: {networkData.physical_active_count ?? 0}/{networkData.physical_total_count ?? 0} Bridges: {networkData.bridge_active_count ?? 0}/{networkData.bridge_total_count ?? 0}

{(networkData.physical_total_count ?? 0) + (networkData.bridge_total_count ?? 0)} total interfaces

Top Interface
{topInterface.name}
{topInterface.vm_type ? ( {getVMTypeBadge(topInterface.vm_type).label} ) : ( {getInterfaceTypeBadge(topInterface.type).label} )} {topInterface.vm_name && topInterface.vm_name !== "N/A" && ( → {topInterface.vm_name} )}

Total traffic: {formatBytes(topInterfaceTraffic)}

Network Health {healthStatus}
Packet Loss: {avgPacketLoss}%
Errors: {totalErrors}
{/* Timeframe Selector */}
{/* Network Traffic Card with Chart */} Network Traffic {/* Physical Interfaces section */} Physical Interfaces {networkData.physical_active_count ?? 0}/{networkData.physical_total_count ?? 0} Active
{networkData.physical_interfaces.map((interface_, index) => { const typeBadge = getInterfaceTypeBadge(interface_.type) return (
setSelectedInterface(interface_)} > {/* First row: Icon, Name, Type Badge, Status */}
{interface_.name}
{typeBadge.label}
{interface_.status.toUpperCase()}
{/* Second row: Details - Responsive layout */}
IP Address
{interface_.addresses.length > 0 ? interface_.addresses[0].ip : "N/A"}
Speed
{formatSpeed(interface_.speed)}
Traffic since last boot
↓ {formatBytes(interface_.bytes_recv)} {" / "} ↑ {formatBytes(interface_.bytes_sent)}
{interface_.mac_address && (
MAC
{interface_.mac_address}
)}
) })}
{networkData.bridge_interfaces && networkData.bridge_interfaces.length > 0 && ( Bridge Interfaces {networkData.bridge_active_count ?? 0}/{networkData.bridge_total_count ?? 0} Active
{networkData.bridge_interfaces.map((interface_, index) => { const typeBadge = getInterfaceTypeBadge(interface_.type) return (
setSelectedInterface(interface_)} > {/* First row: Icon, Name, Type Badge, Physical Interface (responsive), Status */}
{interface_.name}
{typeBadge.label} {interface_.bridge_physical_interface && (
→ {interface_.bridge_physical_interface} {interface_.bridge_physical_interface.startsWith("bond") && networkData.physical_interfaces && ( <> {(() => { const bondInterface = networkData.physical_interfaces.find( (iface) => iface.name === interface_.bridge_physical_interface, ) if (bondInterface?.bond_slaves && bondInterface.bond_slaves.length > 0) { return ( ({bondInterface.bond_slaves.join(", ")}) ) } return null })()} )} {interface_.bridge_bond_slaves && interface_.bridge_bond_slaves.length > 0 && ( ({interface_.bridge_bond_slaves.join(", ")}) )}
)}
{interface_.status.toUpperCase()}
{/* Second row: Details - Responsive layout */}
IP Address
{interface_.addresses.length > 0 ? interface_.addresses[0].ip : "N/A"}
Speed
{formatSpeed(interface_.speed)}
Traffic since last boot
↓ {formatBytes(interface_.bytes_recv)} {" / "} ↑ {formatBytes(interface_.bytes_sent)}
{interface_.mac_address && (
MAC
{interface_.mac_address}
)}
) })}
)} {/* VM & LXC Network Interfaces section */} {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
{vmLxcInterfaces.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 since last boot
↓ {formatBytes(interface_.bytes_recv)} {" / "} ↑ {formatBytes(interface_.bytes_sent)}
{interface_.mac_address && (
MAC
{interface_.mac_address}
)}
) })}
)} {/* Interface Details Modal */} setSelectedInterface(null)}> {selectedInterface?.name} - Interface Details {selectedInterface?.status.toLowerCase() === "up" && selectedInterface?.vm_type !== "vm" && (
)}
{selectedInterface && (
{(() => { // Find the current interface data from modalNetworkData if available const currentInterfaceData = modalNetworkData ? [ ...(modalNetworkData.physical_interfaces || []), ...(modalNetworkData.bridge_interfaces || []), ...(modalNetworkData.vm_lxc_interfaces || []), ].find((iface) => iface.name === selectedInterface.name) : selectedInterface const displayInterface = currentInterfaceData || selectedInterface console.log("[v0] Selected Interface:", selectedInterface.name) console.log("[v0] Selected Interface bytes_recv:", selectedInterface.bytes_recv) console.log("[v0] Selected Interface bytes_sent:", selectedInterface.bytes_sent) console.log("[v0] Display Interface bytes_recv:", displayInterface.bytes_recv) console.log("[v0] Display Interface bytes_sent:", displayInterface.bytes_sent) console.log("[v0] Modal Network Data available:", !!modalNetworkData) return ( <> {/* Basic Information */}

Basic Information

Interface Name
{displayInterface.name}
Type
{getInterfaceTypeBadge(displayInterface.type).label}
{displayInterface.type === "bridge" && displayInterface.bridge_physical_interface && (
Physical Interface
{displayInterface.bridge_physical_interface}
{displayInterface.bridge_physical_interface.startsWith("bond") && modalNetworkData?.physical_interfaces && ( <> {(() => { const bondInterface = modalNetworkData.physical_interfaces.find( (iface) => iface.name === displayInterface.bridge_physical_interface, ) if (bondInterface?.bond_slaves && bondInterface.bond_slaves.length > 0) { return (
Bond Members
{bondInterface.bond_slaves.map((slave, idx) => ( {slave} ))}
) } return null })()} )} {displayInterface.bridge_bond_slaves && displayInterface.bridge_bond_slaves.length > 0 && (
Bond Members
{displayInterface.bridge_bond_slaves.map((slave, idx) => ( {slave} ))}
)}
)} {displayInterface.type === "vm_lxc" && displayInterface.vm_name && (
VM/LXC Name
{displayInterface.vm_name} {displayInterface.vm_type && ( {getVMTypeBadge(displayInterface.vm_type).label} )}
)}
Status
{displayInterface.status.toUpperCase()}
Speed
{formatSpeed(displayInterface.speed)}
Duplex
{displayInterface.duplex}
MTU
{displayInterface.mtu}
{displayInterface.mac_address && (
MAC Address
{displayInterface.mac_address}
)}
{/* IP Addresses */} {displayInterface.addresses.length > 0 && (

IP Addresses

{displayInterface.addresses.map((addr, idx) => (
{addr.ip}
Netmask: {addr.netmask}
))}
)} {/* Network Traffic Statistics - Only show if interface is UP and NOT a VM interface */} {displayInterface.status.toLowerCase() === "up" && displayInterface.vm_type !== "vm" ? (

Network Traffic Statistics ( {modalTimeframe === "hour" ? "Last Hour" : modalTimeframe === "day" ? "Last 24 Hours" : modalTimeframe === "week" ? "Last 7 Days" : modalTimeframe === "month" ? "Last 30 Days" : "Last Year"} )

{/* Traffic Data - Top Row */}
Bytes Received
{formatStorage(interfaceTotals.received * 1024 * 1024 * 1024)}
Bytes Sent
{formatStorage(interfaceTotals.sent * 1024 * 1024 * 1024)}
{/* Network Traffic Chart - Full Width Below */}
) : displayInterface.status.toLowerCase() === "up" && displayInterface.vm_type === "vm" ? (

Traffic since last boot

Bytes Received
{formatBytes(displayInterface.bytes_recv)}
Bytes Sent
{formatBytes(displayInterface.bytes_sent)}
Packets Received
{displayInterface.packets_recv?.toLocaleString() || "N/A"}
Packets Sent
{displayInterface.packets_sent?.toLocaleString() || "N/A"}
Errors In
{displayInterface.errors_in || 0}
Errors Out
{displayInterface.errors_out || 0}
Drops In
{displayInterface.drops_in || 0}
Drops Out
{displayInterface.drops_out || 0}
{displayInterface.packet_loss_in !== undefined && (
Packet Loss In
1 ? "text-red-500" : "text-green-500"}`} > {displayInterface.packet_loss_in}%
)} {displayInterface.packet_loss_out !== undefined && (
Packet Loss Out
1 ? "text-red-500" : "text-green-500"}`} > {displayInterface.packet_loss_out}%
)}
) : (

Interface Inactive

This interface is currently down. Network traffic statistics are not available.

)} {/* Cumulative Statistics - Only show if interface is UP and NOT a VM interface */} {displayInterface.status.toLowerCase() === "up" && displayInterface.vm_type !== "vm" && (

Cumulative Statistics (Since Last Boot)

Bytes Received
{formatBytes(displayInterface.bytes_recv)}
Bytes Sent
{formatBytes(displayInterface.bytes_sent)}
Packets Received
{displayInterface.packets_recv?.toLocaleString() || "N/A"}
Packets Sent
{displayInterface.packets_sent?.toLocaleString() || "N/A"}
Errors In
{displayInterface.errors_in || 0}
Errors Out
{displayInterface.errors_out || 0}
Drops In
{displayInterface.drops_in || 0}
Drops Out
{displayInterface.drops_out || 0}
{displayInterface.packet_loss_in !== undefined && (
Packet Loss In
1 ? "text-red-500" : "text-green-500"}`} > {displayInterface.packet_loss_in}%
)} {displayInterface.packet_loss_out !== undefined && (
Packet Loss Out
1 ? "text-red-500" : "text-green-500"}`} > {displayInterface.packet_loss_out}%
)}
)} {/* Bond Information */} {displayInterface.type === "bond" && displayInterface.bond_slaves && (

Bond Configuration

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

Bridge Configuration

Virtual Member Interfaces
{displayInterface.bridge_members.length > 0 ? ( displayInterface.bridge_members .filter( (member) => !member.startsWith("enp") && !member.startsWith("eth") && !member.startsWith("eno") && !member.startsWith("ens") && !member.startsWith("wlan") && !member.startsWith("wlp"), ) .map((member, idx) => ( {member} )) ) : (
No virtual members
)}
)} ) })()}
)}
) }