2025-10-06 22:23:56 +02:00
"use client"
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"
2025-10-23 15:31:25 +02:00
import {
Thermometer ,
CpuIcon ,
Zap ,
HardDrive ,
Network ,
FanIcon ,
PowerIcon ,
Battery ,
Cpu ,
MemoryStick ,
Cpu as Gpu ,
Loader2 ,
} from "lucide-react"
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-10-06 22:23:56 +02:00
import { type HardwareData , type GPU , type PCIDevice , type StorageDevice , fetcher } from "../types/hardware"
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
const match = cleaned . match ( /^([\d.]+)([KMGT]?)$/ )
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
// Convert to GB if >= 1024 MB
if ( mb >= 1024 ) {
const gb = mb / 1024
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
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 ] )
}
2025-10-10 00:13:54 +02:00
2025-10-10 21:18:49 +02:00
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 ( )
if ( lowerType . includes ( "storage" ) || lowerType . includes ( "sata" ) || lowerType . includes ( "raid" ) ) {
return "bg-orange-500/10 text-orange-500 border-orange-500/20"
}
if ( lowerType . includes ( "usb" ) ) {
return "bg-purple-500/10 text-purple-500 border-purple-500/20"
}
if ( lowerType . includes ( "network" ) || lowerType . includes ( "ethernet" ) ) {
return "bg-blue-500/10 text-blue-500 border-blue-500/20"
}
if ( lowerType . includes ( "graphics" ) || lowerType . includes ( "vga" ) || lowerType . includes ( "display" ) ) {
return "bg-green-500/10 text-green-500 border-green-500/20"
}
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-24 22:50:03 +02:00
const improveSensorLabel = ( sensorName : string , adapter : string , chipName? : string ) : string = > {
2025-10-24 22:34:29 +02:00
const adapterLower = adapter ? . toLowerCase ( ) || ""
2025-10-24 22:50:03 +02:00
const chipNameLower = chipName ? . toLowerCase ( ) || ""
const sensorNameLower = sensorName ? . toLowerCase ( ) || ""
console . log ( "[v0] improveSensorLabel called:" , { sensorName , adapter , chipName , adapterLower , chipNameLower } )
const isNVIDIA =
adapterLower . includes ( "nouveau" ) ||
adapterLower . includes ( "nvidia" ) ||
chipNameLower . includes ( "nouveau" ) ||
chipNameLower . includes ( "nvidia" ) ||
// If it's a PCI adapter and the sensor name suggests it's a GPU sensor
( adapterLower . includes ( "pci" ) &&
( sensorNameLower . includes ( "temp" ) || sensorNameLower . includes ( "fan" ) || sensorNameLower . includes ( "pwm" ) ) )
if ( isNVIDIA ) {
console . log ( "[v0] NVIDIA/GPU sensor detected!" )
2025-10-24 22:34:29 +02:00
// Improve temperature labels
2025-10-24 22:50:03 +02:00
if ( sensorNameLower . includes ( "temp" ) ) {
2025-10-24 22:34:29 +02:00
return "NVIDIA GPU Temperature"
}
// Improve fan labels
2025-10-24 22:50:03 +02:00
if ( sensorNameLower . includes ( "fan" ) ) {
2025-10-24 22:34:29 +02:00
return "NVIDIA GPU Fan"
}
// Improve PWM labels
2025-10-24 22:50:03 +02:00
if ( sensorNameLower . includes ( "pwm" ) ) {
2025-10-24 22:34:29 +02:00
return "NVIDIA GPU PWM"
}
}
// Return original name if no improvement needed
return sensorName
}
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 22:34:29 +02:00
if ( adapterLower . includes ( "nouveau" ) || adapterLower . includes ( "nvidia" ) ) {
groups . GPU . push ( temp )
} else 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() {
2025-10-14 19:00:24 +02:00
const {
data : hardwareData ,
error ,
isLoading ,
} = useSWR < HardwareData > ( "/api/hardware" , fetcher , {
2025-10-06 22:23:56 +02:00
refreshInterval : 5000 ,
} )
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-10-05 22:46:14 +02:00
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 {
const apiUrl = ` http:// ${ window . location . hostname } :8008/api/gpu/ ${ fullSlot } /realtime `
const response = await fetch ( apiUrl , {
method : "GET" ,
headers : {
"Content-Type" : "application/json" ,
} ,
signal : abortController.signal ,
} )
if ( ! response . ok ) {
throw new Error ( ` HTTP error! status: ${ response . status } ` )
}
const data = await response . json ( )
setRealtimeGPUData ( data )
setDetailsLoading ( false )
} catch ( error ) {
// Only log non-abort errors
if ( error instanceof Error && error . name !== "AbortError" ) {
console . error ( "[v0] Error fetching GPU realtime data:" , error )
}
setRealtimeGPUData ( { has_monitoring_tool : false } )
setDetailsLoading ( false )
}
}
// Initial fetch
fetchRealtimeData ( )
// Poll every 3 seconds
const interval = setInterval ( fetchRealtimeData , 3000 )
return ( ) = > {
clearInterval ( interval )
abortController . abort ( )
}
2025-10-14 15:34:19 +02:00
} , [ selectedGPU ] )
2025-10-08 12:06:24 +02:00
2025-10-09 23:14:47 +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 = > {
if ( ! hardwareData ? . pci_devices || ! gpu . slot ) return null
// Try to find exact match first (e.g., "00:02.0")
let pciDevice = hardwareData . pci_devices . find ( ( d ) = > d . slot === gpu . slot )
// If not found, try to match by partial slot (e.g., "00" matches "00:02.0")
if ( ! pciDevice && gpu . slot . length <= 2 ) {
pciDevice = hardwareData . pci_devices . find (
( 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
}
2025-10-14 19:00:24 +02:00
if ( isLoading ) {
return (
2025-10-18 18:32:13 +02:00
< div className = "space-y-6" >
2025-10-14 19:00:24 +02:00
< div className = "text-center py-8" >
< div className = "text-lg font-medium text-foreground mb-2" > Loading hardware data . . . < / div >
< / div >
< / 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 */ }
{ ( hardwareData ? . cpu || hardwareData ? . motherboard ) && (
< 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 */ }
{ hardwareData ? . cpu && Object . keys ( hardwareData . cpu ) . length > 0 && (
< 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" >
{ hardwareData . cpu . model && (
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Model < / span >
< span className = "font-medium text-right" > { hardwareData . cpu . model } < / span >
< / div >
) }
{ hardwareData . cpu . cores_per_socket && hardwareData . cpu . sockets && (
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Cores < / span >
< span className = "font-medium" >
{ hardwareData . cpu . sockets } × { hardwareData . cpu . cores_per_socket } = { " " }
{ hardwareData . cpu . sockets * hardwareData . cpu . cores_per_socket } cores
< / span >
< / div >
) }
{ hardwareData . cpu . total_threads && (
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Threads < / span >
< span className = "font-medium" > { hardwareData . cpu . total_threads } < / span >
< / div >
) }
{ hardwareData . cpu . l3_cache && (
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > L3 Cache < / span >
< span className = "font-medium" > { hardwareData . cpu . l3_cache } < / span >
< / div >
) }
{ hardwareData . cpu . virtualization && (
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Virtualization < / span >
< span className = "font-medium" > { hardwareData . cpu . virtualization } < / span >
< / div >
) }
< / div >
< / div >
) }
{ /* Motherboard Info */ }
{ hardwareData ? . motherboard && Object . keys ( hardwareData . motherboard ) . length > 0 && (
< 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" >
{ hardwareData . motherboard . manufacturer && (
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Manufacturer < / span >
< span className = "font-medium text-right" > { hardwareData . motherboard . manufacturer } < / span >
< / div >
) }
{ hardwareData . motherboard . model && (
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Model < / span >
< span className = "font-medium text-right" > { hardwareData . motherboard . model } < / span >
< / div >
) }
{ hardwareData . motherboard . bios ? . vendor && (
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > BIOS < / span >
< span className = "font-medium text-right" > { hardwareData . motherboard . bios . vendor } < / span >
< / div >
) }
{ hardwareData . motherboard . bios ? . version && (
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Version < / span >
< span className = "font-medium" > { hardwareData . motherboard . bios . version } < / span >
< / div >
) }
{ hardwareData . motherboard . bios ? . date && (
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Date < / span >
< span className = "font-medium" > { hardwareData . motherboard . bios . date } < / span >
< / div >
) }
< / div >
< / div >
) }
< / div >
< / Card >
) }
{ /* Memory Modules */ }
{ hardwareData ? . memory_modules && hardwareData . memory_modules . length > 0 && (
< 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" >
{ hardwareData . memory_modules . length } installed
< / Badge >
< / div >
< div className = "grid gap-3 md:grid-cols-2 lg:grid-cols-3" >
{ 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 >
) }
{ module . speed && (
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Speed < / span >
< span className = "font-medium" > { module . speed } < / span >
< / 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 */ }
2025-10-06 22:23:56 +02:00
{ hardwareData ? . temperatures && hardwareData . temperatures . length > 0 && (
< 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" >
{ hardwareData . temperatures . length } sensors
< / Badge >
< / div >
2025-10-24 22:34:29 +02:00
< div className = "grid gap-6 md:grid-cols-2" >
{ /* CPU Sensors */ }
{ groupAndSortTemperatures ( hardwareData . temperatures ) . CPU . length > 0 && (
< 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" >
{ groupAndSortTemperatures ( hardwareData . temperatures ) . CPU . length }
< / Badge >
< / div >
< div className = "grid gap-4 md:grid-cols-2" >
{ groupAndSortTemperatures ( hardwareData . temperatures ) . CPU . map ( ( temp , index ) = > {
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 >
) }
2025-10-14 17:23:59 +02:00
2025-10-24 22:34:29 +02:00
{ /* GPU Sensors */ }
{ groupAndSortTemperatures ( hardwareData . temperatures ) . GPU . length > 0 && (
< div
className = { groupAndSortTemperatures ( hardwareData . temperatures ) . GPU . length > 1 ? "md:col-span-2" : "" }
>
< 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" >
{ groupAndSortTemperatures ( hardwareData . temperatures ) . GPU . length }
< / Badge >
< / div >
< div
className = { ` grid gap-4 ${ groupAndSortTemperatures ( hardwareData . temperatures ) . GPU . length > 1 ? "md:grid-cols-2" : "" } ` }
>
{ groupAndSortTemperatures ( hardwareData . temperatures ) . GPU . map ( ( temp , index ) = > {
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-14 17:23:59 +02:00
2025-10-24 22:50:03 +02:00
const displayName = improveSensorLabel ( temp . name , temp . adapter , temp . chip_name )
2025-10-14 17:23:59 +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" > { displayName } < / 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 >
) }
2025-10-14 17:23:59 +02:00
2025-10-24 22:34:29 +02:00
{ /* NVME Sensors */ }
{ groupAndSortTemperatures ( hardwareData . temperatures ) . NVME . length > 0 && (
< div
className = { groupAndSortTemperatures ( hardwareData . temperatures ) . NVME . length > 1 ? "md:col-span-2" : "" }
>
< 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" >
{ groupAndSortTemperatures ( hardwareData . temperatures ) . NVME . length }
< / Badge >
< / div >
< div
className = { ` grid gap-4 ${ groupAndSortTemperatures ( hardwareData . temperatures ) . NVME . length > 1 ? "md:grid-cols-2" : "" } ` }
>
{ groupAndSortTemperatures ( hardwareData . temperatures ) . NVME . map ( ( temp , index ) = > {
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 */ }
{ groupAndSortTemperatures ( hardwareData . temperatures ) . PCI . length > 0 && (
< div
className = { groupAndSortTemperatures ( hardwareData . temperatures ) . PCI . length > 1 ? "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" > PCI < / h3 >
< Badge variant = "outline" className = "text-xs" >
{ groupAndSortTemperatures ( hardwareData . temperatures ) . PCI . length }
< / Badge >
< / div >
< div
className = { ` grid gap-4 ${ groupAndSortTemperatures ( hardwareData . temperatures ) . PCI . length > 1 ? "md:grid-cols-2" : "" } ` }
>
{ groupAndSortTemperatures ( hardwareData . temperatures ) . PCI . map ( ( temp , index ) = > {
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-24 22:50:03 +02:00
const displayName = improveSensorLabel ( temp . name , temp . adapter , temp . chip_name )
2025-10-24 22:42:08 +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" >
2025-10-24 22:42:08 +02:00
< span className = "text-sm font-medium" > { displayName } < / 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 */ }
{ groupAndSortTemperatures ( hardwareData . temperatures ) . OTHER . length > 0 && (
< div
className = { groupAndSortTemperatures ( hardwareData . temperatures ) . OTHER . length > 1 ? "md:col-span-2" : "" }
>
< 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" >
{ groupAndSortTemperatures ( hardwareData . temperatures ) . OTHER . length }
< / Badge >
< / div >
< div
className = { ` grid gap-4 ${ groupAndSortTemperatures ( hardwareData . temperatures ) . OTHER . length > 1 ? "md:grid-cols-2" : "" } ` }
>
{ groupAndSortTemperatures ( hardwareData . temperatures ) . OTHER . map ( ( temp , index ) = > {
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 >
2025-10-14 17:23:59 +02:00
< / div >
2025-10-24 22:34:29 +02:00
) }
< / 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 */ }
2025-10-06 22:23:56 +02:00
{ hardwareData ? . gpus && hardwareData . gpus . length > 0 && (
< 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" >
{ hardwareData . gpus . length } GPU { hardwareData . gpus . length > 1 ? "s" : "" }
< / Badge >
< / div >
2025-10-06 22:39:37 +02:00
< div className = "grid gap-4 sm:grid-cols-2" >
2025-10-06 23:40:54 +02:00
{ hardwareData . gpus . map ( ( gpu , index ) = > {
const pciDevice = findPCIDeviceForGPU ( gpu )
const fullSlot = pciDevice ? . slot || gpu . slot
2025-10-06 22:23:56 +02:00
2025-10-06 23:40:54 +02:00
return (
< div
key = { index }
2025-10-07 02:46:35 +02:00
onClick = { ( ) = > handleGPUClick ( gpu ) }
2025-10-17 19:34:35 +02:00
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"
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:23:56 +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 && (
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > PCI Slot < / span >
< span className = "font-mono text-xs" > { fullSlot } < / span >
< / div >
) }
2025-10-06 22:39:37 +02:00
2025-10-06 23:40:54 +02:00
{ gpu . pci_driver && (
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Driver < / span >
< span className = "font-mono text-xs text-green-500" > { gpu . pci_driver } < / span >
< / div >
) }
2025-10-06 22:23:56 +02:00
2025-10-06 23:40:54 +02:00
{ gpu . pci_kernel_module && (
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" > Kernel Module < / span >
< span className = "font-mono text-xs" > { gpu . pci_kernel_module } < / span >
2025-10-06 22:23:56 +02:00
< / div >
2025-10-06 23:40:54 +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-10-07 02:46:35 +02:00
< Dialog
2025-10-08 12:06:24 +02:00
open = { selectedGPU !== null }
2025-10-07 02:46:35 +02:00
onOpenChange = { ( ) = > {
setSelectedGPU ( null )
setRealtimeGPUData ( 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 >
< 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 >
< / div >
) : realtimeGPUData ? . has_monitoring_tool === true ? (
< >
< 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 >
< 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 >
) }
2025-10-09 23:14:47 +02:00
< / div >
2025-10-23 15:31:25 +02:00
< / 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 >
< Progress value = { utilizationNum } className = "h-1.5 [&>div]:bg-blue-500" / >
< / 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 >
) }
< / >
) : (
< 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 >
< div >
< h4 className = "text-sm font-semibold text-blue-500 mb-1" > Extended Monitoring Not Available < / h4 >
< p className = "text-sm text-muted-foreground" >
{ getMonitoringToolRecommendation ( selectedGPU . vendor ) }
< / p >
< / div >
2025-10-08 12:06:24 +02:00
< / div >
< / div >
) }
2025-10-23 15:31:25 +02:00
< / div >
< / >
) }
< / DialogContent >
< / Dialog >
{ /* PCI Devices - Changed to modal */ }
{ hardwareData ? . pci_devices && hardwareData . pci_devices . length > 0 && (
< 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" >
{ hardwareData . pci_devices . length } devices
< / Badge >
< / div >
< div className = "grid gap-3 sm:grid-cols-2 lg:grid-cols-3" >
{ hardwareData . pci_devices . map ( ( device , index ) = > (
< 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 >
) }
2025-10-06 22:23:56 +02:00
2025-10-23 15:31:25 +02:00
{ /* 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 >
< 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 >
{ /* Power Consumption */ }
{ hardwareData ? . power_meter && (
< 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" >
< 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-16 19:23:41 +02:00
) }
< / div >
2025-10-23 15:31:25 +02:00
< div className = "text-right" >
< p className = "text-2xl font-bold text-blue-500" > { hardwareData . power_meter . watts . toFixed ( 1 ) } W < / p >
< p className = "text-xs text-muted-foreground" > Current Draw < / p >
< / div >
< / div >
< / div >
< / Card >
) }
{ /* Power Supplies */ }
{ hardwareData ? . power_supplies && hardwareData . power_supplies . length > 0 && (
< 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" >
{ hardwareData . power_supplies . length } PSUs
< / Badge >
< / div >
< div className = "grid gap-3 md:grid-cols-2" >
{ hardwareData . power_supplies . map ( ( psu , index ) = > (
< 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 */ }
{ hardwareData ? . fans && hardwareData . fans . length > 0 && (
< 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" >
{ hardwareData . fans . length } fans
< / Badge >
< / div >
< div className = "grid gap-4 md:grid-cols-2" >
{ hardwareData . fans . map ( ( fan , index ) = > {
const isPercentage = fan . unit === "percent" || fan . unit === "%"
const percentage = isPercentage ? fan.speed : Math.min ( ( fan . speed / 5000 ) * 100 , 100 )
2025-10-24 22:50:03 +02:00
const displayName = improveSensorLabel ( fan . name , fan . adapter , fan . chip_name )
2025-10-24 22:34:29 +02:00
2025-10-23 15:31:25 +02:00
return (
< div key = { index } className = "space-y-2" >
< div className = "flex items-center justify-between" >
2025-10-24 22:34:29 +02:00
< span className = "text-sm font-medium" > { displayName } < / 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 */ }
{ hardwareData ? . ups && Array . isArray ( hardwareData . ups ) && hardwareData . ups . length > 0 && (
< 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" >
{ hardwareData . ups . length } UPS
< / Badge >
< / div >
< div className = "grid gap-4 md:grid-cols-2" >
{ hardwareData . ups . map ( ( ups : any , index : number ) = > {
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" >
< span className = "text-sm text-muted-foreground" > Manufacturer < / span >
< span className = "text-sm font-medium" > { selectedUPS . manufacturer } < / span >
< / div >
) }
{ selectedUPS . model && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Model < / span >
< span className = "text-sm font-medium" > { selectedUPS . model } < / span >
< / div >
) }
{ selectedUPS . serial && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Serial Number < / span >
< span className = "font-mono text-sm" > { selectedUPS . serial } < / span >
< / div >
) }
{ selectedUPS . firmware && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Firmware < / span >
< span className = "text-sm font-medium" > { selectedUPS . firmware } < / span >
< / div >
) }
{ selectedUPS . driver && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm text-muted-foreground" > Driver < / span >
< span className = "font-mono text-sm text-green-500" > { selectedUPS . driver } < / span >
< / div >
) }
< / div >
< / div >
< / div >
2025-10-23 14:58:46 +02:00
< / >
2025-10-06 22:23:56 +02:00
) }
< / DialogContent >
< / Dialog >
2025-10-23 15:31:25 +02:00
{ /* Network Summary - Clickable */ }
{ hardwareData ? . pci_devices &&
hardwareData . pci_devices . filter ( ( d ) = > d . type . toLowerCase ( ) . includes ( "network" ) ) . length > 0 && (
< 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" >
{ hardwareData . pci_devices . filter ( ( d ) = > d . type . toLowerCase ( ) . includes ( "network" ) ) . length } interfaces
< / Badge >
< / div >
< div className = "grid gap-3 sm:grid-cols-2 lg:grid-cols-3" >
{ hardwareData . pci_devices
. 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 = "bg-blue-500/10 text-blue-500 border-blue-500/20 px-2.5 py-0.5 shrink-0" >
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 >
< p className = "mt-4 text-xs text-muted-foreground" > Click on an interface for detailed information < / p >
< / Card >
) }
{ /* 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 */ }
{ hardwareData ? . storage_devices && hardwareData . storage_devices . length > 0 && (
< 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" >
{
hardwareData . storage_devices . filter (
( 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" >
{ hardwareData . storage_devices
. filter (
( device ) = > device . type === "disk" && ! device . name . startsWith ( "zd" ) && ! device . name . startsWith ( "loop" ) ,
)
2025-10-24 22:34:29 +02:00
. map ( ( device , index ) = > {
const getDiskTypeBadge = ( diskName : string , rotationRate : number | undefined ) = > {
let diskType = "HDD"
if ( diskName . startsWith ( "nvme" ) ) {
diskType = "NVMe"
} else if ( ! rotationRate || rotationRate === 0 ) {
diskType = "SSD"
}
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 )
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 >
{ device . size && < p className = "text-sm font-medium" > { formatMemory ( parseLsblkSize ( device . size ) ) } < / p > }
{ device . model && (
< p className = "text-xs text-muted-foreground line-clamp-2 break-words" > { device . model } < / p >
) }
{ device . driver && (
< p className = "mt-1 font-mono text-xs text-green-500 truncate" > Driver : { device . driver } < / p >
) }
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 >
< p className = "mt-4 text-xs text-muted-foreground" > Click on a device for detailed hardware information < / p >
< / 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 >
{ selectedDisk . type && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Type < / span >
< Badge className = "bg-blue-500/10 text-blue-500 border-blue-500/20" > { selectedDisk . type } < / Badge >
< / div >
) }
{ 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 >
< span className = "text-sm font-medium" > { formatMemory ( parseLsblkSize ( selectedDisk . size ) ) } < / span >
< / div >
) }
{ 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 >
) }
{ selectedDisk . rotation_rate && (
< div className = "flex justify-between border-b border-border/50 pb-2" >
< span className = "text-sm font-medium text-muted-foreground" > Rotation Rate < / span >
< span className = "text-sm" > { selectedDisk . rotation_rate } < / span >
< / 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 >
) }
{ 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" > { selectedDisk . sata_version } < / span >
< / div >
) }
< / div >
) }
< / DialogContent >
< / Dialog >
2025-10-05 20:45:54 +02:00
< / div >
)
}