diff --git a/AppImage/components/network-card.tsx b/AppImage/components/network-card.tsx new file mode 100644 index 0000000..3c00c28 --- /dev/null +++ b/AppImage/components/network-card.tsx @@ -0,0 +1,251 @@ +"use client" + +import { Card, CardContent } from "./ui/card" +import { Badge } from "./ui/badge" +import { Wifi, Zap } from "lucide-react" +import { useState, useEffect } from "react" + +interface NetworkCardProps { + interface_: { + 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 + bridge_physical_interface?: string + bridge_bond_slaves?: string[] + vmid?: number + vm_name?: string + vm_type?: string + } + timeframe: "hour" | "day" | "week" | "month" | "year" + onClick?: () => void +} + +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 formatSpeed = (speed: number): string => { + if (speed === 0) return "N/A" + if (speed >= 1000) return `${(speed / 1000).toFixed(1)} Gbps` + return `${speed} Mbps` +} + +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) + const decimals = value >= 10 ? 1 : 2 + return `${value.toFixed(decimals)} ${sizes[i]}` +} + +export function NetworkCard({ interface_, timeframe, onClick }: NetworkCardProps) { + const typeBadge = getInterfaceTypeBadge(interface_.type) + const vmTypeBadge = interface_.vm_type ? getVMTypeBadge(interface_.vm_type) : null + + const [trafficData, setTrafficData] = useState<{ received: number; sent: number }>({ + received: 0, + sent: 0, + }) + + useEffect(() => { + const fetchTrafficData = async () => { + try { + const response = await fetch(`/api/network/${interface_.name}/metrics?timeframe=${timeframe}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + signal: AbortSignal.timeout(5000), + }) + + if (!response.ok) { + throw new Error(`Failed to fetch traffic data: ${response.status}`) + } + + const data = await response.json() + + // Calculate totals from the data points + if (data.data && data.data.length > 0) { + const lastPoint = data.data[data.data.length - 1] + const firstPoint = data.data[0] + + // Calculate the difference between last and first data points + const receivedGB = Math.max(0, (lastPoint.netin || 0) - (firstPoint.netin || 0)) + const sentGB = Math.max(0, (lastPoint.netout || 0) - (firstPoint.netout || 0)) + + setTrafficData({ + received: receivedGB, + sent: sentGB, + }) + } + } catch (error) { + console.error("[v0] Failed to fetch traffic data for card:", error) + // Keep showing 0 values on error + setTrafficData({ received: 0, sent: 0 }) + } + } + + // Only fetch if interface is up and not a VM + if (interface_.status.toLowerCase() === "up" && interface_.vm_type !== "vm") { + fetchTrafficData() + + // Refresh every 60 seconds + const interval = setInterval(fetchTrafficData, 60000) + return () => clearInterval(interval) + } + }, [interface_.name, interface_.status, interface_.vm_type, timeframe]) + + const getTimeframeLabel = () => { + switch (timeframe) { + case "hour": + return "Last Hour" + case "day": + return "Last 24 Hours" + case "week": + return "Last 7 Days" + case "month": + return "Last 30 Days" + case "year": + return "Last Year" + default: + return "Last 24 Hours" + } + } + + return ( + + +
+ {/* First row: Icon, Name, Type Badge, Status */} +
+ +
+
{interface_.name}
+ {vmTypeBadge ? ( + + {vmTypeBadge.label} + + ) : ( + + {typeBadge.label} + + )} + {interface_.vm_name && ( +
→ {interface_.vm_name}
+ )} + {interface_.type === "bridge" && interface_.bridge_physical_interface && ( +
+ → {interface_.bridge_physical_interface} +
+ )} +
+ + {interface_.status.toUpperCase()} + +
+ + {/* Second row: Details - Responsive layout */} +
+
+
+ {interface_.type === "vm_lxc" ? "VMID" : "IP Address"} +
+
+ {interface_.type === "vm_lxc" + ? (interface_.vmid ?? "N/A") + : interface_.addresses.length > 0 + ? interface_.addresses[0].ip + : "N/A"} +
+
+ +
+
Speed
+
+ + {formatSpeed(interface_.speed)} +
+
+ +
+
{getTimeframeLabel()}
+
+ {interface_.status.toLowerCase() === "up" && interface_.vm_type !== "vm" ? ( + <> + ↓ {formatStorage(trafficData.received * 1024 * 1024 * 1024)} + {" / "} + ↑ {formatStorage(trafficData.sent * 1024 * 1024 * 1024)} + + ) : ( + <> + ↓ {formatBytes(interface_.bytes_recv)} + {" / "} + ↑ {formatBytes(interface_.bytes_sent)} + + )} +
+
+ + {interface_.mac_address && ( +
+
MAC
+
{interface_.mac_address}
+
+ )} +
+
+
+
+ ) +} diff --git a/AppImage/components/network-metrics.tsx b/AppImage/components/network-metrics.tsx index 717df85..293ee51 100644 --- a/AppImage/components/network-metrics.tsx +++ b/AppImage/components/network-metrics.tsx @@ -892,6 +892,37 @@ export function NetworkMetrics() { refreshInterval={60000} /> + +
+
+
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.status.toLowerCase() === "up" && displayInterface.vm_type === "vm" ? ( @@ -938,26 +969,6 @@ export function NetworkMetrics() {
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}% -
-
- )} ) : ( @@ -970,77 +981,6 @@ export function NetworkMetrics() { )} - {/* 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 && (