Files
ProxMenux/AppImage/components/hardware.tsx

833 lines
36 KiB
TypeScript
Raw Normal View History

2025-10-06 22:23:56 +02:00
"use client"
import { Card } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
2025-10-23 14:58:46 +02:00
import { Thermometer, CpuIcon, HardDrive, Cpu, MemoryStick, Cpu as Gpu } 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-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() || ""
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
}
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-23 14:58:46 +02:00
{/* 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>
)}
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-14 17:23:59 +02:00
{(() => {
const groupedTemps = groupAndSortTemperatures(hardwareData.temperatures)
return (
2025-10-14 17:38:26 +02:00
<div className="grid gap-6 md:grid-cols-2">
2025-10-14 17:23:59 +02:00
{/* CPU Sensors */}
{groupedTemps.CPU.length > 0 && (
2025-10-14 17:38:26 +02:00
<div className="md:col-span-2">
2025-10-14 17:23:59 +02:00
<div className="mb-3 flex items-center gap-2">
<CpuIcon className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-semibold">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>
2025-10-14 15:34:19 +02:00
</div>
2025-10-14 17:23:59 +02:00
)}
{/* GPU Sensors */}
{groupedTemps.GPU.length > 0 && (
2025-10-14 17:38:26 +02:00
<div className={groupedTemps.GPU.length > 1 ? "md:col-span-2" : ""}>
2025-10-14 17:23:59 +02:00
<div className="mb-3 flex items-center gap-2">
<Gpu className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-semibold">GPU</h3>
<Badge variant="outline" className="text-xs">
{groupedTemps.GPU.length}
</Badge>
</div>
2025-10-14 17:38:26 +02:00
<div className={`grid gap-4 ${groupedTemps.GPU.length > 1 ? "md:grid-cols-2" : ""}`}>
2025-10-14 17:23:59 +02:00
{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>
2025-10-14 15:34:19 +02:00
</div>
2025-10-14 17:23:59 +02:00
)}
{/* NVME Sensors */}
{groupedTemps.NVME.length > 0 && (
2025-10-14 17:38:26 +02:00
<div className={groupedTemps.NVME.length > 1 ? "md:col-span-2" : ""}>
2025-10-14 17:23:59 +02:00
<div className="mb-3 flex items-center gap-2">
<HardDrive className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-semibold">NVME</h3>
<Badge variant="outline" className="text-xs">
{groupedTemps.NVME.length}
</Badge>
</div>
2025-10-14 17:38:26 +02:00
<div className={`grid gap-4 ${groupedTemps.NVME.length > 1 ? "md:grid-cols-2" : ""}`}>
2025-10-14 17:23:59 +02:00
{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 && (
2025-10-14 17:38:26 +02:00
<div className={groupedTemps.PCI.length > 1 ? "md:col-span-2" : ""}>
2025-10-14 17:23:59 +02:00
<div className="mb-3 flex items-center gap-2">
<CpuIcon className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-semibold">PCI</h3>
<Badge variant="outline" className="text-xs">
{groupedTemps.PCI.length}
</Badge>
</div>
2025-10-14 17:38:26 +02:00
<div className={`grid gap-4 ${groupedTemps.PCI.length > 1 ? "md:grid-cols-2" : ""}`}>
2025-10-14 17:23:59 +02:00
{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 && (
2025-10-14 17:38:26 +02:00
<div className={groupedTemps.OTHER.length > 1 ? "md:col-span-2" : ""}>
2025-10-14 17:23:59 +02:00
<div className="mb-3 flex items-center gap-2">
<Thermometer className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-semibold">OTHER</h3>
<Badge variant="outline" className="text-xs">
{groupedTemps.OTHER.length}
</Badge>
</div>
2025-10-14 17:38:26 +02:00
<div className={`grid gap-4 ${groupedTemps.OTHER.length > 1 ? "md:grid-cols-2" : ""}`}>
2025-10-14 17:23:59 +02:00
{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>
)
})()}
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 14:58:46 +02:00
<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
2025-10-10 22:52:22 +02:00
</Badge>
)}
</div>
2025-10-08 12:06:24 +02:00
</div>
)}
</div>
</div>
2025-10-07 02:46:35 +02:00
2025-10-23 14:58:46 +02:00
{/* 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>
2025-10-09 23:14:47 +02:00
</div>
2025-10-23 14:58:46 +02:00
))}
2025-10-08 12:06:24 +02:00
</div>
</div>
)}
2025-10-06 22:23:56 +02:00
2025-10-23 14:58:46 +02:00
{/* 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>
2025-10-06 22:23:56 +02:00
</div>
2025-10-16 19:23:41 +02:00
)}
</div>
2025-10-23 14:58:46 +02:00
</>
2025-10-06 22:23:56 +02:00
)}
</DialogContent>
</Dialog>
2025-10-05 20:45:54 +02:00
</div>
)
}