2025-10-06 22:23:56 +02:00
"use client"
2025-10-05 20:45:54 +02:00
2025-10-06 22:23:56 +02:00
import { Card } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
2025-10-23 15:31:25 +02:00
import { Progress } from "@/components/ui/progress"
2025-10-06 22:23:56 +02:00
import { Dialog , DialogContent , DialogDescription , DialogHeader , DialogTitle } from "@/components/ui/dialog"
2026-04-17 19:53:17 +02:00
import { Cpu , HardDrive , Thermometer , Zap , Loader2 , CpuIcon , Cpu as Gpu , Network , MemoryStick , PowerIcon , FanIcon , Battery , Usb , BrainCircuit , AlertCircle } from "lucide-react"
2025-11-30 19:15:07 +01:00
import { Download } from "lucide-react"
import { Button } from "@/components/ui/button"
2026-06-24 23:40:22 +02:00
import { getDiskType } from "../lib/disk-type"
2025-10-06 22:23:56 +02:00
import useSWR from "swr"
2025-10-14 12:58:53 +02:00
import { useState , useEffect } from "react"
2025-11-25 19:04:54 +01:00
import {
type HardwareData ,
type GPU ,
type PCIDevice ,
type StorageDevice ,
2026-04-17 19:53:17 +02:00
type CoralTPU ,
type UsbDevice ,
2025-11-25 19:04:54 +01:00
fetcher as swrFetcher ,
} from "../types/hardware"
import { fetchApi } from "@/lib/api-config"
2025-12-01 01:04:31 +01:00
import { ScriptTerminalModal } from "./script-terminal-modal"
2026-04-09 15:02:25 +02:00
import { GpuSwitchModeIndicator } from "./gpu-switch-mode-indicator"
2026-04-09 19:27:45 +02:00
import { Settings2 , CheckCircle2 } from "lucide-react"
2025-10-06 22:23:56 +02:00
2025-10-23 14:38:17 +02:00
const parseLsblkSize = ( sizeStr : string | undefined ) : number => {
if ( ! sizeStr ) return 0
// Remove spaces and convert to uppercase
const cleaned = sizeStr . trim (). toUpperCase ()
// Extract number and unit
2025-11-25 19:04:54 +01:00
const match = cleaned . match ( /^([\d.]+)([KMGT]?)$/ )
2025-10-23 14:38:17 +02:00
if ( ! match ) return 0
const value = Number . parseFloat ( match [ 1 ])
const unit = match [ 2 ] || "K" // Default to KB if no unit
// Convert to KB
switch ( unit ) {
case "K" :
return value
case "M" :
return value * 1024
case "G" :
return value * 1024 * 1024
case "T" :
return value * 1024 * 1024 * 1024
default :
return value
}
}
2025-10-10 00:27:22 +02:00
const formatMemory = ( memoryKB : number | string ) : string => {
const kb = typeof memoryKB === "string" ? Number . parseFloat ( memoryKB ) : memoryKB
2025-10-10 00:13:54 +02:00
2025-10-10 00:27:22 +02:00
if ( isNaN ( kb )) return "N/A"
// Convert KB to MB
const mb = kb / 1024
2025-10-10 00:13:54 +02:00
2025-10-11 00:00:21 +02:00
// Convert to TB if >= 1024 GB
if ( mb >= 1024 * 1024 ) {
const tb = mb / ( 1024 * 1024 )
return ` ${ tb . toFixed ( 1 ) } TB`
}
2025-10-10 00:13:54 +02:00
if ( mb >= 1024 ) {
const gb = mb / 1024
2025-11-04 12:47:26 +01:00
// If GB value is greater than 999, convert to TB
if ( gb > 999 ) {
return ` ${ ( gb / 1024 ). toFixed ( 2 ) } TB`
}
2025-10-10 00:13:54 +02:00
return ` ${ gb . toFixed ( 1 ) } GB`
}
// Keep in MB if < 1024 MB
return ` ${ mb . toFixed ( 0 ) } MB`
}
2025-10-10 21:18:49 +02:00
const formatClock = ( clockString : string | number ) : string => {
let mhz : number
2025-10-10 00:13:54 +02:00
2025-10-10 21:18:49 +02:00
if ( typeof clockString === "number" ) {
mhz = clockString
} else {
// Extract numeric value from string like "1138.179107 MHz"
const match = clockString . match ( /([\d.]+)\s*MHz/i )
if ( ! match ) return clockString
mhz = Number . parseFloat ( match [ 1 ])
}
if ( isNaN ( mhz )) return String ( clockString )
2025-10-10 00:13:54 +02:00
// Convert to GHz if >= 1000 MHz
if ( mhz >= 1000 ) {
const ghz = mhz / 1000
return ` ${ ghz . toFixed ( 2 ) } GHz`
}
// Keep in MHz if < 1000 MHz
return ` ${ mhz . toFixed ( 0 ) } MHz`
}
2025-10-06 22:23:56 +02:00
const getDeviceTypeColor = ( type : string ) : string => {
const lowerType = type . toLowerCase ()
2026-04-17 20:56:07 +02:00
// UPS / battery — amber: warm orange-yellow, distinct from the orange used
// for Storage and avoids the "warning" connotation of pure yellow.
if ( lowerType === "ups" || lowerType . includes ( "battery" )) {
return "bg-amber-500/10 text-amber-500 border-amber-500/20"
}
// Storage family — orange (Mass Storage USB class + PCI storage controllers)
2025-10-06 22:23:56 +02:00
if ( lowerType . includes ( "storage" ) || lowerType . includes ( "sata" ) || lowerType . includes ( "raid" )) {
return "bg-orange-500/10 text-orange-500 border-orange-500/20"
}
2026-04-17 20:56:07 +02:00
// Printer — rose, unmistakable
if ( lowerType . includes ( "printer" )) {
return "bg-rose-500/10 text-rose-500 border-rose-500/20"
}
// Audio family — teal (Audio, Audio/Video); placed before video so that
// combined "Audio/Video" class labels read as audio-family.
if ( lowerType . includes ( "audio" )) {
return "bg-teal-500/10 text-teal-500 border-teal-500/20"
}
// Graphics / Video / Imaging — green (cameras, webcams, displays, GPUs).
if (
lowerType . includes ( "graphics" ) ||
lowerType . includes ( "vga" ) ||
lowerType . includes ( "display" ) ||
lowerType . includes ( "video" ) ||
lowerType . includes ( "imaging" )
) {
return "bg-green-500/10 text-green-500 border-green-500/20"
}
// Network family — blue (Ethernet / Wi-Fi PCI controllers, USB Communications,
// CDC Data, Wireless Controllers like Bluetooth dongles).
if (
lowerType . includes ( "network" ) ||
lowerType . includes ( "ethernet" ) ||
lowerType . includes ( "communications" ) ||
lowerType . includes ( "wireless" ) ||
lowerType === "cdc data"
) {
return "bg-blue-500/10 text-blue-500 border-blue-500/20"
}
// HID — purple: keyboards, mice, game controllers.
if ( lowerType === "hid" ) {
return "bg-purple-500/10 text-purple-500 border-purple-500/20"
}
// USB host controllers (PCI-level) keep the existing purple identity.
2025-10-06 22:23:56 +02:00
if ( lowerType . includes ( "usb" )) {
return "bg-purple-500/10 text-purple-500 border-purple-500/20"
}
2026-04-17 20:56:07 +02:00
// Smart Card, Billboard, Diagnostic, Hub, Physical, Content Security,
// Personal Healthcare, Miscellaneous, Application/Vendor Specific, unknown.
2025-10-06 22:23:56 +02:00
return "bg-gray-500/10 text-gray-500 border-gray-500/20"
}
2025-10-06 22:58:54 +02:00
const getMonitoringToolRecommendation = ( vendor : string ) : string => {
const lowerVendor = vendor . toLowerCase ()
if ( lowerVendor . includes ( "intel" )) {
return "To get extended GPU monitoring information, please install intel-gpu-tools or igt-gpu-tools package."
}
if ( lowerVendor . includes ( "nvidia" )) {
2025-10-06 23:40:54 +02:00
return "For NVIDIA GPUs, real-time monitoring requires the proprietary drivers (nvidia-driver package). Install them only if your GPU is used directly by the host."
2025-10-06 22:58:54 +02:00
}
2025-10-06 23:40:54 +02:00
2025-10-06 22:58:54 +02:00
if ( lowerVendor . includes ( "amd" ) || lowerVendor . includes ( "ati" )) {
2025-10-14 18:51:39 +02:00
return "To get extended GPU monitoring information for AMD GPUs, please install amdgpu_top. You can download it from: https://github.com/Umio-Yasuno/amdgpu_top"
2025-10-06 22:58:54 +02:00
}
return "To get extended GPU monitoring information, please install the appropriate GPU monitoring tools for your hardware."
}
2025-10-14 17:23:59 +02:00
const groupAndSortTemperatures = ( temperatures : any []) => {
const groups = {
CPU : [] as any [],
GPU : [] as any [],
NVME : [] as any [],
PCI : [] as any [],
OTHER : [] as any [],
}
temperatures . forEach (( temp ) => {
const nameLower = temp . name . toLowerCase ()
const adapterLower = temp . adapter ? . toLowerCase () || ""
2025-10-24 23:15:53 +02:00
if ( nameLower . includes ( "cpu" ) || nameLower . includes ( "core" ) || nameLower . includes ( "package" )) {
2025-10-14 17:23:59 +02:00
groups . CPU . push ( temp )
} else if ( nameLower . includes ( "gpu" ) || adapterLower . includes ( "gpu" )) {
groups . GPU . push ( temp )
} else if ( nameLower . includes ( "nvme" ) || adapterLower . includes ( "nvme" )) {
groups . NVME . push ( temp )
} else if ( adapterLower . includes ( "pci" )) {
groups . PCI . push ( temp )
} else {
groups . OTHER . push ( temp )
}
})
return groups
}
2025-10-06 22:23:56 +02:00
export default function Hardware() {
2026-04-17 16:39:08 +02:00
// Static data - loaded once on mount. Static fields (CPU, motherboard, memory
// modules, PCI, disks, GPU list) don't change at runtime, so no auto-refresh.
// `mutateStatic` is triggered explicitly after GPU switch-mode changes.
2025-10-14 19:00:24 +02:00
const {
2025-11-07 13:41:39 +01:00
data : staticHardwareData ,
error : staticError ,
isLoading : staticLoading ,
2026-04-17 16:39:08 +02:00
mutate : mutateStatic ,
2025-11-25 19:04:54 +01:00
} = useSWR < HardwareData >( "/api/hardware" , swrFetcher , {
2025-11-07 13:41:39 +01:00
revalidateOnFocus : false ,
revalidateOnReconnect : false ,
2026-04-17 16:39:08 +02:00
refreshInterval : 0 ,
2025-10-06 22:23:56 +02:00
})
2026-04-17 16:39:08 +02:00
// Live data - only temperatures, fans, power, UPS. Polled every 5s.
// Backend /api/hardware/live uses cached ipmitool output (10s) so this is cheap.
2025-11-07 13:41:39 +01:00
const {
data : dynamicHardwareData ,
error : dynamicError ,
2026-04-17 16:39:08 +02:00
} = useSWR < HardwareData >( "/api/hardware/live" , swrFetcher , {
refreshInterval : 5000 ,
revalidateOnFocus : true ,
revalidateOnReconnect : true ,
dedupingInterval : 2000 ,
2025-11-07 13:41:39 +01:00
})
2026-04-17 16:39:08 +02:00
// Merge: static fields from initial load, live fields from the 5s poll.
2026-04-17 19:53:17 +02:00
// coral_tpus and usb_devices live in the dynamic payload so that the
2026-04-17 21:55:53 +02:00
// "Install Drivers" button disappears immediately after install_coral.sh
2026-04-17 19:53:17 +02:00
// finishes, without requiring a page reload.
2025-11-07 13:41:39 +01:00
const hardwareData = staticHardwareData
? {
2026-04-17 16:39:08 +02:00
... staticHardwareData ,
temperatures : dynamicHardwareData?.temperatures ?? staticHardwareData . temperatures ,
fans : dynamicHardwareData?.fans ?? staticHardwareData . fans ,
power_meter : dynamicHardwareData?.power_meter ?? staticHardwareData . power_meter ,
power_supplies : dynamicHardwareData?.power_supplies ?? staticHardwareData . power_supplies ,
ups : dynamicHardwareData?.ups ?? staticHardwareData . ups ,
2026-04-17 19:53:17 +02:00
coral_tpus : dynamicHardwareData?.coral_tpus ?? staticHardwareData . coral_tpus ,
usb_devices : dynamicHardwareData?.usb_devices ?? staticHardwareData . usb_devices ,
2025-11-07 13:41:39 +01:00
}
: undefined
const error = staticError || dynamicError
const isLoading = staticLoading
2025-11-04 14:00:01 +01:00
useEffect (() => {
if ( hardwareData ? . storage_devices ) {
hardwareData . storage_devices . forEach (( device ) => {
if ( device . name . startsWith ( "nvme" )) {
2026-05-30 21:54:32 +02:00
console . log ( `NVMe device ${ device . name } :` , {
2025-11-04 14:00:01 +01:00
pcie_gen : device.pcie_gen ,
pcie_width : device.pcie_width ,
pcie_max_gen : device.pcie_max_gen ,
pcie_max_width : device.pcie_max_width ,
})
}
})
}
}, [ hardwareData ])
2026-05-09 18:59:59 +02:00
const [ managedInstalls , setManagedInstalls ] = useState < Array < {
id : string
type : string
name ?: string
current_version ?: string | null
menu_label ?: string | null
update_check ?: {
available : boolean
latest ?: string | null
last_check ?: string | null
error ?: string | null
} | null
}> > ([])
useEffect (() => {
let cancelled = false
fetchApi < { success : boolean ; items : any [] } > ( "/api/managed-installs" )
. then (( res ) => {
if ( cancelled ) return
if ( res ? . success && Array . isArray ( res . items )) {
setManagedInstalls ( res . items )
}
})
. catch (() => {})
return () => { cancelled = true }
}, [])
const nvidiaInstall = managedInstalls . find (( it ) => it . type === "nvidia_xfree86" )
const formatLastChecked = ( iso? : string | null ) : string => {
if ( ! iso ) return "never"
const d = new Date ( iso )
if ( isNaN ( d . getTime ())) return "unknown"
const now = Date . now ()
const ageMs = now - d . getTime ()
const sameDay = new Date ( now ). toDateString () === d . toDateString ()
const yesterday = new Date ( now - 86 _400_000 ). toDateString () === d . toDateString ()
const time = d . toLocaleTimeString ([], { hour : "2-digit" , minute : "2-digit" })
if ( sameDay ) return time
if ( yesterday ) return `yesterday ${ time } `
if ( ageMs < 7 * 86 _400_000 ) {
return d . toLocaleDateString ([], { weekday : "short" }) + " " + time
}
return d . toLocaleDateString ([], { month : "short" , day : "numeric" })
}
2025-10-06 22:23:56 +02:00
const [ selectedGPU , setSelectedGPU ] = useState < GPU | null >( null )
2025-10-07 02:46:35 +02:00
const [ realtimeGPUData , setRealtimeGPUData ] = useState < any >( null )
2025-10-08 12:06:24 +02:00
const [ detailsLoading , setDetailsLoading ] = useState ( false )
2025-10-06 22:23:56 +02:00
const [ selectedPCIDevice , setSelectedPCIDevice ] = useState < PCIDevice | null >( null )
const [ selectedDisk , setSelectedDisk ] = useState < StorageDevice | null >( null )
const [ selectedNetwork , setSelectedNetwork ] = useState < PCIDevice | null >( null )
2025-10-14 18:51:39 +02:00
const [ selectedUPS , setSelectedUPS ] = useState < any >( null )
2025-12-01 01:04:31 +01:00
const [ showNvidiaInstaller , setShowNvidiaInstaller ] = useState ( false )
2025-11-30 19:15:07 +01:00
const [ installingNvidiaDriver , setInstallingNvidiaDriver ] = useState ( false )
2026-01-29 19:04:52 +01:00
const [ showAmdInstaller , setShowAmdInstaller ] = useState ( false )
2026-01-29 20:03:39 +01:00
const [ showIntelInstaller , setShowIntelInstaller ] = useState ( false )
2026-04-17 19:53:17 +02:00
const [ showCoralInstaller , setShowCoralInstaller ] = useState ( false )
const [ selectedCoral , setSelectedCoral ] = useState < CoralTPU | null >( null )
const [ selectedUsbDevice , setSelectedUsbDevice ] = useState < UsbDevice | null >( null )
2026-04-09 15:02:25 +02:00
// GPU Switch Mode states
const [ editingSwitchModeGpu , setEditingSwitchModeGpu ] = useState < string | null >( null ) // GPU slot being edited
const [ pendingSwitchModes , setPendingSwitchModes ] = useState < Record < string , "lxc" | "vm" >>({})
const [ showSwitchModeModal , setShowSwitchModeModal ] = useState ( false )
2026-04-09 19:58:54 +02:00
const [ switchModeParams , setSwitchModeParams ] = useState < { gpuSlot : string ; targetMode : "lxc" | "vm" } | null > ( null )
2025-10-05 22:46:14 +02:00
2026-04-19 12:26:52 +02:00
// Determine GPU mode based on driver (vfio-pci = VM, native driver = LXC).
// SR-IOV short-circuits the driver check: if the GPU is either a VF or a
// PF with active VFs, the slot is in a hardware-partitioned state that
// ProxMenux does not manage from the UI, so it's surfaced as its own mode.
const getGpuSwitchMode = ( gpu : GPU ) : "lxc" | "vm" | "sriov" | "unknown" => {
if ( gpu . sriov_role === "vf" || gpu . sriov_role === "pf-active" ) return "sriov"
2026-04-09 15:02:25 +02:00
const driver = gpu . pci_driver ? . toLowerCase () || ""
2026-04-09 15:42:05 +02:00
const kernelModule = gpu . pci_kernel_module ? . toLowerCase () || ""
2026-04-19 12:26:52 +02:00
2026-04-09 15:42:05 +02:00
// Check driver first
2026-04-09 15:02:25 +02:00
if ( driver === "vfio-pci" ) return "vm"
2026-04-09 15:42:05 +02:00
if ( driver === "nvidia" || driver === "amdgpu" || driver === "radeon" || driver === "i915" || driver === "xe" || driver === "nouveau" || driver === "mgag200" ) return "lxc"
if ( driver && driver !== "none" && driver !== "" ) return "lxc"
// Fallback to kernel module if no driver
if ( kernelModule . includes ( "vfio" )) return "vm"
if ( kernelModule . includes ( "nvidia" ) || kernelModule . includes ( "amdgpu" ) || kernelModule . includes ( "radeon" ) || kernelModule . includes ( "i915" ) || kernelModule . includes ( "xe" ) || kernelModule . includes ( "nouveau" ) || kernelModule . includes ( "mgag200" )) return "lxc"
if ( kernelModule && kernelModule !== "none" && kernelModule !== "" ) return "lxc"
2026-04-09 15:02:25 +02:00
return "unknown"
}
const handleSwitchModeEdit = ( gpuSlot : string , e : React.MouseEvent ) => {
e . stopPropagation () // Prevent opening GPU modal
setEditingSwitchModeGpu ( gpuSlot )
}
const handleSwitchModeToggle = ( gpu : GPU , e? : React.MouseEvent ) => {
const slot = gpu . slot
const currentMode = getGpuSwitchMode ( gpu )
const pendingMode = pendingSwitchModes [ slot ]
// Toggle between modes
if ( pendingMode ) {
// Already has pending - toggle it
const newMode = pendingMode === "lxc" ? "vm" : "lxc"
if ( newMode === currentMode ) {
// Back to original - remove pending
const newPending = { ... pendingSwitchModes }
delete newPending [ slot ]
setPendingSwitchModes ( newPending )
} else {
setPendingSwitchModes ({ ... pendingSwitchModes , [ slot ] : newMode })
}
} else {
// No pending - set opposite of current
const newMode = currentMode === "lxc" ? "vm" : "lxc"
setPendingSwitchModes ({ ... pendingSwitchModes , [ slot ] : newMode })
}
}
const handleSwitchModeSave = ( gpuSlot : string , e : React.MouseEvent ) => {
e . stopPropagation ()
const pendingMode = pendingSwitchModes [ gpuSlot ]
2026-04-17 16:39:08 +02:00
const gpu = hardwareData ? . gpus ? . find ( g => g . slot === gpuSlot )
2026-04-09 15:02:25 +02:00
const currentMode = gpu ? getGpuSwitchMode ( gpu ) : "unknown"
2026-04-09 19:58:54 +02:00
if ( pendingMode && pendingMode !== currentMode && gpu ) {
// Mode has changed - save params and launch the script
setSwitchModeParams ({
gpuSlot : gpu.slot ,
targetMode : pendingMode
})
2026-04-09 15:02:25 +02:00
setShowSwitchModeModal ( true )
}
setEditingSwitchModeGpu ( null )
}
const handleSwitchModeCancel = ( gpuSlot : string , e : React.MouseEvent ) => {
e . stopPropagation ()
// Remove pending change for this GPU
const newPending = { ... pendingSwitchModes }
delete newPending [ gpuSlot ]
setPendingSwitchModes ( newPending )
setEditingSwitchModeGpu ( null )
}
const handleSwitchModeModalClose = () => {
setShowSwitchModeModal ( false )
2026-04-09 19:58:54 +02:00
// Clear params and pending changes after script runs
setSwitchModeParams ( null )
2026-04-09 15:02:25 +02:00
setPendingSwitchModes ({})
// Refresh hardware data
2026-04-17 16:39:08 +02:00
mutateStatic ()
2026-04-09 15:02:25 +02:00
}
2025-12-01 01:04:31 +01:00
const handleInstallNvidiaDriver = () => {
setShowNvidiaInstaller ( true )
2025-11-30 19:40:42 +01:00
}
2026-01-29 19:04:52 +01:00
const handleInstallAmdTools = () => {
setShowAmdInstaller ( true )
}
2026-01-29 20:03:39 +01:00
const handleInstallIntelTools = () => {
setShowIntelInstaller ( true )
}
2025-10-14 12:58:53 +02:00
useEffect (() => {
if ( ! selectedGPU ) return
const pciDevice = findPCIDeviceForGPU ( selectedGPU )
const fullSlot = pciDevice ? . slot || selectedGPU . slot
2025-10-14 15:34:19 +02:00
if ( ! fullSlot ) return
2025-10-14 12:58:53 +02:00
const abortController = new AbortController ()
const fetchRealtimeData = async () => {
try {
2025-11-25 19:04:54 +01:00
const data = await fetchApi ( `/api/gpu/ ${ fullSlot } /realtime` )
2025-10-14 12:58:53 +02:00
setRealtimeGPUData ( data )
setDetailsLoading ( false )
} catch ( error ) {
if ( error instanceof Error && error . name !== "AbortError" ) {
2026-05-30 21:54:32 +02:00
console . error ( "Error fetching GPU realtime data:" , error )
2025-10-14 12:58:53 +02:00
}
setRealtimeGPUData ({ has_monitoring_tool : false })
setDetailsLoading ( false )
}
}
fetchRealtimeData ()
const interval = setInterval ( fetchRealtimeData , 3000 )
return () => {
clearInterval ( interval )
abortController . abort ()
}
2025-10-14 15:34:19 +02:00
}, [ selectedGPU ])
2025-10-09 23:14:47 +02:00
2025-10-07 02:46:35 +02:00
const handleGPUClick = async ( gpu : GPU ) => {
2025-10-08 12:06:24 +02:00
setSelectedGPU ( gpu )
setDetailsLoading ( true )
setRealtimeGPUData ( null )
2025-10-07 02:46:35 +02:00
}
2025-10-07 00:50:17 +02:00
2025-10-06 23:26:26 +02:00
const findPCIDeviceForGPU = ( gpu : GPU ) : PCIDevice | null => {
2026-04-17 16:39:08 +02:00
if ( ! hardwareData ? . pci_devices || ! gpu . slot ) return null
2025-10-06 23:26:26 +02:00
// Try to find exact match first (e.g., "00:02.0")
2026-04-17 16:39:08 +02:00
let pciDevice = hardwareData . pci_devices . find (( d ) => d . slot === gpu . slot )
2025-10-06 23:26:26 +02:00
// If not found, try to match by partial slot (e.g., "00" matches "00:02.0")
if ( ! pciDevice && gpu . slot . length <= 2 ) {
2026-04-17 16:39:08 +02:00
pciDevice = hardwareData . pci_devices . find (
2025-10-06 23:26:26 +02:00
( d ) =>
d . slot . startsWith ( gpu . slot + ":" ) &&
( d . type . toLowerCase (). includes ( "vga" ) ||
d . type . toLowerCase (). includes ( "graphics" ) ||
d . type . toLowerCase (). includes ( "display" )),
)
}
return pciDevice || null
}
2025-10-07 02:46:35 +02:00
const hasRealtimeData = () : boolean => {
if ( ! realtimeGPUData ) return false
2025-10-07 23:36:13 +02:00
// Esto permite mostrar datos incluso cuando la GPU está inactiva (valores en 0 o null)
return realtimeGPUData . has_monitoring_tool === true
2025-10-06 23:40:54 +02:00
}
2026-04-17 16:39:08 +02:00
if ( isLoading ) {
2025-10-14 19:00:24 +02:00
return (
2026-02-16 12:11:37 +01:00
< div className = "flex flex-col items-center justify-center min-h-[400px] gap-4" >
< div className = "relative" >
< div className = "h-12 w-12 rounded-full border-2 border-muted" ></ div >
< div className = "absolute inset-0 h-12 w-12 rounded-full border-2 border-transparent border-t-primary animate-spin" ></ div >
2025-10-14 19:00:24 +02:00
</ div >
2026-02-16 12:11:37 +01:00
< div className = "text-sm font-medium text-foreground" > Loading hardware data ...</ div >
< p className = "text-xs text-muted-foreground" > Detecting CPU , GPU , storage and PCI devices </ p >
2025-10-14 19:00:24 +02:00
</ div >
)
}
2025-10-05 20:45:54 +02:00
return (
2025-10-18 18:32:13 +02:00
< div className = "space-y-6" >
2025-10-06 22:23:56 +02:00
{ /* System Information - CPU & Motherboard */ }
2026-04-17 16:39:08 +02:00
{( hardwareData ? . cpu || hardwareData ? . motherboard ) && (
2025-10-06 22:23:56 +02:00
< Card className = "border-border/50 bg-card/50 p-6" >
< div className = "mb-4 flex items-center gap-2" >
< Cpu className = "h-5 w-5 text-primary" />
< h2 className = "text-lg font-semibold" > System Information </ h2 >
</ div >
< div className = "grid gap-6 md:grid-cols-2" >
{ /* CPU Info */ }
2026-04-17 16:39:08 +02:00
{ hardwareData ? . cpu && Object . keys ( hardwareData . cpu ). length > 0 && (
2025-10-06 22:23:56 +02:00
< div >
< div className = "mb-2 flex items-center gap-2" >
< CpuIcon className = "h-4 w-4 text-muted-foreground" />
< h3 className = "text-sm font-semibold" > CPU </ h3 >
</ div >
< div className = "space-y-2" >
2026-04-17 16:39:08 +02:00
{ hardwareData . cpu . model && (
2025-10-06 22:23:56 +02:00
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Model </ span >
2026-04-17 16:39:08 +02:00
< span className = "font-medium text-right" >{ hardwareData . cpu . model }</ span >
2025-10-06 22:23:56 +02:00
</ div >
)}
2026-04-17 16:39:08 +02:00
{ hardwareData . cpu . cores_per_socket && hardwareData . cpu . sockets && (
2025-10-06 22:23:56 +02:00
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Cores </ span >
< span className = "font-medium" >
2026-04-17 16:39:08 +02:00
{ hardwareData . cpu . sockets } × { hardwareData . cpu . cores_per_socket } = { " " }
{ hardwareData . cpu . sockets * hardwareData . cpu . cores_per_socket } cores
2025-10-06 22:23:56 +02:00
</ span >
</ div >
)}
2026-04-17 16:39:08 +02:00
{ hardwareData . cpu . total_threads && (
2025-10-06 22:23:56 +02:00
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Threads </ span >
2026-04-17 16:39:08 +02:00
< span className = "font-medium" >{ hardwareData . cpu . total_threads }</ span >
2025-10-06 22:23:56 +02:00
</ div >
)}
2026-04-17 16:39:08 +02:00
{ hardwareData . cpu . l3_cache && (
2025-10-06 22:23:56 +02:00
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > L3 Cache </ span >
2026-04-17 16:39:08 +02:00
< span className = "font-medium" >{ hardwareData . cpu . l3_cache }</ span >
2025-10-06 22:23:56 +02:00
</ div >
)}
2026-04-17 16:39:08 +02:00
{ hardwareData . cpu . virtualization && (
2025-10-06 22:23:56 +02:00
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Virtualization </ span >
2026-04-17 16:39:08 +02:00
< span className = "font-medium" >{ hardwareData . cpu . virtualization }</ span >
2025-10-06 22:23:56 +02:00
</ div >
)}
</ div >
</ div >
)}
{ /* Motherboard Info */ }
2026-04-17 16:39:08 +02:00
{ hardwareData ? . motherboard && Object . keys ( hardwareData . motherboard ). length > 0 && (
2025-10-06 22:23:56 +02:00
< div >
< div className = "mb-2 flex items-center gap-2" >
< Cpu className = "h-4 w-4 text-muted-foreground" />
< h3 className = "text-sm font-semibold" > Motherboard </ h3 >
</ div >
< div className = "space-y-2" >
2026-04-17 16:39:08 +02:00
{ hardwareData . motherboard . manufacturer && (
2025-10-06 22:23:56 +02:00
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Manufacturer </ span >
2026-04-17 16:39:08 +02:00
< span className = "font-medium text-right" >{ hardwareData . motherboard . manufacturer }</ span >
2025-10-06 22:23:56 +02:00
</ div >
)}
2026-04-17 16:39:08 +02:00
{ hardwareData . motherboard . model && (
2025-10-06 22:23:56 +02:00
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Model </ span >
2026-04-17 16:39:08 +02:00
< span className = "font-medium text-right" >{ hardwareData . motherboard . model }</ span >
2025-10-06 22:23:56 +02:00
</ div >
)}
2026-04-17 16:39:08 +02:00
{ hardwareData . motherboard . bios ? . vendor && (
2025-10-06 22:23:56 +02:00
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > BIOS </ span >
2026-04-17 16:39:08 +02:00
< span className = "font-medium text-right" >{ hardwareData . motherboard . bios . vendor }</ span >
2025-10-06 22:23:56 +02:00
</ div >
)}
2026-04-17 16:39:08 +02:00
{ hardwareData . motherboard . bios ? . version && (
2025-10-06 22:23:56 +02:00
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Version </ span >
2026-04-17 16:39:08 +02:00
< span className = "font-medium" >{ hardwareData . motherboard . bios . version }</ span >
2025-10-06 22:23:56 +02:00
</ div >
)}
2026-04-17 16:39:08 +02:00
{ hardwareData . motherboard . bios ? . date && (
2025-10-06 22:23:56 +02:00
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Date </ span >
2026-04-17 16:39:08 +02:00
< span className = "font-medium" >{ hardwareData . motherboard . bios . date }</ span >
2025-10-06 22:23:56 +02:00
</ div >
)}
</ div >
</ div >
)}
</ div >
</ Card >
)}
{ /* Memory Modules */ }
2026-04-17 16:39:08 +02:00
{ hardwareData ? . memory_modules && hardwareData . memory_modules . length > 0 && (
2025-10-06 22:23:56 +02:00
< Card className = "border-border/50 bg-card/50 p-6" >
< div className = "mb-4 flex items-center gap-2" >
< MemoryStick className = "h-5 w-5 text-primary" />
< h2 className = "text-lg font-semibold" > Memory Modules </ h2 >
< Badge variant = "outline" className = "ml-auto" >
2026-04-17 16:39:08 +02:00
{ hardwareData . memory_modules . length } installed
2025-10-06 22:23:56 +02:00
</ Badge >
</ div >
< div className = "grid gap-3 md:grid-cols-2 lg:grid-cols-3" >
2026-04-17 16:39:08 +02:00
{ hardwareData . memory_modules . map (( module , index ) => (
2025-10-17 17:53:06 +02:00
< div key = { index } className = "rounded-lg border border-border/30 bg-background/60 p-4" >
2025-10-06 22:23:56 +02:00
< div className = "mb-2 font-medium text-sm" >{ module . slot }</ div >
< div className = "space-y-1" >
{ module . size && (
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Size </ span >
2025-10-12 19:40:35 +02:00
< span className = "font-medium text-green-500" >{ formatMemory ( module . size )}</ span >
2025-10-06 22:23:56 +02:00
</ div >
)}
{ module . type && (
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Type </ span >
< span className = "font-medium" >{ module . type }</ span >
</ div >
)}
2026-02-13 12:30:27 +01:00
{( module . configured_speed || module . max_speed ) && (
2025-10-06 22:23:56 +02:00
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Speed </ span >
2026-02-13 12:30:27 +01:00
< span className = "font-medium" >
{ module . configured_speed && module . max_speed && module . configured_speed !== module . max_speed ? (
< span className = "flex items-center gap-1.5" >
< span className = { module . configured_speed . replace ( /[^0-9]/g , '' ) < module . max_speed . replace ( /[^0-9]/g , '' ) ? "text-orange-500" : "text-blue-500" }>
{ module . configured_speed }
</ span >
< span className = "text-xs text-muted-foreground" >( max : { module . max_speed })</ span >
</ span >
) : (
< span >{ module . configured_speed || module . max_speed }</ span >
)}
</ span >
2025-10-06 22:23:56 +02:00
</ div >
)}
{ module . manufacturer && (
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Manufacturer </ span >
< span className = "font-medium text-right" >{ module . manufacturer }</ span >
</ div >
)}
</ div >
</ div >
))}
</ div >
</ Card >
)}
2025-10-14 15:34:19 +02:00
{ /* Thermal Monitoring */ }
2026-04-17 16:39:08 +02:00
{ hardwareData ? . temperatures && hardwareData . temperatures . length > 0 && (
2025-10-06 22:23:56 +02:00
< Card className = "border-border/50 bg-card/50 p-6" >
< div className = "mb-4 flex items-center gap-2" >
< Thermometer className = "h-5 w-5 text-primary" />
< h2 className = "text-lg font-semibold" > Thermal Monitoring </ h2 >
< Badge variant = "outline" className = "ml-auto" >
2026-04-17 16:39:08 +02:00
{ hardwareData . temperatures . length } sensors
2025-10-06 22:23:56 +02:00
</ Badge >
</ div >
2025-10-24 22:34:29 +02:00
< div className = "grid gap-6 md:grid-cols-2" >
{ /* CPU Sensors */ }
2026-04-17 16:39:08 +02:00
{ groupAndSortTemperatures ( hardwareData . temperatures ). CPU . length > 0 && (
2025-10-24 22:34:29 +02:00
< div className = "md:col-span-2" >
< div className = "mb-3 flex items-center gap-2" >
< CpuIcon className = "h-4 w-4 text-muted-foreground" />
< h3 className = "text-sm font-semibold" > CPU </ h3 >
< Badge variant = "outline" className = "text-xs" >
2026-04-17 16:39:08 +02:00
{ groupAndSortTemperatures ( hardwareData . temperatures ). CPU . length }
2025-10-24 22:34:29 +02:00
</ Badge >
</ div >
< div className = "grid gap-4 md:grid-cols-2" >
2026-04-17 16:39:08 +02:00
{ groupAndSortTemperatures ( hardwareData . temperatures ). CPU . map (( temp , index ) => {
2025-10-24 22:34:29 +02:00
const percentage =
temp . critical > 0 ? ( temp . current / temp . critical ) * 100 : ( temp . current / 100 ) * 100
const isHot = temp . current > ( temp . high || 80 )
const isCritical = temp . current > ( temp . critical || 90 )
2025-10-06 22:23:56 +02:00
2025-10-24 22:34:29 +02:00
return (
< div key = { index } className = "space-y-2" >
< div className = "flex items-center justify-between" >
< span className = "text-sm font-medium" >{ temp . name }</ span >
< span
className = { `text-sm font-semibold ${ isCritical ? "text-red-500" : isHot ? "text-orange-500" : "text-green-500" } ` }
>
{ temp . current . toFixed ( 1 )} ° C
</ span >
</ div >
< div className = "h-2 w-full overflow-hidden rounded-full bg-secondary" >
< div
className = "h-full bg-blue-500 transition-all"
style = {{ width : ` ${ Math . min ( percentage , 100 ) } %` }}
/>
</ div >
{ temp . adapter && < span className = "text-xs text-muted-foreground" >{ temp . adapter }</ span >}
</ div >
)
})}
</ div >
2025-10-14 17:23:59 +02:00
</ div >
2025-10-24 22:34:29 +02:00
)}
{ /* GPU Sensors */ }
2026-04-17 16:39:08 +02:00
{ groupAndSortTemperatures ( hardwareData . temperatures ). GPU . length > 0 && (
2025-10-24 22:34:29 +02:00
< div
2026-04-17 16:39:08 +02:00
className = { groupAndSortTemperatures ( hardwareData . temperatures ). GPU . length > 1 ? "md:col-span-2" : "" }
2025-10-24 22:34:29 +02:00
>
< div className = "mb-3 flex items-center gap-2" >
< Gpu className = "h-4 w-4 text-muted-foreground" />
< h3 className = "text-sm font-semibold" > GPU </ h3 >
< Badge variant = "outline" className = "text-xs" >
2026-04-17 16:39:08 +02:00
{ groupAndSortTemperatures ( hardwareData . temperatures ). GPU . length }
2025-10-24 22:34:29 +02:00
</ Badge >
</ div >
< div
2026-04-17 16:39:08 +02:00
className = { `grid gap-4 ${ groupAndSortTemperatures ( hardwareData . temperatures ). GPU . length > 1 ? "md:grid-cols-2" : "" } ` }
2025-10-24 22:34:29 +02:00
>
2026-04-17 16:39:08 +02:00
{ groupAndSortTemperatures ( hardwareData . temperatures ). GPU . map (( temp , index ) => {
2025-10-24 22:34:29 +02:00
const percentage =
temp . critical > 0 ? ( temp . current / temp . critical ) * 100 : ( temp . current / 100 ) * 100
const isHot = temp . current > ( temp . high || 80 )
const isCritical = temp . current > ( temp . critical || 90 )
return (
< div key = { index } className = "space-y-2" >
< div className = "flex items-center justify-between" >
2025-10-24 23:15:53 +02:00
< span className = "text-sm font-medium" >{ temp . name }</ span >
2025-10-24 22:34:29 +02:00
< span
className = { `text-sm font-semibold ${ isCritical ? "text-red-500" : isHot ? "text-orange-500" : "text-green-500" } ` }
>
{ temp . current . toFixed ( 1 )} ° C
</ span >
</ div >
< div className = "h-2 w-full overflow-hidden rounded-full bg-secondary" >
< div
className = "h-full bg-blue-500 transition-all"
style = {{ width : ` ${ Math . min ( percentage , 100 ) } %` }}
/>
</ div >
{ temp . adapter && < span className = "text-xs text-muted-foreground" >{ temp . adapter }</ span >}
</ div >
)
})}
</ div >
</ div >
)}
{ /* NVME Sensors */ }
2026-04-17 16:39:08 +02:00
{ groupAndSortTemperatures ( hardwareData . temperatures ). NVME . length > 0 && (
2025-10-24 22:34:29 +02:00
< div
2025-11-25 19:04:54 +01:00
className = {
2026-04-17 16:39:08 +02:00
groupAndSortTemperatures ( hardwareData . temperatures ). NVME . length > 1 ? "md:col-span-2" : ""
2025-11-25 19:04:54 +01:00
}
2025-10-24 22:34:29 +02:00
>
< div className = "mb-3 flex items-center gap-2" >
< HardDrive className = "h-4 w-4 text-muted-foreground" />
< h3 className = "text-sm font-semibold" > NVME </ h3 >
< Badge variant = "outline" className = "text-xs" >
2026-04-17 16:39:08 +02:00
{ groupAndSortTemperatures ( hardwareData . temperatures ). NVME . length }
2025-10-24 22:34:29 +02:00
</ Badge >
</ div >
< div
2026-04-17 16:39:08 +02:00
className = { `grid gap-4 ${ groupAndSortTemperatures ( hardwareData . temperatures ). NVME . length > 1 ? "md:grid-cols-2" : "" } ` }
2025-10-24 22:34:29 +02:00
>
2026-04-17 16:39:08 +02:00
{ groupAndSortTemperatures ( hardwareData . temperatures ). NVME . map (( temp , index ) => {
2025-10-24 22:34:29 +02:00
const percentage =
temp . critical > 0 ? ( temp . current / temp . critical ) * 100 : ( temp . current / 100 ) * 100
const isHot = temp . current > ( temp . high || 80 )
const isCritical = temp . current > ( temp . critical || 90 )
return (
< div key = { index } className = "space-y-2" >
< div className = "flex items-center justify-between" >
< span className = "text-sm font-medium" >{ temp . name }</ span >
< span
className = { `text-sm font-semibold ${ isCritical ? "text-red-500" : isHot ? "text-orange-500" : "text-green-500" } ` }
>
{ temp . current . toFixed ( 1 )} ° C
</ span >
</ div >
< div className = "h-2 w-full overflow-hidden rounded-full bg-secondary" >
< div
className = "h-full bg-blue-500 transition-all"
style = {{ width : ` ${ Math . min ( percentage , 100 ) } %` }}
/>
</ div >
{ temp . adapter && < span className = "text-xs text-muted-foreground" >{ temp . adapter }</ span >}
</ div >
)
})}
</ div >
</ div >
)}
{ /* PCI Sensors */ }
2026-04-17 16:39:08 +02:00
{ groupAndSortTemperatures ( hardwareData . temperatures ). PCI . length > 0 && (
2025-10-24 22:34:29 +02:00
< div
2026-04-17 16:39:08 +02:00
className = { groupAndSortTemperatures ( hardwareData . temperatures ). PCI . length > 1 ? "md:col-span-2" : "" }
2025-10-24 22:34:29 +02:00
>
< div className = "mb-3 flex items-center gap-2" >
< CpuIcon className = "h-4 w-4 text-muted-foreground" />
< h3 className = "text-sm font-semibold" > PCI </ h3 >
< Badge variant = "outline" className = "text-xs" >
2026-04-17 16:39:08 +02:00
{ groupAndSortTemperatures ( hardwareData . temperatures ). PCI . length }
2025-10-24 22:34:29 +02:00
</ Badge >
</ div >
< div
2026-04-17 16:39:08 +02:00
className = { `grid gap-4 ${ groupAndSortTemperatures ( hardwareData . temperatures ). PCI . length > 1 ? "md:grid-cols-2" : "" } ` }
2025-10-24 22:34:29 +02:00
>
2026-04-17 16:39:08 +02:00
{ groupAndSortTemperatures ( hardwareData . temperatures ). PCI . map (( temp , index ) => {
2025-10-24 22:34:29 +02:00
const percentage =
temp . critical > 0 ? ( temp . current / temp . critical ) * 100 : ( temp . current / 100 ) * 100
const isHot = temp . current > ( temp . high || 80 )
const isCritical = temp . current > ( temp . critical || 90 )
return (
< div key = { index } className = "space-y-2" >
< div className = "flex items-center justify-between" >
2025-10-24 23:15:53 +02:00
< span className = "text-sm font-medium" >{ temp . name }</ span >
2025-10-24 22:34:29 +02:00
< span
className = { `text-sm font-semibold ${ isCritical ? "text-red-500" : isHot ? "text-orange-500" : "text-green-500" } ` }
>
{ temp . current . toFixed ( 1 )} ° C
</ span >
</ div >
< div className = "h-2 w-full overflow-hidden rounded-full bg-secondary" >
< div
className = "h-full bg-blue-500 transition-all"
style = {{ width : ` ${ Math . min ( percentage , 100 ) } %` }}
/>
</ div >
{ temp . adapter && < span className = "text-xs text-muted-foreground" >{ temp . adapter }</ span >}
</ div >
)
})}
</ div >
</ div >
)}
{ /* OTHER Sensors */ }
2026-04-17 16:39:08 +02:00
{ groupAndSortTemperatures ( hardwareData . temperatures ). OTHER . length > 0 && (
2025-10-24 22:34:29 +02:00
< div
2025-11-25 19:04:54 +01:00
className = {
2026-04-17 16:39:08 +02:00
groupAndSortTemperatures ( hardwareData . temperatures ). OTHER . length > 1 ? "md:col-span-2" : ""
2025-11-25 19:04:54 +01:00
}
2025-10-24 22:34:29 +02:00
>
< div className = "mb-3 flex items-center gap-2" >
< Thermometer className = "h-4 w-4 text-muted-foreground" />
< h3 className = "text-sm font-semibold" > OTHER </ h3 >
< Badge variant = "outline" className = "text-xs" >
2026-04-17 16:39:08 +02:00
{ groupAndSortTemperatures ( hardwareData . temperatures ). OTHER . length }
2025-10-24 22:34:29 +02:00
</ Badge >
</ div >
< div
2026-04-17 16:39:08 +02:00
className = { `grid gap-4 ${ groupAndSortTemperatures ( hardwareData . temperatures ). OTHER . length > 1 ? "md:grid-cols-2" : "" } ` }
2025-10-24 22:34:29 +02:00
>
2026-04-17 16:39:08 +02:00
{ groupAndSortTemperatures ( hardwareData . temperatures ). OTHER . map (( temp , index ) => {
2025-10-24 22:34:29 +02:00
const percentage =
temp . critical > 0 ? ( temp . current / temp . critical ) * 100 : ( temp . current / 100 ) * 100
const isHot = temp . current > ( temp . high || 80 )
const isCritical = temp . current > ( temp . critical || 90 )
return (
< div key = { index } className = "space-y-2" >
< div className = "flex items-center justify-between" >
< span className = "text-sm font-medium" >{ temp . name }</ span >
< span
className = { `text-sm font-semibold ${ isCritical ? "text-red-500" : isHot ? "text-orange-500" : "text-green-500" } ` }
>
{ temp . current . toFixed ( 1 )} ° C
</ span >
</ div >
< div className = "h-2 w-full overflow-hidden rounded-full bg-secondary" >
< div
className = "h-full bg-blue-500 transition-all"
style = {{ width : ` ${ Math . min ( percentage , 100 ) } %` }}
/>
</ div >
{ temp . adapter && < span className = "text-xs text-muted-foreground" >{ temp . adapter }</ span >}
</ div >
)
})}
</ div >
</ div >
)}
</ div >
2025-10-06 22:23:56 +02:00
</ Card >
)}
2025-10-07 02:46:35 +02:00
{ /* GPU Information - Enhanced with on-demand data fetching */ }
2026-04-17 16:39:08 +02:00
{ hardwareData ? . gpus && hardwareData . gpus . length > 0 && (
2025-10-06 22:23:56 +02:00
< Card className = "border-border/50 bg-card/50 p-6" >
< div className = "mb-4 flex items-center gap-2" >
< Gpu className = "h-5 w-5 text-primary" />
< h2 className = "text-lg font-semibold" > Graphics Cards </ h2 >
< Badge variant = "outline" className = "ml-auto" >
2026-04-17 16:39:08 +02:00
{ hardwareData . gpus . length } GPU { hardwareData . gpus . length > 1 ? "s" : "" }
2025-10-06 22:23:56 +02:00
</ Badge >
</ div >
2026-05-27 17:55:41 +02:00
< div className = "grid gap-4 lg:grid-cols-2" >
2026-04-17 16:39:08 +02:00
{ hardwareData . gpus . map (( gpu , index ) => {
2025-10-06 23:40:54 +02:00
const pciDevice = findPCIDeviceForGPU ( gpu )
const fullSlot = pciDevice ? . slot || gpu . slot
2025-10-06 22:23:56 +02:00
2026-04-09 19:40:49 +02:00
return (
< div
key = { index }
onClick = {() => {
// Don't open modal if we're editing this GPU's switch mode
if ( editingSwitchModeGpu !== fullSlot ) {
handleGPUClick ( gpu )
}
}}
className = { `rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card p-4 transition-colors ${
editingSwitchModeGpu === fullSlot
? "cursor-default"
: "cursor-pointer sm:hover:bg-white/5"
} ` }
>
2025-10-06 23:40:54 +02:00
< div className = "mb-3 flex items-center justify-between" >
< span className = "font-medium text-sm" >{ gpu . name }</ span >
< Badge className = { getDeviceTypeColor ( "graphics" )}>{ gpu . vendor }</ Badge >
2025-10-06 22:23:56 +02:00
</ div >
2025-10-06 23:40:54 +02:00
< div className = "space-y-2" >
2025-10-06 22:39:37 +02:00
< div className = "flex justify-between text-sm" >
2025-10-06 23:40:54 +02:00
< span className = "text-muted-foreground" > Type </ span >
< span className = "font-medium" >{ gpu . type }</ span >
2025-10-06 22:39:37 +02:00
</ div >
2025-10-06 23:40:54 +02:00
{ fullSlot && (
2025-10-06 22:23:56 +02:00
< div className = "flex justify-between text-sm" >
2025-10-06 23:40:54 +02:00
< span className = "text-muted-foreground" > PCI Slot </ span >
< span className = "font-mono text-xs" >{ fullSlot }</ span >
2025-10-06 22:23:56 +02:00
</ div >
2025-10-06 23:40:54 +02:00
)}
2025-10-06 22:23:56 +02:00
2025-10-06 23:40:54 +02:00
{ gpu . pci_driver && (
2025-10-06 22:23:56 +02:00
< div className = "flex justify-between text-sm" >
2025-10-06 23:40:54 +02:00
< span className = "text-muted-foreground" > Driver </ span >
< span className = "font-mono text-xs text-green-500" >{ gpu . pci_driver }</ span >
2025-10-06 22:23:56 +02:00
</ div >
2025-10-06 23:40:54 +02:00
)}
2025-10-06 22:23:56 +02:00
2025-10-06 23:40:54 +02:00
{ gpu . pci_kernel_module && (
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Kernel Module </ span >
< span className = "font-mono text-xs" >{ gpu . pci_kernel_module }</ span >
</ div >
)}
2026-05-09 18:59:59 +02:00
2025-10-06 23:40:54 +02:00
</ div >
2026-04-09 15:02:25 +02:00
2026-05-09 18:59:59 +02:00
{ gpu . vendor ? . toLowerCase (). includes ( "nvidia" ) &&
nvidiaInstall ? . current_version &&
nvidiaInstall . update_check ? . last_check && (
< div className = "pt-2 mt-2 border-t border-border" >
{ nvidiaInstall . update_check . available ? (
<>
< div className = "text-xs text-muted-foreground" >
Last checked : { formatLastChecked ( nvidiaInstall . update_check . last_check )} · { " " }
< span className = "text-purple-400 font-medium" >
NVIDIA driver v { nvidiaInstall . update_check . latest } available
</ span >
</ div >
{ nvidiaInstall . menu_label && (
< div className = "text-[11px] text-muted-foreground mt-1" >
Reinstall via ProxMenux post - install : { nvidiaInstall . menu_label }
</ div >
)}
</>
) : (
< div className = "text-xs text-muted-foreground" >
Last checked : { formatLastChecked ( nvidiaInstall . update_check . last_check )}
{ ` · NVIDIA driver v ${ nvidiaInstall . current_version } ` }
{ " · " }
< span className = "text-green-500/80" > No updates available </ span >
</ div >
)}
</ div >
)}
2026-04-09 15:02:25 +02:00
{ /* GPU Switch Mode Indicator */ }
{ getGpuSwitchMode ( gpu ) !== "unknown" && (
2026-04-09 19:40:49 +02:00
< div className = "mt-3 pt-3 border-t border-border/30" >
2026-04-09 19:27:45 +02:00
< div className = "flex items-center justify-between mb-3" >
2026-04-09 15:02:25 +02:00
< span className = "text-xs font-medium text-muted-foreground uppercase tracking-wide" >
Switch Mode
</ span >
2026-04-09 15:29:41 +02:00
< div className = "flex items-center gap-2" >
2026-04-19 12:26:52 +02:00
{ getGpuSwitchMode ( gpu ) === "sriov" ? (
// SR-IOV: edit controls hidden — the state is
// hardware-managed and not togglable from here.
null
) : editingSwitchModeGpu === fullSlot ? (
2026-04-09 19:27:45 +02:00
<>
< button
className = "h-7 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors text-muted-foreground"
2026-04-09 19:40:49 +02:00
onClick = {( e ) => {
e . stopPropagation ()
handleSwitchModeCancel ( fullSlot , e )
}}
2026-04-09 15:02:25 +02:00
>
2026-04-09 19:27:45 +02:00
Cancel
</ button >
< button
className = "h-7 px-3 text-xs rounded-md bg-blue-600 hover:bg-blue-700 text-white transition-colors flex items-center gap-1.5"
2026-04-09 19:40:49 +02:00
onClick = {( e ) => {
e . stopPropagation ()
handleSwitchModeSave ( fullSlot , e )
}}
2026-04-09 19:27:45 +02:00
>
< CheckCircle2 className = "h-3 w-3" />
Save
</ button >
</>
2026-04-09 15:02:25 +02:00
) : (
2026-04-09 19:27:45 +02:00
< button
className = "h-7 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors flex items-center gap-1.5"
2026-04-09 19:40:49 +02:00
onClick = {( e ) => {
e . stopPropagation ()
handleSwitchModeEdit ( fullSlot , e )
}}
2026-04-09 15:02:25 +02:00
>
2026-04-09 19:27:45 +02:00
< Settings2 className = "h-3 w-3" />
2026-04-09 15:29:41 +02:00
Edit
2026-04-09 19:27:45 +02:00
</ button >
2026-04-09 15:02:25 +02:00
)}
</ div >
</ div >
< GpuSwitchModeIndicator
mode = { getGpuSwitchMode ( gpu )}
isEditing = { editingSwitchModeGpu === fullSlot }
pendingMode = { pendingSwitchModes [ gpu . slot ] || null }
onToggle = {( e ) => handleSwitchModeToggle ( gpu , e )}
2026-04-19 12:26:52 +02:00
sriovInfo = {
gpu . sriov_role === "vf" || gpu . sriov_role === "pf-active"
? {
role : gpu.sriov_role ,
physfn : gpu.sriov_physfn ,
vfCount : gpu.sriov_vf_count ,
totalvfs : gpu.sriov_totalvfs ,
}
: undefined
}
2026-04-09 15:02:25 +02:00
/>
</ div >
)}
2025-10-06 22:23:56 +02:00
</ div >
2025-10-06 23:40:54 +02:00
)
})}
2025-10-06 22:23:56 +02:00
</ div >
</ Card >
)}
2025-10-08 12:06:24 +02:00
{ /* GPU Detail Modal - Shows immediately with basic info, then loads real-time data */ }
2025-11-30 22:50:40 +01:00
< Dialog open = { !! selectedGPU } onOpenChange = {( open ) => ! open && setSelectedGPU ( null )}>
2025-10-09 23:14:47 +02:00
< DialogContent className = "max-w-3xl max-h-[85vh] overflow-y-auto" >
2025-10-07 02:46:35 +02:00
{ selectedGPU && (
<>
2025-10-08 12:06:24 +02:00
< DialogHeader className = "pb-4 border-b border-border" >
< DialogTitle >{ selectedGPU . name }</ DialogTitle >
2025-10-10 00:13:54 +02:00
< DialogDescription > GPU Real - Time Monitoring </ DialogDescription >
2025-10-08 12:06:24 +02:00
</ DialogHeader >
2025-10-07 00:24:35 +02:00
2025-10-08 12:06:24 +02:00
< div className = "space-y-6 py-4" >
< div >
< h3 className = "text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide" >
Basic Information
</ h3 >
< div className = "grid gap-2" >
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Vendor </ span >
< Badge className = { getDeviceTypeColor ( "graphics" )}>{ selectedGPU . vendor }</ Badge >
</ div >
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Type </ span >
< span className = "text-sm font-medium" >{ selectedGPU . type }</ span >
</ div >
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > PCI Slot </ span >
< span className = "font-mono text-sm" >
{ findPCIDeviceForGPU ( selectedGPU ) ? . slot || selectedGPU . slot }
</ span >
</ div >
{( findPCIDeviceForGPU ( selectedGPU ) ? . driver || selectedGPU . pci_driver ) && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Driver </ span >
2025-10-10 22:52:22 +02:00
{ /* CHANGE: Added monitoring availability indicator */ }
< div className = "flex items-center gap-2" >
< span className = "font-mono text-sm text-green-500" >
{ findPCIDeviceForGPU ( selectedGPU ) ? . driver || selectedGPU . pci_driver }
</ span >
{ realtimeGPUData ? . has_monitoring_tool === true && (
2025-10-23 15:31:25 +02:00
< Badge className = "bg-green-500/10 text-green-500 border-green-500/20 text-xs px-1.5 py-0" >
{ realtimeGPUData ? . driver_version ? `✓ v ${ realtimeGPUData . driver_version } ` : "✓" }
2025-10-10 22:52:22 +02:00
</ Badge >
)}
</ div >
2025-10-08 12:06:24 +02:00
</ div >
)}
2025-10-23 15:31:25 +02:00
{( findPCIDeviceForGPU ( selectedGPU ) ? . kernel_module || selectedGPU . pci_kernel_module ) && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Kernel Module </ span >
< span className = "font-mono text-sm" >
{ findPCIDeviceForGPU ( selectedGPU ) ? . kernel_module || selectedGPU . pci_kernel_module }
</ span >
</ div >
)}
2025-10-08 12:06:24 +02:00
</ div >
</ div >
2025-10-07 02:46:35 +02:00
2025-10-23 15:31:25 +02:00
{ detailsLoading ? (
< div className = "text-center py-8 text-muted-foreground" >
< Loader2 className = "h-8 w-8 animate-spin mx-auto mb-2 text-primary" />
< p className = "text-sm" > Loading real - time data ...</ p >
2025-10-08 12:06:24 +02:00
</ div >
2026-04-19 12:26:52 +02:00
) : selectedGPU . sriov_role === "vf" ? (
// SR-IOV Virtual Function: per-VF telemetry is not exposed
// by the kernel, so we skip the metrics panel and show
// identity + consumer + a link back to the parent PF.
< div className = "space-y-4" >
< div className = "rounded-lg bg-teal-500/10 p-4 border border-teal-500/20" >
< div className = "flex gap-3" >
< div className = "flex-shrink-0" >
< svg className = "h-5 w-5 text-teal-500" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 }>
< path strokeLinecap = "round" strokeLinejoin = "round" d = "M13 10V3L4 14h7v7l9-11h-7z" />
</ svg >
</ div >
< div className = "flex-1" >
< h4 className = "text-sm font-semibold text-teal-500 mb-1" > SR - IOV Virtual Function </ h4 >
< p className = "text-sm text-muted-foreground" >
This device is a Virtual Function spawned by a Physical Function . Per - VF
telemetry ( temperature , utilization , memory ) is not exposed by the kernel —
open the parent PF to see aggregate GPU metrics .
</ p >
</ div >
</ div >
</ div >
< div className = "rounded-lg border border-border/50 p-4 space-y-3" >
< h3 className = "text-sm font-semibold text-muted-foreground mb-1 uppercase tracking-wide" >
Virtual Function Detail
</ h3 >
< div className = "flex justify-between items-center" >
< span className = "text-sm text-muted-foreground" > Parent Physical Function </ span >
{ selectedGPU . sriov_physfn ? (
< button
className = "font-mono text-sm text-teal-500 hover:underline"
onClick = {( e ) => {
e . stopPropagation ()
const pf = hardwareData ? . gpus ? . find (
( g ) => g . slot === selectedGPU . sriov_physfn
)
if ( pf ) setSelectedGPU ( pf )
}}
>
{ selectedGPU . sriov_physfn }
</ button >
) : (
< span className = "font-mono text-sm text-muted-foreground" > unknown </ span >
)}
</ div >
< div className = "flex justify-between items-center" >
< span className = "text-sm text-muted-foreground" > Current Driver </ span >
< span className = "font-mono text-sm" >
{ selectedGPU . pci_driver || "none" }
</ span >
</ div >
< div className = "flex justify-between items-start" >
< span className = "text-sm text-muted-foreground" > Consumer </ span >
< div className = "text-sm text-right" >
{ realtimeGPUData ? . sriov_consumer ? (
< span className = { cn (
"inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md text-xs font-medium" ,
realtimeGPUData . sriov_consumer . running
? "bg-teal-500/10 text-teal-500"
: "bg-muted text-muted-foreground"
)}>
< span className = "h-1.5 w-1.5 rounded-full bg-current" />
{ realtimeGPUData . sriov_consumer . type . toUpperCase ()} { realtimeGPUData . sriov_consumer . id }
{ realtimeGPUData . sriov_consumer . name && ` · ${ realtimeGPUData . sriov_consumer . name } ` }
{ ` · ${ realtimeGPUData . sriov_consumer . running ? "running" : "stopped" } ` }
</ span >
) : (
< span className = "text-muted-foreground italic" > unused </ span >
)}
</ div >
</ div >
</ div >
</ div >
2025-10-23 15:31:25 +02:00
) : realtimeGPUData ? . has_monitoring_tool === true ? (
<>
2026-04-19 12:26:52 +02:00
{ selectedGPU . sriov_role === "pf-active" && (
// SR-IOV Physical Function: metrics below are the
// aggregate of the whole GPU (PF + all active VFs).
// Flag it explicitly so the reader interprets numbers
// correctly.
< div className = "rounded-lg bg-teal-500/10 p-3 border border-teal-500/20" >
< div className = "flex items-center gap-2 flex-wrap" >
< span className = "inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md bg-teal-500/15 text-teal-500 text-xs font-semibold" >
< span className = "h-1.5 w-1.5 rounded-full bg-teal-500" />
SR - IOV active
</ span >
< span className = "text-sm text-muted-foreground" >
Metrics below reflect the Physical Function ( aggregate across
{ " " }
< span className = "font-semibold text-foreground" >
{ realtimeGPUData ? . sriov_vf_count ?? selectedGPU . sriov_vf_count ?? "N" }
</ span >
{ " " } VFs ).
</ span >
</ div >
</ div >
)}
2025-10-23 15:31:25 +02:00
< div className = "flex items-center gap-2 text-xs text-muted-foreground" >
< div className = "h-2 w-2 rounded-full bg-green-500 animate-pulse" />
< span > Updating every 3 seconds </ span >
</ div >
2025-10-09 23:14:47 +02:00
2025-10-23 15:31:25 +02:00
< div >
< h3 className = "text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide" >
Real - Time Metrics
</ h3 >
< div className = "grid gap-3 md:grid-cols-2" >
{ realtimeGPUData . clock_graphics && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Graphics Clock </ span >
< span className = "text-sm font-medium" >{ formatClock ( realtimeGPUData . clock_graphics )}</ span >
</ div >
)}
{ realtimeGPUData . clock_memory && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Memory Clock </ span >
< span className = "text-sm font-medium" >{ formatClock ( realtimeGPUData . clock_memory )}</ span >
</ div >
)}
{ realtimeGPUData . power_draw && realtimeGPUData . power_draw !== "0.00 W" && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Power Draw </ span >
< span className = "text-sm font-medium text-blue-500" >{ realtimeGPUData . power_draw }</ span >
</ div >
)}
{ realtimeGPUData . temperature !== undefined && realtimeGPUData . temperature !== null && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Temperature </ span >
< span className = "text-sm font-semibold text-green-500" >
{ realtimeGPUData . temperature } ° C
</ span >
</ div >
)}
</ div >
</ div >
{ /* Engine Utilization (Intel/AMD) */ }
{( realtimeGPUData . engine_render !== undefined ||
realtimeGPUData . engine_blitter !== undefined ||
realtimeGPUData . engine_video !== undefined ||
realtimeGPUData . engine_video_enhance !== undefined ) && (
< div >
< h3 className = "text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide" >
Engine Utilization ( Total )
</ h3 >
< div className = "grid gap-3" >
{ realtimeGPUData . engine_render !== undefined && (
< div className = "space-y-1" >
< div className = "flex justify-between" >
< span className = "text-sm text-muted-foreground" > Render / 3 D </ span >
< span className = "text-sm font-medium" >
{ typeof realtimeGPUData . engine_render === "number"
? ` ${ realtimeGPUData . engine_render . toFixed ( 1 ) } %`
: realtimeGPUData . engine_render }
</ span >
</ div >
< Progress
value = {
typeof realtimeGPUData . engine_render === "number"
? realtimeGPUData.engine_render
: Number.parseFloat ( realtimeGPUData . engine_render ) || 0
}
className = "h-2 [&>div]:bg-blue-500"
/>
</ div >
)}
{ realtimeGPUData . engine_video !== undefined && (
< div className = "space-y-1" >
< div className = "flex justify-between" >
< span className = "text-sm text-muted-foreground" > Video </ span >
< span className = "text-sm font-medium" >
{ typeof realtimeGPUData . engine_video === "number"
? ` ${ realtimeGPUData . engine_video . toFixed ( 1 ) } %`
: realtimeGPUData . engine_video }
</ span >
</ div >
< Progress
value = {
typeof realtimeGPUData . engine_video === "number"
? realtimeGPUData.engine_video
: Number.parseFloat ( realtimeGPUData . engine_video ) || 0
}
className = "h-2 [&>div]:bg-blue-500"
/>
</ div >
)}
{ realtimeGPUData . engine_blitter !== undefined && (
< div className = "space-y-1" >
< div className = "flex justify-between" >
< span className = "text-sm text-muted-foreground" > Blitter </ span >
< span className = "text-sm font-medium" >
{ typeof realtimeGPUData . engine_blitter === "number"
? ` ${ realtimeGPUData . engine_blitter . toFixed ( 1 ) } %`
: realtimeGPUData . engine_blitter }
</ span >
</ div >
< Progress
value = {
typeof realtimeGPUData . engine_blitter === "number"
? realtimeGPUData.engine_blitter
: Number.parseFloat ( realtimeGPUData . engine_blitter ) || 0
}
className = "h-2 [&>div]:bg-blue-500"
/>
</ div >
)}
{ realtimeGPUData . engine_video_enhance !== undefined && (
< div className = "space-y-1" >
< div className = "flex justify-between" >
< span className = "text-sm text-muted-foreground" > VideoEnhance </ span >
< span className = "text-sm font-medium" >
{ typeof realtimeGPUData . engine_video_enhance === "number"
? ` ${ realtimeGPUData . engine_video_enhance . toFixed ( 1 ) } %`
: realtimeGPUData . engine_video_enhance }
</ span >
</ div >
< Progress
value = {
typeof realtimeGPUData . engine_video_enhance === "number"
? realtimeGPUData.engine_video_enhance
: Number.parseFloat ( realtimeGPUData . engine_video_enhance ) || 0
}
className = "h-2 [&>div]:bg-blue-500"
/>
</ div >
)}
</ div >
</ div >
)}
{ /* CHANGE: Changed process name badge from blue to purple to match Intel/AMD */ }
{ realtimeGPUData . processes && realtimeGPUData . processes . length > 0 && (
< div >
< h3 className = "text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide" >
Active Processes ({ realtimeGPUData . processes . length })
</ h3 >
< div className = "space-y-3" >
{ realtimeGPUData . processes . map (( proc : any , idx : number ) => (
< div key = { idx } className = "rounded-lg border border-border/30 bg-background/50 p-4" >
< div className = "flex justify-between items-start mb-3" >
< div >
< Badge className = "bg-purple-500/10 text-purple-500 border-purple-500/20 mb-1" >
{ proc . name }
</ Badge >
< p className = "font-mono text-xs text-muted-foreground" > PID : { proc . pid }</ p >
</ div >
{ proc . memory && (
< Badge
variant = "outline"
className = "font-mono text-xs bg-green-500/10 text-green-500 border-green-500/20"
>
{ typeof proc . memory === "object"
? formatMemory ( proc . memory . resident / 1024 )
: formatMemory ( proc . memory )}
</ Badge >
)}
</ div >
{ proc . engines && Object . keys ( proc . engines ). length > 0 && (
< div className = "space-y-2" >
< p className = "text-xs text-muted-foreground mb-1" > Engine Utilization : </ p >
{ Object . entries ( proc . engines ). map (([ engineName , engineData ] : [ string , any ]) => {
const utilization =
typeof engineData === "object" ? engineData . busy || 0 : engineData
const utilizationNum =
typeof utilization === "string" ? Number . parseFloat ( utilization ) : utilization
if ( utilizationNum === 0 || isNaN ( utilizationNum )) return null
return (
< div key = { engineName } className = "space-y-1" >
< div className = "flex justify-between" >
< span className = "text-xs text-muted-foreground" >{ engineName }</ span >
< span className = "text-xs font-medium" >{ utilizationNum . toFixed ( 1 )} % </ span >
</ div >
2026-04-17 10:38:39 +02:00
< Progress value = { utilizationNum } className = "h-2 [&>div]:bg-blue-500" />
2025-10-23 15:31:25 +02:00
</ div >
)
})}
</ div >
)}
</ div >
))}
</ div >
</ div >
)}
{ realtimeGPUData . processes && realtimeGPUData . processes . length === 0 && (
< div className = "rounded-lg bg-muted/50 p-4 text-center" >
< p className = "text-sm text-muted-foreground" > No active processes using the GPU </ p >
</ div >
)}
{ /* Memory Info (NVIDIA) */ }
{ realtimeGPUData . memory_total && (
< div >
< h3 className = "text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide" >
Memory
</ h3 >
< div className = "grid gap-2" >
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Total </ span >
< span className = "text-sm font-medium" >{ realtimeGPUData . memory_total }</ span >
</ div >
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Used </ span >
< span className = "text-sm font-medium" >{ realtimeGPUData . memory_used }</ span >
</ div >
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Free </ span >
< span className = "text-sm font-medium" >{ realtimeGPUData . memory_free }</ span >
</ div >
{ realtimeGPUData . utilization_memory !== undefined && (
< div className = "space-y-1" >
< div className = "flex justify-between" >
< span className = "text-sm text-muted-foreground" > Memory Utilization </ span >
< span className = "text-sm font-medium" >{ realtimeGPUData . utilization_memory } % </ span >
</ div >
< Progress
value = { realtimeGPUData . utilization_memory }
className = "h-2 [&>div]:bg-blue-500"
/>
</ div >
)}
</ div >
</ div >
)}
2026-04-19 12:26:52 +02:00
{ selectedGPU . sriov_role === "pf-active" &&
Array . isArray ( realtimeGPUData ? . sriov_vfs ) &&
realtimeGPUData . sriov_vfs . length > 0 && (
// Per-VF table: one row per virtfn* under the PF.
// Driver is color-coded (teal native / purple vfio-pci
// / muted fallback) and consumer pills go green when
// the guest is currently running, muted otherwise.
< div >
< h3 className = "text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide" >
Virtual Functions
</ h3 >
< div className = "rounded-lg border border-border/50 divide-y divide-border/30 overflow-hidden" >
{ realtimeGPUData . sriov_vfs . map (( vf : any ) => (
< div
key = { vf . bdf }
className = "flex items-center justify-between gap-3 px-4 py-2.5 hover:bg-muted/30 transition-colors"
>
< span className = "font-mono text-xs text-foreground" >{ vf . bdf }</ span >
< div className = "flex items-center gap-3 flex-wrap justify-end" >
< span
className = { cn (
"font-mono text-[11px] px-2 py-0.5 rounded" ,
vf . driver === "vfio-pci"
? "bg-purple-500/10 text-purple-500"
: vf . driver === "i915" ||
vf . driver === "xe" ||
vf . driver === "amdgpu" ||
vf . driver === "radeon" ||
vf . driver === "nvidia"
? "bg-teal-500/10 text-teal-500"
: "bg-muted text-muted-foreground"
)}
>
{ vf . driver || "unbound" }
</ span >
{ vf . consumer ? (
< span
className = { cn (
"inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md text-xs font-medium" ,
vf . consumer . running
? "bg-green-500/10 text-green-500"
: "bg-muted text-muted-foreground"
)}
>
< span className = "h-1.5 w-1.5 rounded-full bg-current" />
{ vf . consumer . type . toUpperCase ()} { vf . consumer . id }
{ vf . consumer . name && (
< span className = "opacity-70" > · { vf . consumer . name }</ span >
)}
</ span >
) : (
< span className = "text-xs text-muted-foreground italic" >
unused
</ span >
)}
</ div >
</ div >
))}
</ div >
</ div >
)}
2025-10-23 15:31:25 +02:00
</>
2026-04-04 00:51:20 +02:00
) : ( findPCIDeviceForGPU ( selectedGPU ) ? . driver === 'vfio-pci' || selectedGPU . pci_driver === 'vfio-pci' ) ? (
< div className = "rounded-lg bg-purple-500/10 p-4 border border-purple-500/20" >
< div className = "flex gap-3" >
< div className = "flex-shrink-0" >
< svg className = "h-5 w-5 text-purple-500" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 }>
< path strokeLinecap = "round" strokeLinejoin = "round" d = "M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
</ svg >
</ div >
< div className = "flex-1" >
< h4 className = "text-sm font-semibold text-purple-500 mb-1" > GPU in Switch Mode VM </ h4 >
< p className = "text-sm text-muted-foreground" >
This GPU is assigned to a virtual machine via VFIO passthrough . Real - time monitoring is not available from the host because the GPU is controlled by the VM .
</ p >
</ div >
</ div >
</ div >
2025-10-23 15:31:25 +02:00
) : (
< div className = "rounded-lg bg-blue-500/10 p-4 border border-blue-500/20" >
< div className = "flex gap-3" >
< div className = "flex-shrink-0" >
< svg className = "h-5 w-5 text-blue-500" fill = "currentColor" viewBox = "0 0 20 20" >
< path
fillRule = "evenodd"
d = "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule = "evenodd"
/>
</ svg >
</ div >
2025-11-30 19:15:07 +01:00
< div className = "flex-1" >
2025-10-23 15:31:25 +02:00
< h4 className = "text-sm font-semibold text-blue-500 mb-1" > Extended Monitoring Not Available </ h4 >
2025-11-30 19:15:07 +01:00
< p className = "text-sm text-muted-foreground mb-3" >
2025-10-23 15:31:25 +02:00
{ getMonitoringToolRecommendation ( selectedGPU . vendor )}
</ p >
2025-11-30 19:15:07 +01:00
{ selectedGPU . vendor . toLowerCase (). includes ( "nvidia" ) && (
2025-12-06 13:11:04 +01:00
< Button
onClick = { handleInstallNvidiaDriver }
className = "w-full bg-blue-600 hover:bg-blue-700 text-white"
>
2025-12-01 01:04:31 +01:00
<>
< Download className = "mr-2 h-4 w-4" />
Install NVIDIA Drivers
</>
2025-11-30 19:15:07 +01:00
</ Button >
)}
2026-01-29 19:04:52 +01:00
{( selectedGPU . vendor . toLowerCase (). includes ( "amd" ) || selectedGPU . vendor . toLowerCase (). includes ( "ati" )) && (
< Button
onClick = { handleInstallAmdTools }
className = "w-full bg-red-600 hover:bg-red-700 text-white"
>
<>
< Download className = "mr-2 h-4 w-4" />
Install AMD GPU Tools
</>
</ Button >
)}
2026-01-29 20:03:39 +01:00
{ selectedGPU . vendor . toLowerCase (). includes ( "intel" ) && (
< Button
onClick = { handleInstallIntelTools }
className = "w-full bg-sky-600 hover:bg-sky-700 text-white"
>
<>
< Download className = "mr-2 h-4 w-4" />
Install Intel GPU Tools
</>
</ Button >
)}
2025-10-23 15:31:25 +02:00
</ div >
</ div >
2025-10-08 12:06:24 +02:00
</ div >
)}
</ div >
2025-10-07 02:46:35 +02:00
</>
)}
2025-10-07 01:37:43 +02:00
</ DialogContent >
</ Dialog >
2025-10-23 15:31:25 +02:00
2026-04-17 19:53:17 +02:00
{ /* Coral TPU / AI Accelerators — only rendered when at least one device is detected.
Unlike GPUs, Coral exposes no temperature/utilization/power counters, so the
modal shows identity + driver state + an Install CTA when drivers are missing. */ }
{ hardwareData ? . coral_tpus && hardwareData . coral_tpus . length > 0 && (
< Card className = "border-border/50 bg-card/50 p-6" >
< div className = "mb-4 flex items-center gap-2" >
< BrainCircuit className = "h-5 w-5 text-primary" />
< h2 className = "text-lg font-semibold" > Coral TPU / AI Accelerators </ h2 >
< Badge variant = "outline" className = "ml-auto" >
{ hardwareData . coral_tpus . length } device { hardwareData . coral_tpus . length > 1 ? "s" : "" }
</ Badge >
</ div >
< div className = "grid gap-3 sm:grid-cols-2 lg:grid-cols-3" >
{ hardwareData . coral_tpus . map (( coral , index ) => (
< div
key = { `coral- ${ index } - ${ coral . slot || coral . bus_device } ` }
onClick = {() => setSelectedCoral ( coral )}
className = "cursor-pointer rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 p-4 transition-colors"
>
< div className = "flex items-center justify-between gap-2 mb-2" >
< span className = "text-sm font-medium line-clamp-2 break-words flex-1" >
{ coral . name }
</ span >
< Badge
className = {
coral . type === "usb"
? "bg-purple-500/10 text-purple-500 border-purple-500/20 px-2.5 py-0.5 shrink-0"
: "bg-blue-500/10 text-blue-500 border-blue-500/20 px-2.5 py-0.5 shrink-0"
}
>
{ coral . type === "usb" ? "USB" : "PCIe" }
</ Badge >
</ div >
< div className = "space-y-1 text-xs text-muted-foreground" >
{ coral . form_factor && (
< div className = "flex items-center gap-1.5" >
< span >{ coral . form_factor }</ span >
{ coral . interface_speed && < span className = "text-muted-foreground/60" > · { coral . interface_speed }</ span >}
</ div >
)}
< div className = "font-mono" >
{ coral . type === "pcie" ? coral.slot : coral.bus_device }
</ div >
</ div >
< div className = "mt-3 flex items-center gap-2 text-xs" >
{ coral . drivers_ready ? (
<>
< CheckCircle2 className = "h-3.5 w-3.5 text-green-500" />
< span className = "text-green-500" > Drivers ready </ span >
</>
) : (
<>
< AlertCircle className = "h-3.5 w-3.5 text-yellow-500" />
< span className = "text-yellow-500" > Drivers not installed </ span >
</>
)}
</ div >
</ div >
))}
</ div >
{ /* Primary CTA at the section level when ANY of the detected Coral devices
is missing drivers — avoids a per-card button repetition. */ }
{ hardwareData . coral_tpus . some (( c ) => ! c . drivers_ready ) && (
< div className = "mt-4 rounded-lg border border-blue-500/20 bg-blue-500/10 p-3 flex items-center justify-between gap-3" >
< div className = "flex items-start gap-3 flex-1" >
< AlertCircle className = "h-4 w-4 text-blue-500 mt-0.5 shrink-0" />
< div className = "text-sm" >
< p className = "font-medium text-blue-500" > Install Coral TPU drivers </ p >
< p className = "text-xs text-muted-foreground" >
One or more detected Coral devices need drivers . A server reboot is required after installation .
</ p >
</ div >
</ div >
< Button
onClick = {() => setShowCoralInstaller ( true )}
className = "bg-blue-600 hover:bg-blue-700 text-white shrink-0"
>
< Download className = "mr-2 h-4 w-4" />
Install Drivers
</ Button >
</ div >
)}
</ Card >
)}
{ /* Coral TPU detail modal */ }
< Dialog open = { selectedCoral !== null } onOpenChange = {( open ) => ! open && setSelectedCoral ( null )}>
< DialogContent className = "max-w-2xl" >
< DialogHeader >
< DialogTitle >{ selectedCoral ? . name }</ DialogTitle >
< DialogDescription > Coral TPU Device Information </ DialogDescription >
</ DialogHeader >
{ selectedCoral && (
< div className = "space-y-3" >
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Connection </ span >
< Badge
className = {
selectedCoral . type === "usb"
? "bg-purple-500/10 text-purple-500 border-purple-500/20"
: "bg-blue-500/10 text-blue-500 border-blue-500/20"
}
>
{ selectedCoral . type === "usb" ? "USB" : "PCIe / M.2" }
</ Badge >
</ div >
{ selectedCoral . form_factor && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Form Factor </ span >
< span className = "text-sm" >{ selectedCoral . form_factor }</ span >
</ div >
)}
{ selectedCoral . interface_speed && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Link </ span >
< span className = "font-mono text-sm" >{ selectedCoral . interface_speed }</ span >
</ div >
)}
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" >
{ selectedCoral . type === "usb" ? "Bus:Device" : "PCI Slot" }
</ span >
< span className = "font-mono text-sm" >
{ selectedCoral . type === "usb" ? selectedCoral.bus_device : selectedCoral.slot }
</ span >
</ div >
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Vendor / Product ID </ span >
< span className = "font-mono text-sm" >
{ selectedCoral . vendor_id } : { selectedCoral . device_id }
</ span >
</ div >
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Vendor </ span >
< span className = "text-sm" >{ selectedCoral . vendor }</ span >
</ div >
{ selectedCoral . type === "pcie" && selectedCoral . kernel_driver && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Kernel Driver </ span >
< span className = { `font-mono text-sm ${ selectedCoral . kernel_driver === "apex" ? "text-green-500" : "text-yellow-500" } ` }>
{ selectedCoral . kernel_driver }
</ span >
</ div >
)}
{ selectedCoral . kernel_modules && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Kernel Modules </ span >
< div className = "flex gap-2" >
< Badge variant = "outline" className = { selectedCoral . kernel_modules . gasket ? "text-green-500 border-green-500/20" : "text-red-500 border-red-500/20" }>
gasket { selectedCoral . kernel_modules . gasket ? "✓" : "✗" }
</ Badge >
< Badge variant = "outline" className = { selectedCoral . kernel_modules . apex ? "text-green-500 border-green-500/20" : "text-red-500 border-red-500/20" }>
apex { selectedCoral . kernel_modules . apex ? "✓" : "✗" }
</ Badge >
</ div >
</ div >
)}
{ selectedCoral . device_nodes && selectedCoral . device_nodes . length > 0 && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Device Nodes </ span >
< span className = "font-mono text-xs text-right" >
{ selectedCoral . device_nodes . join ( ", " )}
</ span >
</ div >
)}
{ selectedCoral . type === "usb" && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Runtime State </ span >
< span className = "text-sm" >
{ selectedCoral . programmed ? "Programmed (runtime loaded)" : "Unprogrammed (runtime not loaded)" }
</ span >
</ div >
)}
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Edge TPU Runtime </ span >
< span className = "text-sm text-right" >
{ selectedCoral . edgetpu_runtime || < span className = "text-muted-foreground/60" > not installed </ span >}
</ span >
</ div >
2026-04-17 20:56:07 +02:00
{ typeof selectedCoral . temperature === "number" && (() => {
const trips = selectedCoral . temperature_trips
// Dynamic thresholds when the driver exposes trip points.
// Otherwise fall back to conservative hardcoded limits.
// trips are reported warn→critical, so [N-1] is critical (red)
// and [N-2] is the throttle/warn level (amber).
const redAt = trips && trips . length >= 1 ? trips [ trips . length - 1 ] : 85
const amberAt =
trips && trips . length >= 2
? trips [ trips . length - 2 ]
: trips && trips . length === 1
? redAt - 10
: 75
const color =
selectedCoral . temperature >= redAt
? "text-red-500"
: selectedCoral . temperature >= amberAt
? "text-amber-500"
: "text-green-500"
return (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Temperature </ span >
< div className = "text-right" >
< span className = { `text-sm font-semibold ${ color } ` }>
{ selectedCoral . temperature . toFixed ( 1 )} ° C
</ span >
{ trips && trips . length > 0 && (
< div className = "text-xs text-muted-foreground mt-0.5" >
Thresholds : { trips . map (( t ) => ` ${ t . toFixed ( 0 ) } °C` ). join ( " · " )}
</ div >
)}
</ div >
</ div >
)
})()}
{ selectedCoral . thermal_warnings && selectedCoral . thermal_warnings . length > 0 && (
< div className = "flex justify-between items-start border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Hardware Warnings </ span >
< div className = "flex flex-col gap-1 items-end" >
{ selectedCoral . thermal_warnings . map (( w ) => (
< div key = { w . name } className = "flex items-center gap-2" >
< span className = "text-xs font-mono text-muted-foreground" >
{ w . name }
{ typeof w . threshold_c === "number" && ` @ ${ w . threshold_c . toFixed ( 0 ) } °C` }
</ span >
< Badge
variant = "outline"
className = {
w . enabled
? "text-green-500 border-green-500/20"
: "text-muted-foreground/70"
}
>
{ w . enabled ? "enabled" : "disabled" }
</ Badge >
</ div >
))}
</ div >
</ div >
)}
2026-04-17 19:53:17 +02:00
{ ! selectedCoral . drivers_ready && (
< Button
onClick = {() => {
setSelectedCoral ( null )
setShowCoralInstaller ( true )
}}
className = "w-full bg-blue-600 hover:bg-blue-700 text-white"
>
< Download className = "mr-2 h-4 w-4" />
Install Coral TPU Drivers
</ Button >
)}
</ div >
)}
</ DialogContent >
</ Dialog >
2025-10-23 15:31:25 +02:00
{ /* Power Consumption */ }
2026-04-17 16:39:08 +02:00
{ hardwareData ? . power_meter && (
2025-10-23 15:31:25 +02:00
< Card className = "border-border/50 bg-card/50 p-6" >
< div className = "mb-4 flex items-center gap-2" >
< Zap className = "h-5 w-5 text-blue-500" />
< h2 className = "text-lg font-semibold" > Power Consumption </ h2 >
</ div >
< div className = "space-y-4" >
< div className = "flex items-center justify-between rounded-lg border border-border/30 bg-background/60 p-4" >
< div className = "space-y-1" >
2026-04-17 16:39:08 +02:00
< p className = "text-sm font-medium" >{ hardwareData . power_meter . name }</ p >
{ hardwareData . power_meter . adapter && (
< p className = "text-xs text-muted-foreground" >{ hardwareData . power_meter . adapter }</ p >
2025-10-23 15:31:25 +02:00
)}
</ div >
< div className = "text-right" >
2026-04-17 16:39:08 +02:00
< p className = "text-2xl font-bold text-blue-500" >{ hardwareData . power_meter . watts . toFixed ( 1 )} W </ p >
2025-10-23 15:31:25 +02:00
< p className = "text-xs text-muted-foreground" > Current Draw </ p >
</ div >
</ div >
</ div >
</ Card >
)}
{ /* Power Supplies */ }
2026-04-17 16:39:08 +02:00
{ hardwareData ? . power_supplies && hardwareData . power_supplies . length > 0 && (
2025-10-23 15:31:25 +02:00
< Card className = "border-border/50 bg-card/50 p-6" >
< div className = "mb-4 flex items-center gap-2" >
< PowerIcon className = "h-5 w-5 text-green-500" />
< h2 className = "text-lg font-semibold" > Power Supplies </ h2 >
< Badge variant = "outline" className = "ml-auto" >
2026-04-17 16:39:08 +02:00
{ hardwareData . power_supplies . length } PSUs
2025-10-23 15:31:25 +02:00
</ Badge >
</ div >
< div className = "grid gap-3 md:grid-cols-2" >
2026-04-17 16:39:08 +02:00
{ hardwareData . power_supplies . map (( psu , index ) => (
2025-10-23 15:31:25 +02:00
< div key = { index } className = "rounded-lg border border-border/30 bg-background/60 p-4" >
< div className = "flex items-center justify-between" >
< span className = "text-sm font-medium" >{ psu . name }</ span >
{ psu . status && (
< Badge variant = { psu . status . toLowerCase () === "ok" ? "default" : "destructive" }>{ psu . status }</ Badge >
)}
</ div >
< p className = "mt-2 text-2xl font-bold text-green-500" >{ psu . watts } W </ p >
< p className = "text-xs text-muted-foreground" > Current Output </ p >
</ div >
))}
</ div >
</ Card >
)}
{ /* Fans */ }
2026-04-17 16:39:08 +02:00
{ hardwareData ? . fans && hardwareData . fans . length > 0 && (
2025-10-23 15:31:25 +02:00
< Card className = "border-border/50 bg-card/50 p-6" >
< div className = "mb-4 flex items-center gap-2" >
< FanIcon className = "h-5 w-5 text-primary" />
< h2 className = "text-lg font-semibold" > System Fans </ h2 >
< Badge variant = "outline" className = "ml-auto" >
2026-04-17 16:39:08 +02:00
{ hardwareData . fans . length } fans
2025-10-23 15:31:25 +02:00
</ Badge >
</ div >
< div className = "grid gap-4 md:grid-cols-2" >
2026-04-17 16:39:08 +02:00
{ hardwareData . fans . map (( fan , index ) => {
2025-10-23 15:31:25 +02:00
const isPercentage = fan . unit === "percent" || fan . unit === "%"
const percentage = isPercentage ? fan.speed : Math.min (( fan . speed / 5000 ) * 100 , 100 )
return (
< div key = { index } className = "space-y-2" >
< div className = "flex items-center justify-between" >
2025-10-24 23:15:53 +02:00
< span className = "text-sm font-medium" >{ fan . name }</ span >
2025-10-23 15:31:25 +02:00
< span className = "text-sm font-semibold text-blue-500" >
{ isPercentage ? ` ${ fan . speed . toFixed ( 0 ) } percent` : ` ${ fan . speed . toFixed ( 0 ) } ${ fan . unit } ` }
</ span >
</ div >
< div className = "h-2 w-full overflow-hidden rounded-full bg-secondary" >
< div className = "h-full bg-blue-500 transition-all" style = {{ width : ` ${ percentage } %` }} />
</ div >
{ fan . adapter && < span className = "text-xs text-muted-foreground" >{ fan . adapter }</ span >}
</ div >
)
})}
</ div >
</ Card >
)}
{ /* UPS */ }
2026-04-17 16:39:08 +02:00
{ hardwareData ? . ups && Array . isArray ( hardwareData . ups ) && hardwareData . ups . length > 0 && (
2025-10-23 15:31:25 +02:00
< Card className = "border-border/50 bg-card/50 p-6" >
< div className = "mb-4 flex items-center gap-2" >
< Battery className = "h-5 w-5 text-primary" />
< h2 className = "text-lg font-semibold" > UPS Status </ h2 >
< Badge variant = "outline" className = "ml-auto" >
2026-04-17 16:39:08 +02:00
{ hardwareData . ups . length } UPS
2025-10-23 15:31:25 +02:00
</ Badge >
</ div >
< div className = "grid gap-4 md:grid-cols-2" >
2026-04-17 16:39:08 +02:00
{ hardwareData . ups . map (( ups : any , index : number ) => {
2025-10-23 15:31:25 +02:00
const batteryCharge =
ups . battery_charge_raw || Number . parseFloat ( ups . battery_charge ? . replace ( "%" , "" ) || "0" )
const loadPercent = ups . load_percent_raw || Number . parseFloat ( ups . load_percent ? . replace ( "%" , "" ) || "0" )
// Determine status badge color
const getStatusColor = ( status : string ) => {
if ( ! status ) return "bg-gray-500/10 text-gray-500 border-gray-500/20"
const statusUpper = status . toUpperCase ()
if ( statusUpper . includes ( "OL" )) return "bg-green-500/10 text-green-500 border-green-500/20"
if ( statusUpper . includes ( "OB" )) return "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
if ( statusUpper . includes ( "LB" )) return "bg-red-500/10 text-red-500 border-red-500/20"
return "bg-blue-500/10 text-blue-500 border-blue-500/20"
}
return (
< div
key = { index }
onClick = {() => setSelectedUPS ( ups )}
className = "cursor-pointer rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 p-4 transition-colors"
>
< div className = "flex items-center justify-between mb-4" >
< div className = "flex-1 min-w-0" >
< span className = "text-sm font-medium block truncate" >{ ups . model || ups . name }</ span >
{ ups . is_remote && < span className = "text-xs text-muted-foreground" > Remote : { ups . host }</ span >}
</ div >
< Badge className = { getStatusColor ( ups . status )}>{ ups . status || "Unknown" }</ Badge >
</ div >
< div className = "grid gap-3 md:grid-cols-2" >
{ ups . battery_charge && (
< div className = "space-y-1" >
< div className = "flex items-center justify-between" >
< span className = "text-xs text-muted-foreground" > Battery Charge </ span >
< span className = "text-sm font-semibold text-green-500" >{ ups . battery_charge }</ span >
</ div >
< Progress value = { batteryCharge } className = "h-2 [&>div]:bg-blue-500" />
</ div >
)}
{ ups . load_percent && (
< div className = "space-y-1" >
< div className = "flex items-center justify-between" >
< span className = "text-xs text-muted-foreground" > Load </ span >
< span className = "text-sm font-semibold text-green-500" >{ ups . load_percent }</ span >
</ div >
< Progress value = { loadPercent } className = "h-2 [&>div]:bg-blue-500" />
</ div >
)}
{ ups . time_left && (
< div >
< span className = "text-xs text-muted-foreground" > Runtime </ span >
< div className = "mt-1" >
< Badge className = "bg-green-500/10 text-green-500 border-green-500/20" >{ ups . time_left }</ Badge >
</ div >
</ div >
)}
{ ups . input_voltage && (
< div >
< span className = "text-xs text-muted-foreground" > Input Voltage </ span >
< div className = "mt-1" >
< span className = "text-sm font-medium text-green-500" >{ ups . input_voltage }</ span >
</ div >
</ div >
)}
</ div >
</ div >
)
})}
</ div >
</ Card >
)}
< Dialog open = { selectedUPS !== null } onOpenChange = {() => setSelectedUPS ( null )}>
< DialogContent className = "max-w-3xl max-h-[85vh] overflow-y-auto" >
{ selectedUPS && (
<>
< DialogHeader className = "pb-4 border-b border-border" >
< DialogTitle >{ selectedUPS . model || selectedUPS . name }</ DialogTitle >
< DialogDescription >
UPS Detailed Information
{ selectedUPS . is_remote && ` • Remote: ${ selectedUPS . host } ` }
</ DialogDescription >
</ DialogHeader >
< div className = "space-y-6 py-4" >
{ /* Status Overview */ }
< div >
< h3 className = "text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide" >
Status Overview
</ h3 >
< div className = "grid gap-2" >
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Status </ span >
< Badge
className = {
selectedUPS . status ? . includes ( "OL" )
? "bg-green-500/10 text-green-500 border-green-500/20"
: selectedUPS . status ? . includes ( "OB" )
? "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
: selectedUPS . status ? . includes ( "LB" )
? "bg-red-500/10 text-red-500 border-red-500/20"
: "bg-blue-500/10 text-blue-500 border-blue-500/20"
}
>
{ selectedUPS . status || "Unknown" }
</ Badge >
</ div >
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Connection </ span >
< Badge variant = "outline" >{ selectedUPS . connection_type }</ Badge >
</ div >
{ selectedUPS . host && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Host </ span >
< span className = "text-sm font-medium" >{ selectedUPS . host }</ span >
</ div >
)}
</ div >
</ div >
{ /* Battery Information */ }
< div >
< h3 className = "text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide" >
Battery Information
</ h3 >
< div className = "grid gap-3" >
{ selectedUPS . battery_charge && (
< div className = "space-y-1" >
< div className = "flex justify-between" >
< span className = "text-sm text-muted-foreground" > Charge Level </ span >
< span className = "text-sm font-semibold text-green-500" >{ selectedUPS . battery_charge }</ span >
</ div >
< Progress
value = {
selectedUPS . battery_charge_raw ||
Number . parseFloat ( selectedUPS . battery_charge . replace ( "%" , "" ))
}
className = "h-2 [&>div]:bg-blue-500"
/>
</ div >
)}
{ selectedUPS . time_left && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Runtime Remaining </ span >
< Badge className = "bg-green-500/10 text-green-500 border-green-500/20" >
{ selectedUPS . time_left }
</ Badge >
</ div >
)}
{ selectedUPS . battery_voltage && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Battery Voltage </ span >
< span className = "text-sm font-medium" >{ selectedUPS . battery_voltage }</ span >
</ div >
)}
{ selectedUPS . battery_date && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Battery Date </ span >
< span className = "text-sm font-medium" >{ selectedUPS . battery_date }</ span >
</ div >
)}
</ div >
</ div >
{ /* Input/Output Information */ }
< div >
< h3 className = "text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide" >
Power Information
</ h3 >
< div className = "grid gap-2 md:grid-cols-2" >
{ selectedUPS . input_voltage && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Input Voltage </ span >
< span className = "text-sm font-medium text-green-500" >{ selectedUPS . input_voltage }</ span >
</ div >
)}
{ selectedUPS . output_voltage && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Output Voltage </ span >
< span className = "text-sm font-medium text-green-500" >{ selectedUPS . output_voltage }</ span >
</ div >
)}
{ selectedUPS . input_frequency && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Input Frequency </ span >
< span className = "text-sm font-medium" >{ selectedUPS . input_frequency }</ span >
</ div >
)}
{ selectedUPS . output_frequency && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Output Frequency </ span >
< span className = "text-sm font-medium" >{ selectedUPS . output_frequency }</ span >
</ div >
)}
{ selectedUPS . load_percent && (
< div className = "md:col-span-2 space-y-1" >
< div className = "flex justify-between" >
< span className = "text-sm text-muted-foreground" > Load </ span >
< span className = "text-sm font-semibold text-green-500" >{ selectedUPS . load_percent }</ span >
</ div >
< Progress
value = {
selectedUPS . load_percent_raw || Number . parseFloat ( selectedUPS . load_percent . replace ( "%" , "" ))
}
className = "h-2 [&>div]:bg-blue-500"
/>
</ div >
)}
{ selectedUPS . real_power && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Real Power </ span >
< span className = "text-sm font-medium text-blue-500" >{ selectedUPS . real_power }</ span >
</ div >
)}
{ selectedUPS . apparent_power && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Apparent Power </ span >
< span className = "text-sm font-medium text-blue-500" >{ selectedUPS . apparent_power }</ span >
</ div >
)}
</ div >
</ div >
{ /* Device Information */ }
< div >
< h3 className = "text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide" >
Device Information
</ h3 >
< div className = "grid gap-2" >
{ selectedUPS . manufacturer && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
2025-11-30 21:49:13 +01:00
< span className = "text-sm font-medium text-muted-foreground" > Manufacturer </ span >
2025-10-23 15:31:25 +02:00
< span className = "text-sm font-medium" >{ selectedUPS . manufacturer }</ span >
</ div >
)}
{ selectedUPS . model && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
2025-11-30 21:49:13 +01:00
< span className = "text-sm font-medium text-muted-foreground" > Model </ span >
2025-10-23 15:31:25 +02:00
< span className = "text-sm font-medium" >{ selectedUPS . model }</ span >
</ div >
)}
{ selectedUPS . serial && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
2025-11-30 21:49:13 +01:00
< span className = "text-sm font-medium text-muted-foreground" > Serial Number </ span >
2025-10-23 15:31:25 +02:00
< span className = "font-mono text-sm" >{ selectedUPS . serial }</ span >
</ div >
)}
{ selectedUPS . firmware && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
2025-11-30 21:49:13 +01:00
< span className = "text-sm font-medium text-muted-foreground" > Firmware </ span >
2025-10-23 15:31:25 +02:00
< span className = "text-sm font-medium" >{ selectedUPS . firmware }</ span >
</ div >
)}
{ selectedUPS . driver && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
2025-11-30 21:49:13 +01:00
< span className = "text-sm font-medium text-muted-foreground" > Driver </ span >
2025-10-23 15:31:25 +02:00
< span className = "font-mono text-sm text-green-500" >{ selectedUPS . driver }</ span >
</ div >
)}
</ div >
</ div >
</ div >
</>
)}
</ DialogContent >
</ Dialog >
2025-11-27 09:32:41 +01:00
{ /* PCI Devices - Changed to modal */ }
2026-04-17 16:39:08 +02:00
{ hardwareData ? . pci_devices && hardwareData . pci_devices . length > 0 && (
2025-11-27 09:32:41 +01:00
< Card className = "border-border/50 bg-card/50 p-6" >
< div className = "mb-4 flex items-center gap-2" >
< CpuIcon className = "h-5 w-5 text-primary" />
< h2 className = "text-lg font-semibold" > PCI Devices </ h2 >
< Badge variant = "outline" className = "ml-auto" >
2026-04-17 16:39:08 +02:00
{ hardwareData . pci_devices . length } devices
2025-11-27 09:32:41 +01:00
</ Badge >
</ div >
< div className = "grid gap-3 sm:grid-cols-2 lg:grid-cols-3" >
2026-04-17 16:39:08 +02:00
{ hardwareData . pci_devices . map (( device , index ) => (
2025-11-27 09:32:41 +01:00
< div
key = { index }
onClick = {() => setSelectedPCIDevice ( device )}
className = "cursor-pointer rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 p-3 transition-colors"
>
< div className = "flex items-center justify-between gap-2 mb-2" >
< Badge className = { ` ${ getDeviceTypeColor ( device . type ) } text-xs shrink-0` }>{ device . type }</ Badge >
< span className = "font-mono text-xs text-muted-foreground shrink-0" >{ device . slot }</ span >
</ div >
< p className = "font-medium text-sm line-clamp-2 break-words" >{ device . device }</ p >
< p className = "text-xs text-muted-foreground truncate" >{ device . vendor }</ p >
{ device . driver && (
< p className = "mt-1 font-mono text-xs text-green-500 truncate" > Driver : { device . driver }</ p >
)}
</ div >
))}
</ div >
</ Card >
)}
{ /* PCI Device Detail Modal */ }
< Dialog open = { selectedPCIDevice !== null } onOpenChange = {() => setSelectedPCIDevice ( null )}>
< DialogContent className = "max-w-2xl" >
< DialogHeader >
< DialogTitle >{ selectedPCIDevice ? . device }</ DialogTitle >
< DialogDescription > PCI Device Information </ DialogDescription >
</ DialogHeader >
{ selectedPCIDevice && (
< div className = "space-y-3" >
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Device Type </ span >
< Badge className = { getDeviceTypeColor ( selectedPCIDevice . type )}>{ selectedPCIDevice . type }</ Badge >
</ div >
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > PCI Slot </ span >
< span className = "font-mono text-sm" >{ selectedPCIDevice . slot }</ span >
</ div >
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Device Name </ span >
< span className = "text-sm text-right" >{ selectedPCIDevice . device }</ span >
</ div >
2026-03-22 18:43:33 +01:00
{ selectedPCIDevice . sdevice && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Product Name </ span >
< span className = "text-sm text-right text-blue-400" >{ selectedPCIDevice . sdevice }</ span >
</ div >
)}
2025-11-27 09:32:41 +01:00
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Vendor </ span >
< span className = "text-sm" >{ selectedPCIDevice . vendor }</ span >
</ div >
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Class </ span >
< span className = "font-mono text-sm" >{ selectedPCIDevice . class }</ span >
</ div >
{ selectedPCIDevice . driver && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Driver </ span >
< span className = "font-mono text-sm text-green-500" >{ selectedPCIDevice . driver }</ span >
</ div >
)}
{ selectedPCIDevice . kernel_module && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Kernel Module </ span >
< span className = "font-mono text-sm" >{ selectedPCIDevice . kernel_module }</ span >
</ div >
)}
</ div >
)}
</ DialogContent >
</ Dialog >
2025-11-26 21:33:25 +01:00
{ /* Network Summary - Clickable */ }
2026-04-17 16:39:08 +02:00
{ hardwareData ? . pci_devices &&
hardwareData . pci_devices . filter (( d ) => d . type . toLowerCase (). includes ( "network" )). length > 0 && (
2025-11-26 21:33:25 +01:00
< Card className = "border-border/50 bg-card/50 p-6" >
< div className = "mb-4 flex items-center gap-2" >
< Network className = "h-5 w-5 text-primary" />
< h2 className = "text-lg font-semibold" > Network Summary </ h2 >
< Badge variant = "outline" className = "ml-auto" >
2026-04-17 16:39:08 +02:00
{ hardwareData . pci_devices . filter (( d ) => d . type . toLowerCase (). includes ( "network" )). length } interfaces
2025-11-26 21:33:25 +01:00
</ Badge >
</ div >
< div className = "grid gap-3 sm:grid-cols-2 lg:grid-cols-3" >
2026-04-17 16:39:08 +02:00
{ hardwareData . pci_devices
2025-11-26 21:33:25 +01:00
. filter (( d ) => d . type . toLowerCase (). includes ( "network" ))
. map (( device , index ) => (
< div
key = { index }
onClick = {() => setSelectedNetwork ( device )}
className = "cursor-pointer rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 p-3 transition-colors"
>
< div className = "flex items-center justify-between gap-2 mb-1" >
< span className = "text-sm font-medium line-clamp-2 break-words flex-1" >{ device . device }</ span >
< Badge
className = {
device . network_subtype === "Wireless"
? "bg-purple-500/10 text-purple-500 border-purple-500/20 px-2.5 py-0.5 shrink-0"
: "bg-blue-500/10 text-blue-500 border-blue-500/20 px-2.5 py-0.5 shrink-0"
}
>
{ device . network_subtype || "Ethernet" }
</ Badge >
</ div >
< p className = "text-xs text-muted-foreground truncate" >{ device . vendor }</ p >
{ device . driver && (
< p className = "mt-1 font-mono text-xs text-green-500 truncate" > Driver : { device . driver }</ p >
)}
</ div >
))}
</ div >
</ Card >
)}
2025-10-23 15:31:25 +02:00
{ /* Network Detail Modal */ }
< Dialog open = { selectedNetwork !== null } onOpenChange = {() => setSelectedNetwork ( null )}>
< DialogContent className = "max-w-2xl" >
< DialogHeader >
< DialogTitle >{ selectedNetwork ? . device }</ DialogTitle >
< DialogDescription > Network Interface Information </ DialogDescription >
</ DialogHeader >
{ selectedNetwork && (
< div className = "space-y-3" >
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Device Type </ span >
< Badge className = { getDeviceTypeColor ( selectedNetwork . type )}>{ selectedNetwork . type }</ Badge >
</ div >
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > PCI Slot </ span >
< span className = "font-mono text-sm" >{ selectedNetwork . slot }</ span >
</ div >
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Vendor </ span >
< span className = "text-sm" >{ selectedNetwork . vendor }</ span >
</ div >
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Class </ span >
< span className = "font-mono text-sm" >{ selectedNetwork . class }</ span >
</ div >
{ selectedNetwork . driver && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Driver </ span >
< span className = "font-mono text-sm text-green-500" >{ selectedNetwork . driver }</ span >
</ div >
)}
{ selectedNetwork . kernel_module && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Kernel Module </ span >
< span className = "font-mono text-sm" >{ selectedNetwork . kernel_module }</ span >
</ div >
)}
</ div >
)}
</ DialogContent >
</ Dialog >
{ /* Storage Summary - Clickable */ }
2026-04-17 16:39:08 +02:00
{ hardwareData ? . storage_devices && hardwareData . storage_devices . length > 0 && (
2025-10-23 15:31:25 +02:00
< Card className = "border-border/50 bg-card/50 p-6" >
< div className = "mb-4 flex items-center gap-2" >
< HardDrive className = "h-5 w-5 text-primary" />
< h2 className = "text-lg font-semibold" > Storage Summary </ h2 >
< Badge variant = "outline" className = "ml-auto" >
{
2026-04-17 16:39:08 +02:00
hardwareData . storage_devices . filter (
2025-10-23 15:31:25 +02:00
( device ) =>
device . type === "disk" && ! device . name . startsWith ( "zd" ) && ! device . name . startsWith ( "loop" ),
). length
}{ " " }
devices
</ Badge >
</ div >
< div className = "grid gap-3 sm:grid-cols-2 lg:grid-cols-3" >
2026-04-17 16:39:08 +02:00
{ hardwareData . storage_devices
2025-10-23 15:31:25 +02:00
. filter (
( device ) => device . type === "disk" && ! device . name . startsWith ( "zd" ) && ! device . name . startsWith ( "loop" ),
)
2025-10-24 22:34:29 +02:00
. map (( device , index ) => {
2025-10-29 22:16:40 +01:00
const getDiskTypeBadge = ( diskName : string , rotationRate : number | string | undefined ) => {
2026-06-24 23:40:22 +02:00
// Classifier lives in lib/disk-type.ts — same rules
// the Storage page uses, so a drive can't show up as
// HDD here while showing as SSD there.
const diskType = getDiskType ( diskName , rotationRate )
2025-10-24 22:34:29 +02:00
const badgeStyles : Record < string , { className : string ; label : string }> = {
NVMe : {
className : "bg-purple-500/10 text-purple-500 border-purple-500/20" ,
label : "NVMe SSD" ,
},
SSD : {
className : "bg-cyan-500/10 text-cyan-500 border-cyan-500/20" ,
label : "SSD" ,
},
HDD : {
className : "bg-blue-500/10 text-blue-500 border-blue-500/20" ,
label : "HDD" ,
},
}
return badgeStyles [ diskType ]
}
const diskBadge = getDiskTypeBadge ( device . name , device . rotation_rate )
2025-11-04 12:47:26 +01:00
const getLinkSpeedInfo = ( device : StorageDevice ) => {
// NVMe PCIe information
if ( device . name . startsWith ( "nvme" ) && ( device . pcie_gen || device . pcie_width )) {
const current = ` ${ device . pcie_gen || "" } ${ device . pcie_width || "" } ` . trim ()
const max =
device . pcie_max_gen && device . pcie_max_width
? ` ${ device . pcie_max_gen } ${ device . pcie_max_width } ` . trim ()
: null
const isLowerSpeed = max && current !== max
return {
text : current || null ,
maxText : max ,
isWarning : isLowerSpeed ,
color : isLowerSpeed ? "text-orange-500" : "text-blue-500" ,
}
}
// SATA information
if ( device . sata_version ) {
return {
text : device.sata_version ,
maxText : null ,
isWarning : false ,
color : "text-blue-500" ,
}
}
// SAS information
if ( device . sas_version || device . sas_speed ) {
const text = [ device . sas_version , device . sas_speed ]. filter ( Boolean ). join ( " " )
return {
text : text || null ,
maxText : null ,
isWarning : false ,
color : "text-blue-500" ,
}
}
// Generic link speed
if ( device . link_speed ) {
return {
text : device.link_speed ,
maxText : null ,
isWarning : false ,
color : "text-blue-500" ,
}
}
return null
}
const linkSpeed = getLinkSpeedInfo ( device )
2025-10-24 22:34:29 +02:00
return (
< div
key = { index }
onClick = {() => setSelectedDisk ( device )}
className = "cursor-pointer rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 p-3 transition-colors"
>
< div className = "flex items-center justify-between gap-2 mb-2" >
< span className = "text-sm font-medium truncate flex-1" >{ device . name }</ span >
< Badge className = { ` ${ diskBadge . className } px-2.5 py-0.5 shrink-0` }>{ diskBadge . label }</ Badge >
</ div >
2025-11-25 19:04:54 +01:00
{ device . size && < p className = "text-sm font-medium" >{ formatMemory ( parseLsblkSize ( device . size ))}</ p >}
2025-10-24 22:34:29 +02:00
{ device . model && (
< p className = "text-xs text-muted-foreground line-clamp-2 break-words" >{ device . model }</ p >
)}
2025-11-04 12:47:26 +01:00
{ linkSpeed && (
< div className = "mt-1 flex items-center gap-1" >
< span className = { `text-xs font-medium ${ linkSpeed . color } ` }>{ linkSpeed . text }</ span >
2025-11-04 15:28:27 +01:00
{ linkSpeed . maxText && linkSpeed . isWarning && (
< span className = "text-xs font-medium text-blue-500" >( max : { linkSpeed . maxText })</ span >
2025-11-04 12:47:26 +01:00
)}
</ div >
)}
2025-10-23 15:31:25 +02:00
</ div >
2025-10-24 22:34:29 +02:00
)
})}
2025-10-23 15:31:25 +02:00
</ div >
</ Card >
)}
{ /* Disk Detail Modal */ }
< Dialog open = { selectedDisk !== null } onOpenChange = {() => setSelectedDisk ( null )}>
< DialogContent className = "max-w-2xl" >
< DialogHeader >
< DialogTitle >{ selectedDisk ? . name }</ DialogTitle >
< DialogDescription > Storage Device Hardware Information </ DialogDescription >
</ DialogHeader >
{ selectedDisk && (
< div className = "space-y-3" >
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Device Name </ span >
< span className = "font-mono text-sm" >{ selectedDisk . name }</ span >
</ div >
2025-11-04 14:00:01 +01:00
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Type </ span >
{(() => {
2026-06-24 23:40:22 +02:00
const diskType = getDiskType ( selectedDisk . name , selectedDisk . rotation_rate )
const badgeStyles : Record < string , { className : string ; label : string }> = {
NVMe : {
className : "bg-purple-500/10 text-purple-500 border-purple-500/20" ,
label : "NVMe SSD" ,
},
SSD : {
className : "bg-cyan-500/10 text-cyan-500 border-cyan-500/20" ,
label : "SSD" ,
},
HDD : {
className : "bg-blue-500/10 text-blue-500 border-blue-500/20" ,
label : "HDD" ,
},
2025-11-04 14:00:01 +01:00
}
2026-06-24 23:40:22 +02:00
const diskBadge = badgeStyles [ diskType ]
2025-11-04 14:00:01 +01:00
return < Badge className = { diskBadge . className }>{ diskBadge . label }</ Badge >
})()}
</ div >
2025-10-23 15:31:25 +02:00
{ selectedDisk . size && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Capacity </ span >
2025-11-25 19:04:54 +01:00
< span className = "text-sm font-medium" >{ formatMemory ( parseLsblkSize ( selectedDisk . size ))}</ span >
2025-10-23 15:31:25 +02:00
</ div >
)}
2025-11-04 13:18:39 +01:00
< div className = "pt-2" >
< h3 className = "text-sm font-semibold text-muted-foreground mb-2 uppercase tracking-wide" >
Interface Information
</ h3 >
</ div >
2025-11-04 12:47:26 +01:00
2025-11-04 13:18:39 +01:00
{ /* NVMe PCIe Information */ }
{ selectedDisk . name . startsWith ( "nvme" ) && (
<>
{ selectedDisk . pcie_gen || selectedDisk . pcie_width ? (
2025-11-04 12:47:26 +01:00
<>
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Current Link Speed </ span >
2025-11-04 15:28:27 +01:00
< span
className = { `text-sm font-medium ${
selectedDisk . pcie_max_gen &&
selectedDisk . pcie_max_width &&
` ${ selectedDisk . pcie_gen } ${ selectedDisk . pcie_width } ` !==
` ${ selectedDisk . pcie_max_gen } ${ selectedDisk . pcie_max_width } `
? "text-orange-500"
: "text-blue-500"
} ` }
>
2025-11-04 13:18:39 +01:00
{ selectedDisk . pcie_gen || "PCIe" } { selectedDisk . pcie_width || "" }
2025-11-04 12:47:26 +01:00
</ span >
</ div >
{ selectedDisk . pcie_max_gen && selectedDisk . pcie_max_width && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Maximum Link Speed </ span >
2025-11-04 15:28:27 +01:00
< span className = "text-sm font-medium text-blue-500" >
2025-11-04 12:47:26 +01:00
{ selectedDisk . pcie_max_gen } { selectedDisk . pcie_max_width }
</ span >
</ div >
)}
</>
2025-11-04 13:18:39 +01:00
) : (
2025-11-04 12:47:26 +01:00
< div className = "flex justify-between border-b border-border/50 pb-2" >
2025-11-04 13:18:39 +01:00
< span className = "text-sm font-medium text-muted-foreground" > PCIe Link Speed </ span >
< span className = "text-sm text-muted-foreground italic" > Detecting ...</ span >
2025-11-04 12:47:26 +01:00
</ div >
)}
</>
)}
2025-11-04 13:18:39 +01:00
{ /* SATA Information */ }
{ ! selectedDisk . name . startsWith ( "nvme" ) && selectedDisk . sata_version && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > SATA Version </ span >
< span className = "text-sm font-medium text-blue-500" >{ selectedDisk . sata_version }</ span >
</ div >
)}
{ /* SAS Information */ }
{ ! selectedDisk . name . startsWith ( "nvme" ) && selectedDisk . sas_version && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > SAS Version </ span >
< span className = "text-sm font-medium text-blue-500" >{ selectedDisk . sas_version }</ span >
</ div >
)}
{ ! selectedDisk . name . startsWith ( "nvme" ) && selectedDisk . sas_speed && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > SAS Speed </ span >
< span className = "text-sm font-medium text-blue-500" >{ selectedDisk . sas_speed }</ span >
</ div >
)}
{ /* Generic Link Speed - only show if no specific interface info */ }
{ ! selectedDisk . name . startsWith ( "nvme" ) &&
selectedDisk . link_speed &&
! selectedDisk . pcie_gen &&
! selectedDisk . sata_version &&
! selectedDisk . sas_version && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Link Speed </ span >
< span className = "text-sm font-medium text-blue-500" >{ selectedDisk . link_speed }</ span >
</ div >
)}
2025-10-23 15:31:25 +02:00
{ selectedDisk . model && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Model </ span >
< span className = "text-sm text-right" >{ selectedDisk . model }</ span >
</ div >
)}
{ selectedDisk . family && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Family </ span >
< span className = "text-sm text-right" >{ selectedDisk . family }</ span >
</ div >
)}
{ selectedDisk . serial && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Serial Number </ span >
< span className = "font-mono text-sm" >{ selectedDisk . serial }</ span >
</ div >
)}
{ selectedDisk . firmware && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Firmware </ span >
< span className = "font-mono text-sm" >{ selectedDisk . firmware }</ span >
</ div >
)}
{ selectedDisk . interface && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Interface </ span >
< span className = "text-sm font-medium" >{ selectedDisk . interface }</ span >
</ div >
)}
{ selectedDisk . driver && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Driver </ span >
< span className = "font-mono text-sm text-green-500" >{ selectedDisk . driver }</ span >
</ div >
)}
2025-10-29 22:16:40 +01:00
{ selectedDisk . rotation_rate !== undefined && selectedDisk . rotation_rate !== null && (
2025-10-23 15:31:25 +02:00
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Rotation Rate </ span >
2025-11-03 23:26:04 +01:00
< div className = "text-sm" >
{ typeof selectedDisk . rotation_rate === "number" && selectedDisk . rotation_rate === - 1
? "N/A"
: typeof selectedDisk . rotation_rate === "number" && selectedDisk . rotation_rate > 0
? ` ${ selectedDisk . rotation_rate } rpm`
: typeof selectedDisk . rotation_rate === "string"
? selectedDisk . rotation_rate
: "Solid State Device" }
</ div >
2025-10-23 15:31:25 +02:00
</ div >
)}
{ selectedDisk . form_factor && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Form Factor </ span >
< span className = "text-sm" >{ selectedDisk . form_factor }</ span >
</ div >
)}
</ div >
)}
</ DialogContent >
</ Dialog >
2025-11-30 22:50:40 +01:00
2026-04-17 19:53:17 +02:00
{ /* USB Devices — everything physically plugged into the host's USB ports.
Root hubs (vendor 1d6b) are already filtered out by the backend. The
section is hidden on headless servers that have nothing attached. */ }
{ hardwareData ? . usb_devices && hardwareData . usb_devices . length > 0 && (
< Card className = "border-border/50 bg-card/50 p-6" >
< div className = "mb-4 flex items-center gap-2" >
< Usb className = "h-5 w-5 text-primary" />
< h2 className = "text-lg font-semibold" > USB Devices </ h2 >
< Badge variant = "outline" className = "ml-auto" >
{ hardwareData . usb_devices . length } device { hardwareData . usb_devices . length > 1 ? "s" : "" }
</ Badge >
</ div >
< div className = "grid gap-3 sm:grid-cols-2 lg:grid-cols-3" >
{ hardwareData . usb_devices . map (( usb , index ) => (
< div
key = { `usb- ${ index } - ${ usb . bus_device } ` }
onClick = {() => setSelectedUsbDevice ( usb )}
className = "cursor-pointer rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 p-3 transition-colors"
>
< div className = "flex items-center justify-between gap-2 mb-1" >
< span className = "text-sm font-medium line-clamp-2 break-words flex-1" >
{ usb . name }
</ span >
< Badge className = { getDeviceTypeColor ( usb . class_label )}>
{ usb . class_label }
</ Badge >
</ div >
< div className = "space-y-0.5 text-xs text-muted-foreground" >
{ usb . speed_label && < div >{ usb . speed_label }</ div >}
< div className = "font-mono" >
{ usb . bus_device } · { usb . vendor_id } : { usb . product_id }
</ div >
{ usb . driver && (
< div className = "font-mono text-green-500/80" > Driver : { usb . driver }</ div >
)}
</ div >
</ div >
))}
</ div >
</ Card >
)}
{ /* USB Device detail modal — mirrors the PCI Device modal for consistency. */ }
< Dialog open = { selectedUsbDevice !== null } onOpenChange = {( open ) => ! open && setSelectedUsbDevice ( null )}>
< DialogContent className = "max-w-2xl" >
< DialogHeader >
< DialogTitle >{ selectedUsbDevice ? . name }</ DialogTitle >
< DialogDescription > USB Device Information </ DialogDescription >
</ DialogHeader >
{ selectedUsbDevice && (
< div className = "space-y-3" >
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Class </ span >
< Badge className = { getDeviceTypeColor ( selectedUsbDevice . class_label )}>
{ selectedUsbDevice . class_label }
</ Badge >
</ div >
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Bus :Device </ span >
< span className = "font-mono text-sm" >{ selectedUsbDevice . bus_device }</ span >
</ div >
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Device Name </ span >
< span className = "text-sm text-right" >{ selectedUsbDevice . name }</ span >
</ div >
{ selectedUsbDevice . vendor && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Vendor </ span >
< span className = "text-sm" >{ selectedUsbDevice . vendor }</ span >
</ div >
)}
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Vendor / Product ID </ span >
< span className = "font-mono text-sm" >
{ selectedUsbDevice . vendor_id } : { selectedUsbDevice . product_id }
</ span >
</ div >
{ selectedUsbDevice . speed_label && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Speed </ span >
< span className = "text-sm" >
{ selectedUsbDevice . speed_label }
{ selectedUsbDevice . speed_mbps > 0 && (
< span className = "text-muted-foreground/60 ml-2" >({ selectedUsbDevice . speed_mbps } Mbps )</ span >
)}
</ span >
</ div >
)}
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Class Code </ span >
< span className = "font-mono text-sm" > 0 x { selectedUsbDevice . class_code }</ span >
</ div >
{ selectedUsbDevice . driver && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Driver </ span >
< span className = "font-mono text-sm text-green-500" >{ selectedUsbDevice . driver }</ span >
</ div >
)}
{ selectedUsbDevice . serial && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Serial </ span >
< span className = "font-mono text-sm text-right break-all" >{ selectedUsbDevice . serial }</ span >
</ div >
)}
</ div >
)}
</ DialogContent >
</ Dialog >
2025-11-30 22:50:40 +01:00
{ /* NVIDIA Installation Monitor */ }
2025-12-01 01:04:31 +01:00
{ /* <HybridScriptMonitor
sessionId={nvidiaSessionId}
title="NVIDIA Driver Installation"
description="Installing NVIDIA proprietary drivers for GPU monitoring..."
onClose={() => {
setNvidiaSessionId(null)
2026-04-17 16:39:08 +02:00
mutateStatic()
2025-12-01 01:04:31 +01:00
}}
onComplete={(success) => {
if (success) {
2026-04-17 16:39:08 +02:00
mutateStatic()
2025-12-01 01:04:31 +01:00
}
}}
/> */ }
< ScriptTerminalModal
open = { showNvidiaInstaller }
onClose = {() => {
setShowNvidiaInstaller ( false )
2026-04-17 16:39:08 +02:00
mutateStatic ()
2025-12-01 01:04:31 +01:00
}}
scriptPath = "/usr/local/share/proxmenux/scripts/gpu_tpu/nvidia_installer.sh"
scriptName = "nvidia_installer"
2025-12-06 12:25:57 +01:00
params = {{
EXECUTION_MODE : "web" ,
}}
2025-12-01 01:04:31 +01:00
title = "NVIDIA Driver Installation"
description = "Installing NVIDIA proprietary drivers for GPU monitoring..."
/>
2026-01-29 19:04:52 +01:00
< ScriptTerminalModal
open = { showAmdInstaller }
onClose = {() => {
setShowAmdInstaller ( false )
2026-04-17 16:39:08 +02:00
mutateStatic ()
2026-01-29 19:04:52 +01:00
}}
scriptPath = "/usr/local/share/proxmenux/scripts/gpu_tpu/amd_gpu_tools.sh"
scriptName = "amd_gpu_tools"
params = {{
EXECUTION_MODE : "web" ,
}}
2026-01-29 20:03:39 +01:00
title = "AMD GPU Tools Installation"
description = "Installing amdgpu_top for AMD GPU monitoring..."
/>
< ScriptTerminalModal
open = { showIntelInstaller }
onClose = {() => {
setShowIntelInstaller ( false )
2026-04-17 16:39:08 +02:00
mutateStatic ()
2026-01-29 20:03:39 +01:00
}}
scriptPath = "/usr/local/share/proxmenux/scripts/gpu_tpu/intel_gpu_tools.sh"
scriptName = "intel_gpu_tools"
params = {{
EXECUTION_MODE : "web" ,
}}
title = "Intel GPU Tools Installation"
description = "Installing intel-gpu-tools for Intel GPU monitoring..."
/>
2026-04-17 19:53:17 +02:00
< ScriptTerminalModal
open = { showCoralInstaller }
onClose = {() => {
setShowCoralInstaller ( false )
mutateStatic ()
}}
2026-04-17 21:55:53 +02:00
scriptPath = "/usr/local/share/proxmenux/scripts/gpu_tpu/install_coral.sh"
scriptName = "install_coral"
2026-04-17 19:53:17 +02:00
params = {{
EXECUTION_MODE : "web" ,
}}
title = "Coral TPU Driver Installation"
description = "Installing gasket + apex kernel modules and Edge TPU runtime..."
/>
2026-04-09 15:02:25 +02:00
{ /* GPU Switch Mode Modal */ }
2026-04-09 19:58:54 +02:00
{ switchModeParams && (
< ScriptTerminalModal
open = { showSwitchModeModal }
onClose = { handleSwitchModeModalClose }
scriptPath = "/usr/local/share/proxmenux/scripts/gpu_tpu/switch_gpu_mode_direct.sh"
scriptName = "switch_gpu_mode_direct"
params = {{
EXECUTION_MODE : "web" ,
2026-04-09 21:03:06 +02:00
GPU_SWITCH_PARAMS : ` ${ switchModeParams . gpuSlot } | ${ switchModeParams . targetMode } ` ,
2026-04-09 19:58:54 +02:00
}}
title = { `GPU Switch Mode → ${ switchModeParams . targetMode . toUpperCase () } ` }
description = { `Switching GPU ${ switchModeParams . gpuSlot } to ${ switchModeParams . targetMode === "vm" ? "VM (VFIO passthrough)" : "LXC (native driver)" } mode...` }
/>
)}
2026-01-29 20:03:39 +01:00
</ div >
2025-10-05 20:45:54 +02:00
)
}