"use client"
import { useEffect, useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Badge } from "./ui/badge"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog"
import { Wifi, Activity, Network, Router, AlertCircle, Zap, Timer, EthernetPort, ArrowDown, ArrowUp, Box } from 'lucide-react'
import useSWR from "swr"
import { NetworkTrafficChart } from "./network-traffic-chart"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { fetchApi } from "../lib/api-config"
import { formatNetworkTraffic, getNetworkUnit } from "../lib/format-network"
import { LatencyDetailModal } from "./latency-detail-modal"
import { AreaChart, Area, LineChart, Line, ResponsiveContainer, YAxis } from "recharts"
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
hostname?: string
domain?: string
dns_servers?: string[]
}
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
// Live rate (bytes/sec) computed by the backend as the delta
// between this poll and the previous one. Present from the second
// /api/network response onward; absent on the first call after the
// service starts or after a long pause.
rx_Bps?: number
tx_Bps?: 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
}
// Same dot-prefix tone the Storage cards use, so a "no errors" /
// "errors present" cue reads identically across pages.
const NetStatusDot = ({ tone }: { tone: "ok" | "warn" | "fail" }) => {
const cls =
tone === "ok" ? "bg-green-500" : tone === "warn" ? "bg-yellow-500" : "bg-red-500"
return
}
const netCounterTone = (n: number | null | undefined): "ok" | "warn" | "fail" => {
if (!n || n <= 0) return "ok"
if (n < 10) return "warn"
return "fail"
}
// Icon picker — defaults to the actual port type rather than a Wi-Fi
// glyph for everything. Wireless interfaces (wl*/wifi*) keep the Wi-Fi
// glyph; wired NICs use EthernetPort; bonds/bridges/vlans get more
// specific icons so the operator can tell them apart at a glance.
function getInterfaceIcon(iface: NetworkInterface): React.ComponentType<{ className?: string }> {
const name = (iface.name || "").toLowerCase()
const type = (iface.type || "").toLowerCase()
if (name.startsWith("wl") || name.startsWith("wifi")) return Wifi
if (type === "bridge") return Network
if (type === "bond") return Router
if (type === "vlan") return Activity
if (type === "vm_lxc" || type === "virtual") return Box
// Physical wired NIC (eth0, enp*, ens*, eno*, nic0, …) → ethernet port.
return EthernetPort
}
// Match the dark blue badge tone the Storage card uses for the disk
// type chip, but mapped to the actual interface class.
function getInterfaceTypeChip(type: string) {
switch ((type || "").toLowerCase()) {
case "physical":
return { className: "bg-blue-500/10 text-blue-400 border-blue-500/20", label: "Physical" }
case "bridge":
return { className: "bg-green-500/10 text-green-400 border-green-500/20", label: "Bridge" }
case "bond":
return { className: "bg-purple-500/10 text-purple-400 border-purple-500/20", label: "Bond" }
case "vlan":
return { className: "bg-cyan-500/10 text-cyan-400 border-cyan-500/20", label: "VLAN" }
case "vm_lxc":
case "virtual":
return { className: "bg-orange-500/10 text-orange-400 border-orange-500/20", label: "Virtual" }
default:
return { className: "bg-gray-500/10 text-gray-400 border-gray-500/20", label: type || "Unknown" }
}
}
// Per-interface card matching the Storage page's "Physical Disks"
// pattern: 2-line header (identity / live state), horizontal divider,
// vertical key→value stat block, footer with serial + arrow CTA.
// Replaces the row-style block that was unchanged since 1.0.0.
function renderPhysicalInterfaceCardV2(
iface: NetworkInterface,
onOpen: (iface: NetworkInterface) => void,
) {
const Icon = getInterfaceIcon(iface)
const chip = getInterfaceTypeChip(iface.type)
const isUp = (iface.status || "").toLowerCase() === "up"
const firstAddr = iface.addresses?.[0]?.ip || ""
const extraAddrs = Math.max(0, (iface.addresses?.length || 0) - 1)
const speedStr = formatSpeed(iface.speed)
const errIn = iface.errors_in ?? 0
const errOut = iface.errors_out ?? 0
const dropIn = iface.drops_in ?? 0
const dropOut = iface.drops_out ?? 0
const totalErrors = errIn + errOut
const totalDrops = dropIn + dropOut
return (
{/* Header L2: speed | duplex (compact, live link info). */}
{speedStr}
{iface.duplex || "—"}
{/* Separator. */}
{/* Stats: key uppercase left · value right. */}
{firstAddr && (
IP
{firstAddr}{extraAddrs > 0 ? ` (+${extraAddrs})` : ""}
)}
MTU{iface.mtu || "—"}
{/* Live RX/TX rate. Same wording the Network Traffic chart
uses ("Received" / "Sent") and the same canonical colours
(green for Received, blue for Sent). Falls back to "—"
until the backend has a delta — first poll after start
has no previous sample to compute against. */}
Received
{iface.rx_Bps !== undefined ? formatRate(iface.rx_Bps) : "—"}
Sent
{iface.tx_Bps !== undefined ? formatRate(iface.tx_Bps) : "—"}