Files
ProxMenux/AppImage/components/hardware.tsx
2025-10-23 14:58:46 +02:00

833 lines
36 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { Card } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Thermometer, CpuIcon, HardDrive, Cpu, MemoryStick, Cpu as Gpu } from "lucide-react"
import useSWR from "swr"
import { useState, useEffect } from "react"
import { type HardwareData, type GPU, type PCIDevice, type StorageDevice, fetcher } from "../types/hardware"
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
}
}
const formatMemory = (memoryKB: number | string): string => {
const kb = typeof memoryKB === "string" ? Number.parseFloat(memoryKB) : memoryKB
if (isNaN(kb)) return "N/A"
// Convert KB to MB
const mb = kb / 1024
// Convert to TB if >= 1024 GB
if (mb >= 1024 * 1024) {
const tb = mb / (1024 * 1024)
return `${tb.toFixed(1)} TB`
}
// 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`
}
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])
}
if (isNaN(mhz)) return String(clockString)
// 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`
}
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"
}
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")) {
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."
}
if (lowerVendor.includes("amd") || lowerVendor.includes("ati")) {
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"
}
return "To get extended GPU monitoring information, please install the appropriate GPU monitoring tools for your hardware."
}
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() || ""
if (nameLower.includes("cpu") || nameLower.includes("core") || nameLower.includes("package")) {
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
}
export default function Hardware() {
const {
data: hardwareData,
error,
isLoading,
} = useSWR<HardwareData>("/api/hardware", fetcher, {
refreshInterval: 5000,
})
const [selectedGPU, setSelectedGPU] = useState<GPU | null>(null)
const [realtimeGPUData, setRealtimeGPUData] = useState<any>(null)
const [detailsLoading, setDetailsLoading] = useState(false)
const [selectedPCIDevice, setSelectedPCIDevice] = useState<PCIDevice | null>(null)
const [selectedDisk, setSelectedDisk] = useState<StorageDevice | null>(null)
const [selectedNetwork, setSelectedNetwork] = useState<PCIDevice | null>(null)
const [selectedUPS, setSelectedUPS] = useState<any>(null)
useEffect(() => {
if (!selectedGPU) return
const pciDevice = findPCIDeviceForGPU(selectedGPU)
const fullSlot = pciDevice?.slot || selectedGPU.slot
if (!fullSlot) return
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()
}
}, [selectedGPU])
const handleGPUClick = async (gpu: GPU) => {
setSelectedGPU(gpu)
setDetailsLoading(true)
setRealtimeGPUData(null)
}
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
}
const hasRealtimeData = (): boolean => {
if (!realtimeGPUData) return false
// Esto permite mostrar datos incluso cuando la GPU está inactiva (valores en 0 o null)
return realtimeGPUData.has_monitoring_tool === true
}
if (isLoading) {
return (
<div className="space-y-6">
<div className="text-center py-8">
<div className="text-lg font-medium text-foreground mb-2">Loading hardware data...</div>
</div>
</div>
)
}
return (
<div className="space-y-6">
{/* 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) => (
<div key={index} className="rounded-lg border border-border/30 bg-background/60 p-4">
<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>
<span className="font-medium text-green-500">{formatMemory(module.size)}</span>
</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>
)}
{/* 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"),
)
.map((device, index) => (
<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="bg-blue-500/10 text-blue-500 border-blue-500/20 px-2.5 py-0.5 shrink-0">
{device.type}
</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>
)}
</div>
))}
</div>
<p className="mt-4 text-xs text-muted-foreground">Click on a device for detailed hardware information</p>
</Card>
)}
{/* Thermal Monitoring */}
{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>
{(() => {
const groupedTemps = groupAndSortTemperatures(hardwareData.temperatures)
return (
<div className="grid gap-6 md:grid-cols-2">
{/* CPU Sensors */}
{groupedTemps.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">
{groupedTemps.CPU.length}
</Badge>
</div>
<div className="grid gap-4 md:grid-cols-2">
{groupedTemps.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>
)}
{/* GPU Sensors */}
{groupedTemps.GPU.length > 0 && (
<div className={groupedTemps.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">
{groupedTemps.GPU.length}
</Badge>
</div>
<div className={`grid gap-4 ${groupedTemps.GPU.length > 1 ? "md:grid-cols-2" : ""}`}>
{groupedTemps.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)
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>
)}
{/* NVME Sensors */}
{groupedTemps.NVME.length > 0 && (
<div className={groupedTemps.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">
{groupedTemps.NVME.length}
</Badge>
</div>
<div className={`grid gap-4 ${groupedTemps.NVME.length > 1 ? "md:grid-cols-2" : ""}`}>
{groupedTemps.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 */}
{groupedTemps.PCI.length > 0 && (
<div className={groupedTemps.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">
{groupedTemps.PCI.length}
</Badge>
</div>
<div className={`grid gap-4 ${groupedTemps.PCI.length > 1 ? "md:grid-cols-2" : ""}`}>
{groupedTemps.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)
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>
)}
{/* OTHER Sensors */}
{groupedTemps.OTHER.length > 0 && (
<div className={groupedTemps.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">
{groupedTemps.OTHER.length}
</Badge>
</div>
<div className={`grid gap-4 ${groupedTemps.OTHER.length > 1 ? "md:grid-cols-2" : ""}`}>
{groupedTemps.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>
</div>
)}
</div>
)
})()}
</Card>
)}
{/* GPU Information - Enhanced with on-demand data fetching */}
{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>
<div className="grid gap-4 sm:grid-cols-2">
{hardwareData.gpus.map((gpu, index) => {
const pciDevice = findPCIDeviceForGPU(gpu)
const fullSlot = pciDevice?.slot || gpu.slot
return (
<div
key={index}
onClick={() => handleGPUClick(gpu)}
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="mb-3 flex items-center justify-between">
<span className="font-medium text-sm">{gpu.name}</span>
<Badge className={getDeviceTypeColor("graphics")}>{gpu.vendor}</Badge>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Type</span>
<span className="font-medium">{gpu.type}</span>
</div>
{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>
)}
{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>
)}
{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>
)}
</div>
</div>
)
})}
</div>
</Card>
)}
{/* GPU Detail Modal - Shows immediately with basic info, then loads real-time data */}
<Dialog
open={selectedGPU !== null}
onOpenChange={() => {
setSelectedGPU(null)
setRealtimeGPUData(null)
}}
>
<DialogContent className="max-w-3xl max-h-[85vh] overflow-y-auto">
{selectedGPU && (
<>
<DialogHeader className="pb-4 border-b border-border">
<DialogTitle>{selectedGPU.name}</DialogTitle>
<DialogDescription>GPU Real-Time Monitoring</DialogDescription>
</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>
{/* 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 && (
<Badge className="bg-green-500/10 text-green-500 border-green-500/20 text-xs px-1.5 py-0.5 shrink-0">
Monitoring Available
</Badge>
)}
</div>
</div>
)}
</div>
</div>
{/* Real-Time Data */}
{realtimeGPUData && hasRealtimeData() && (
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
Real-Time Data
</h3>
<div className="grid gap-2">
{realtimeGPUData.data.map((data, index) => (
<div key={index} className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">{data.name}</span>
<span className="font-mono text-sm">{data.value}</span>
</div>
))}
</div>
</div>
)}
{/* Monitoring Tool Recommendation */}
{!hasRealtimeData() && (
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
Monitoring Tool Recommendation
</h3>
<p className="text-sm text-muted-foreground">
{getMonitoringToolRecommendation(selectedGPU.vendor)}
</p>
</div>
)}
</div>
</>
)}
</DialogContent>
</Dialog>
</div>
)
}