mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-10-11 04:16:17 +00:00
Update AppImage
This commit is contained in:
@@ -3,11 +3,10 @@
|
|||||||
import { Card } from "@/components/ui/card"
|
import { Card } from "@/components/ui/card"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Progress } from "@/components/ui/progress"
|
import { Progress } from "@/components/ui/progress"
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||||
import {
|
import {
|
||||||
Thermometer,
|
Thermometer,
|
||||||
CpuIcon,
|
CpuIcon,
|
||||||
ChevronDown,
|
|
||||||
ChevronUp,
|
|
||||||
Zap,
|
Zap,
|
||||||
HardDrive,
|
HardDrive,
|
||||||
Network,
|
Network,
|
||||||
@@ -17,10 +16,13 @@ import {
|
|||||||
Cpu,
|
Cpu,
|
||||||
MemoryStick,
|
MemoryStick,
|
||||||
Cpu as Gpu,
|
Cpu as Gpu,
|
||||||
|
Info,
|
||||||
|
Activity,
|
||||||
|
Gauge,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import useSWR from "swr"
|
import useSWR from "swr"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { type HardwareData, fetcher } from "../types/hardware"
|
import { type HardwareData, type GPU, type NetworkInterfaceDetails, type DiskDetails, fetcher } from "../types/hardware"
|
||||||
|
|
||||||
const getDeviceTypeColor = (type: string): string => {
|
const getDeviceTypeColor = (type: string): string => {
|
||||||
const lowerType = type.toLowerCase()
|
const lowerType = type.toLowerCase()
|
||||||
@@ -44,7 +46,17 @@ export default function Hardware() {
|
|||||||
refreshInterval: 5000,
|
refreshInterval: 5000,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [expandedPCIDevice, setExpandedPCIDevice] = useState<string | null>(null)
|
const [selectedGPU, setSelectedGPU] = useState<GPU | null>(null)
|
||||||
|
const [selectedNetworkInterface, setSelectedNetworkInterface] = useState<string | null>(null)
|
||||||
|
const [selectedDisk, setSelectedDisk] = useState<string | null>(null)
|
||||||
|
const [selectedPCIDevice, setSelectedPCIDevice] = useState<any | null>(null)
|
||||||
|
|
||||||
|
const { data: networkDetails } = useSWR<NetworkInterfaceDetails>(
|
||||||
|
selectedNetworkInterface ? `/api/hardware/network/${selectedNetworkInterface}` : null,
|
||||||
|
fetcher,
|
||||||
|
)
|
||||||
|
|
||||||
|
const { data: diskDetails } = useSWR<DiskDetails>(selectedDisk ? `/api/hardware/disk/${selectedDisk}` : null, fetcher)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 p-6">
|
<div className="space-y-6 p-6">
|
||||||
@@ -194,7 +206,7 @@ export default function Hardware() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Thermal Monitoring - Restored blue progress bars */}
|
{/* Thermal Monitoring */}
|
||||||
{hardwareData?.temperatures && hardwareData.temperatures.length > 0 && (
|
{hardwareData?.temperatures && hardwareData.temperatures.length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
@@ -235,7 +247,6 @@ export default function Hardware() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* GPU Information - New dedicated GPU section */}
|
|
||||||
{hardwareData?.gpus && hardwareData.gpus.length > 0 && (
|
{hardwareData?.gpus && hardwareData.gpus.length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
@@ -248,13 +259,29 @@ export default function Hardware() {
|
|||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
{hardwareData.gpus.map((gpu, index) => (
|
{hardwareData.gpus.map((gpu, index) => (
|
||||||
<div key={index} className="rounded-lg border border-border/30 bg-background/50 p-4">
|
<div
|
||||||
|
key={index}
|
||||||
|
onClick={() => setSelectedGPU(gpu)}
|
||||||
|
className="cursor-pointer rounded-lg border border-border/30 bg-background/50 p-4 transition-colors hover:bg-background/80"
|
||||||
|
>
|
||||||
<div className="mb-3 flex items-center justify-between">
|
<div className="mb-3 flex items-center justify-between">
|
||||||
<span className="font-medium">{gpu.name}</span>
|
<span className="font-medium text-sm">{gpu.name}</span>
|
||||||
<Badge className={getDeviceTypeColor("graphics")}>{gpu.vendor}</Badge>
|
<Badge className={getDeviceTypeColor("graphics")}>{gpu.vendor}</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<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>
|
||||||
|
|
||||||
|
{gpu.driver_version && (
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">Driver</span>
|
||||||
|
<span className="font-mono text-xs">{gpu.driver_version}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{gpu.memory_total && (
|
{gpu.memory_total && (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Memory</span>
|
<span className="text-muted-foreground">Memory</span>
|
||||||
@@ -290,27 +317,11 @@ export default function Hardware() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{gpu.power_draw && gpu.power_draw !== "N/A" && (
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-muted-foreground">Power Draw</span>
|
|
||||||
<span className="font-medium">{gpu.power_draw}</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{gpu.driver_version && (
|
<div className="mt-3 flex items-center gap-1 text-xs text-muted-foreground">
|
||||||
<div className="flex justify-between text-sm">
|
<Info className="h-3 w-3" />
|
||||||
<span className="text-muted-foreground">Driver</span>
|
<span>Click for detailed information</span>
|
||||||
<span className="font-mono text-xs">{gpu.driver_version}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{gpu.type && (
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-muted-foreground">Type</span>
|
|
||||||
<span className="font-medium">{gpu.type}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -318,7 +329,199 @@ export default function Hardware() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* PCI Devices */}
|
<Dialog open={selectedGPU !== null} onOpenChange={(open) => !open && setSelectedGPU(null)}>
|
||||||
|
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Gpu className="h-5 w-5" />
|
||||||
|
{selectedGPU?.name}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>Detailed GPU information and statistics</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{selectedGPU && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Basic Information */}
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold">
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
Basic Information
|
||||||
|
</h3>
|
||||||
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-3">
|
||||||
|
<span className="text-sm text-muted-foreground">Vendor</span>
|
||||||
|
<Badge className={getDeviceTypeColor("graphics")}>{selectedGPU.vendor}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-3">
|
||||||
|
<span className="text-sm text-muted-foreground">Type</span>
|
||||||
|
<span className="text-sm font-medium">{selectedGPU.type}</span>
|
||||||
|
</div>
|
||||||
|
{selectedGPU.slot && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-3">
|
||||||
|
<span className="text-sm text-muted-foreground">PCI Slot</span>
|
||||||
|
<span className="font-mono text-sm">{selectedGPU.slot}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedGPU.driver_version && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-3">
|
||||||
|
<span className="text-sm text-muted-foreground">Driver Version</span>
|
||||||
|
<span className="font-mono text-sm">{selectedGPU.driver_version}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedGPU.pcie_gen && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-3">
|
||||||
|
<span className="text-sm text-muted-foreground">PCIe Generation</span>
|
||||||
|
<span className="text-sm font-medium">Gen {selectedGPU.pcie_gen}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedGPU.pcie_width && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-3">
|
||||||
|
<span className="text-sm text-muted-foreground">PCIe Width</span>
|
||||||
|
<span className="text-sm font-medium">{selectedGPU.pcie_width}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Performance Metrics */}
|
||||||
|
{(selectedGPU.utilization !== undefined || selectedGPU.temperature !== undefined) && (
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold">
|
||||||
|
<Activity className="h-4 w-4" />
|
||||||
|
Performance Metrics
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{selectedGPU.utilization !== undefined && (
|
||||||
|
<div className="rounded-lg border border-border/30 bg-background/50 p-3">
|
||||||
|
<div className="mb-2 flex justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">GPU Utilization</span>
|
||||||
|
<span className="font-semibold">{selectedGPU.utilization}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={selectedGPU.utilization} className="h-2" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedGPU.memory_utilization !== undefined && (
|
||||||
|
<div className="rounded-lg border border-border/30 bg-background/50 p-3">
|
||||||
|
<div className="mb-2 flex justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">Memory Utilization</span>
|
||||||
|
<span className="font-semibold">{selectedGPU.memory_utilization}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={selectedGPU.memory_utilization} className="h-2" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedGPU.temperature !== undefined && selectedGPU.temperature > 0 && (
|
||||||
|
<div className="rounded-lg border border-border/30 bg-background/50 p-3">
|
||||||
|
<div className="mb-2 flex justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">Temperature</span>
|
||||||
|
<span className="font-semibold text-green-500">{selectedGPU.temperature}°C</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={(selectedGPU.temperature / 100) * 100} className="h-2" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Memory Information */}
|
||||||
|
{selectedGPU.memory_total && (
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold">
|
||||||
|
<MemoryStick className="h-4 w-4" />
|
||||||
|
Memory Information
|
||||||
|
</h3>
|
||||||
|
<div className="grid gap-3 md:grid-cols-3">
|
||||||
|
<div className="rounded-lg border border-border/30 bg-background/50 p-3">
|
||||||
|
<p className="text-xs text-muted-foreground">Total</p>
|
||||||
|
<p className="text-lg font-semibold">{selectedGPU.memory_total}</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border border-border/30 bg-background/50 p-3">
|
||||||
|
<p className="text-xs text-muted-foreground">Used</p>
|
||||||
|
<p className="text-lg font-semibold">{selectedGPU.memory_used}</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border border-border/30 bg-background/50 p-3">
|
||||||
|
<p className="text-xs text-muted-foreground">Free</p>
|
||||||
|
<p className="text-lg font-semibold">{selectedGPU.memory_free}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Clock Speeds */}
|
||||||
|
{(selectedGPU.clock_graphics || selectedGPU.clock_memory) && (
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold">
|
||||||
|
<Gauge className="h-4 w-4" />
|
||||||
|
Clock Speeds
|
||||||
|
</h3>
|
||||||
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
|
{selectedGPU.clock_graphics && (
|
||||||
|
<div className="rounded-lg border border-border/30 bg-background/50 p-3">
|
||||||
|
<p className="text-xs text-muted-foreground">Graphics Clock</p>
|
||||||
|
<p className="text-lg font-semibold">{selectedGPU.clock_graphics}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedGPU.clock_memory && (
|
||||||
|
<div className="rounded-lg border border-border/30 bg-background/50 p-3">
|
||||||
|
<p className="text-xs text-muted-foreground">Memory Clock</p>
|
||||||
|
<p className="text-lg font-semibold">{selectedGPU.clock_memory}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Power Information */}
|
||||||
|
{(selectedGPU.power_draw || selectedGPU.power_limit) && (
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold">
|
||||||
|
<Zap className="h-4 w-4" />
|
||||||
|
Power Information
|
||||||
|
</h3>
|
||||||
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
|
{selectedGPU.power_draw && selectedGPU.power_draw !== "N/A" && (
|
||||||
|
<div className="rounded-lg border border-border/30 bg-background/50 p-3">
|
||||||
|
<p className="text-xs text-muted-foreground">Current Draw</p>
|
||||||
|
<p className="text-lg font-semibold text-yellow-500">{selectedGPU.power_draw}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedGPU.power_limit && selectedGPU.power_limit !== "N/A" && (
|
||||||
|
<div className="rounded-lg border border-border/30 bg-background/50 p-3">
|
||||||
|
<p className="text-xs text-muted-foreground">Power Limit</p>
|
||||||
|
<p className="text-lg font-semibold">{selectedGPU.power_limit}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Running Processes */}
|
||||||
|
{selectedGPU.processes && selectedGPU.processes.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold">
|
||||||
|
<CpuIcon className="h-4 w-4" />
|
||||||
|
Running Processes
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{selectedGPU.processes.map((process, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="flex items-center justify-between rounded-lg border border-border/30 bg-background/50 p-3"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">{process.name}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">PID: {process.pid}</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline">{process.memory}</Badge>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{hardwareData?.pci_devices && hardwareData.pci_devices.length > 0 && (
|
{hardwareData?.pci_devices && hardwareData.pci_devices.length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
@@ -329,82 +532,85 @@ export default function Hardware() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
{hardwareData.pci_devices.map((device, index) => {
|
{hardwareData.pci_devices.map((device, index) => (
|
||||||
const deviceKey = `${device.slot}-${index}`
|
|
||||||
const isExpanded = expandedPCIDevice === deviceKey
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={index} className="rounded-lg border border-border/30 bg-background/50">
|
|
||||||
<div
|
<div
|
||||||
onClick={() => setExpandedPCIDevice(isExpanded ? null : deviceKey)}
|
key={index}
|
||||||
className="flex cursor-pointer items-start justify-between p-4 transition-colors hover:bg-background/80"
|
onClick={() => setSelectedPCIDevice(device)}
|
||||||
|
className="cursor-pointer rounded-lg border border-border/30 bg-background/50 p-4 transition-colors hover:bg-background/80"
|
||||||
>
|
>
|
||||||
<div className="flex-1 space-y-1">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Badge className={getDeviceTypeColor(device.type)}>{device.type}</Badge>
|
<Badge className={getDeviceTypeColor(device.type)}>{device.type}</Badge>
|
||||||
<span className="font-mono text-xs text-muted-foreground">{device.slot}</span>
|
<span className="font-mono text-xs text-muted-foreground">{device.slot}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-medium text-sm">{device.device}</p>
|
<p className="font-medium text-sm mb-1">{device.device}</p>
|
||||||
<p className="text-xs text-muted-foreground">{device.vendor}</p>
|
<p className="text-xs text-muted-foreground">{device.vendor}</p>
|
||||||
|
<div className="mt-2 flex items-center gap-1 text-xs text-muted-foreground">
|
||||||
|
<Info className="h-3 w-3" />
|
||||||
|
<span>Click for details</span>
|
||||||
</div>
|
</div>
|
||||||
{isExpanded ? (
|
|
||||||
<ChevronUp className="h-5 w-5 text-muted-foreground" />
|
|
||||||
) : (
|
|
||||||
<ChevronDown className="h-5 w-5 text-muted-foreground" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
{isExpanded && (
|
|
||||||
<div className="border-t border-border/30 p-4 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(device.type)}>{device.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">{device.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">{device.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">{device.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">{device.class}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{device.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">{device.driver}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{device.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">{device.kernel_module}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Power Consumption - Only show if data exists */}
|
<Dialog open={selectedPCIDevice !== null} onOpenChange={(open) => !open && setSelectedPCIDevice(null)}>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<CpuIcon className="h-5 w-5" />
|
||||||
|
{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 && (
|
{hardwareData?.power_meter && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
@@ -429,7 +635,6 @@ export default function Hardware() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Fans - Only show if data exists */}
|
|
||||||
{hardwareData?.fans && hardwareData.fans.length > 0 && (
|
{hardwareData?.fans && hardwareData.fans.length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
@@ -448,7 +653,12 @@ export default function Hardware() {
|
|||||||
return (
|
return (
|
||||||
<div key={index} className="space-y-2">
|
<div key={index} className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm font-medium">{fan.name}</span>
|
<span className="text-sm font-medium">{fan.name}</span>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{fan.type}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
<span className="text-sm font-semibold text-blue-500">
|
<span className="text-sm font-semibold text-blue-500">
|
||||||
{fan.speed.toFixed(0)} {fan.unit}
|
{fan.speed.toFixed(0)} {fan.unit}
|
||||||
</span>
|
</span>
|
||||||
@@ -456,6 +666,7 @@ export default function Hardware() {
|
|||||||
<div className="h-2 w-full overflow-hidden rounded-full bg-secondary">
|
<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 className="h-full bg-blue-500 transition-all" style={{ width: `${percentage}%` }} />
|
||||||
</div>
|
</div>
|
||||||
|
{fan.adapter && <span className="text-xs text-muted-foreground">{fan.adapter}</span>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -463,7 +674,7 @@ export default function Hardware() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Power Supplies - Only show if data exists */}
|
{/* Power Supplies */}
|
||||||
{hardwareData?.power_supplies && hardwareData.power_supplies.length > 0 && (
|
{hardwareData?.power_supplies && hardwareData.power_supplies.length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
@@ -491,7 +702,7 @@ export default function Hardware() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* UPS - Only show if data exists and has content */}
|
{/* UPS */}
|
||||||
{hardwareData?.ups && Object.keys(hardwareData.ups).length > 0 && hardwareData.ups.model && (
|
{hardwareData?.ups && Object.keys(hardwareData.ups).length > 0 && hardwareData.ups.model && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
@@ -551,7 +762,6 @@ export default function Hardware() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Network Summary */}
|
|
||||||
{hardwareData?.pci_devices &&
|
{hardwareData?.pci_devices &&
|
||||||
hardwareData.pci_devices.filter((d) => d.type.toLowerCase().includes("network")).length > 0 && (
|
hardwareData.pci_devices.filter((d) => d.type.toLowerCase().includes("network")).length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
@@ -567,22 +777,168 @@ export default function Hardware() {
|
|||||||
{hardwareData.pci_devices
|
{hardwareData.pci_devices
|
||||||
.filter((d) => d.type.toLowerCase().includes("network"))
|
.filter((d) => d.type.toLowerCase().includes("network"))
|
||||||
.map((device, index) => (
|
.map((device, index) => (
|
||||||
<div key={index} className="rounded-lg border border-border/30 bg-background/50 p-3">
|
<div
|
||||||
|
key={index}
|
||||||
|
onClick={() => setSelectedNetworkInterface(device.device.split(" ")[0].toLowerCase())}
|
||||||
|
className="cursor-pointer rounded-lg border border-border/30 bg-background/50 p-3 transition-colors hover:bg-background/80"
|
||||||
|
>
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<span className="text-sm font-medium truncate">{device.device}</span>
|
<span className="text-sm font-medium truncate">{device.device}</span>
|
||||||
<Badge className="bg-blue-500/10 text-blue-500 border-blue-500/20 text-xs">Ethernet</Badge>
|
<Badge className="bg-blue-500/10 text-blue-500 border-blue-500/20 text-xs">Ethernet</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground truncate">{device.vendor}</p>
|
<p className="text-xs text-muted-foreground truncate">{device.vendor}</p>
|
||||||
|
<div className="mt-2 flex items-center gap-1 text-xs text-muted-foreground">
|
||||||
|
<Info className="h-3 w-3" />
|
||||||
|
<span>Click for details</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-4 text-xs text-muted-foreground">
|
|
||||||
For detailed network information, see the Network section
|
|
||||||
</p>
|
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Storage Summary - Larger badges with blue color */}
|
<Dialog
|
||||||
|
open={selectedNetworkInterface !== null}
|
||||||
|
onOpenChange={(open) => !open && setSelectedNetworkInterface(null)}
|
||||||
|
>
|
||||||
|
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Network className="h-5 w-5" />
|
||||||
|
Network Interface: {selectedNetworkInterface}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>Detailed network interface information</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{networkDetails && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Driver Information */}
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-2 text-sm font-semibold">Driver Information</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{networkDetails.driver && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Driver</span>
|
||||||
|
<span className="font-mono text-sm">{networkDetails.driver}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{networkDetails.driver_version && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Version</span>
|
||||||
|
<span className="font-mono text-sm">{networkDetails.driver_version}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{networkDetails.firmware_version && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Firmware</span>
|
||||||
|
<span className="font-mono text-sm">{networkDetails.firmware_version}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{networkDetails.bus_info && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Bus Info</span>
|
||||||
|
<span className="font-mono text-sm">{networkDetails.bus_info}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Connection Status */}
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-2 text-sm font-semibold">Connection Status</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{networkDetails.link_detected && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Link</span>
|
||||||
|
<Badge variant={networkDetails.link_detected === "yes" ? "default" : "destructive"}>
|
||||||
|
{networkDetails.link_detected}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{networkDetails.speed && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Speed</span>
|
||||||
|
<span className="text-sm font-medium">{networkDetails.speed}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{networkDetails.duplex && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Duplex</span>
|
||||||
|
<span className="text-sm font-medium">{networkDetails.duplex}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{networkDetails.mtu && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<span className="text-sm text-muted-foreground">MTU</span>
|
||||||
|
<span className="text-sm font-medium">{networkDetails.mtu}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Addresses */}
|
||||||
|
{(networkDetails.mac_address ||
|
||||||
|
(networkDetails.ip_addresses && networkDetails.ip_addresses.length > 0)) && (
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-2 text-sm font-semibold">Addresses</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{networkDetails.mac_address && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<span className="text-sm text-muted-foreground">MAC Address</span>
|
||||||
|
<span className="font-mono text-sm">{networkDetails.mac_address}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{networkDetails.ip_addresses &&
|
||||||
|
networkDetails.ip_addresses.map((ip, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-2"
|
||||||
|
>
|
||||||
|
<span className="text-sm text-muted-foreground">{ip.type}</span>
|
||||||
|
<span className="font-mono text-sm">{ip.address}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Statistics */}
|
||||||
|
{networkDetails.statistics && Object.keys(networkDetails.statistics).length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-2 text-sm font-semibold">Statistics</h3>
|
||||||
|
<div className="grid gap-2 md:grid-cols-2">
|
||||||
|
{networkDetails.statistics.rx_bytes && (
|
||||||
|
<div className="rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<p className="text-xs text-muted-foreground">RX Bytes</p>
|
||||||
|
<p className="text-sm font-medium">{networkDetails.statistics.rx_bytes}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{networkDetails.statistics.rx_packets && (
|
||||||
|
<div className="rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<p className="text-xs text-muted-foreground">RX Packets</p>
|
||||||
|
<p className="text-sm font-medium">{networkDetails.statistics.rx_packets}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{networkDetails.statistics.tx_bytes && (
|
||||||
|
<div className="rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<p className="text-xs text-muted-foreground">TX Bytes</p>
|
||||||
|
<p className="text-sm font-medium">{networkDetails.statistics.tx_bytes}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{networkDetails.statistics.tx_packets && (
|
||||||
|
<div className="rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<p className="text-xs text-muted-foreground">TX Packets</p>
|
||||||
|
<p className="text-sm font-medium">{networkDetails.statistics.tx_packets}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{hardwareData?.storage_devices && hardwareData.storage_devices.length > 0 && (
|
{hardwareData?.storage_devices && hardwareData.storage_devices.length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
@@ -595,18 +951,180 @@ export default function Hardware() {
|
|||||||
|
|
||||||
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{hardwareData.storage_devices.map((device, index) => (
|
{hardwareData.storage_devices.map((device, index) => (
|
||||||
<div key={index} className="rounded-lg border border-border/30 bg-background/50 p-3">
|
<div
|
||||||
|
key={index}
|
||||||
|
onClick={() => setSelectedDisk(device.name)}
|
||||||
|
className="cursor-pointer rounded-lg border border-border/30 bg-background/50 p-3 transition-colors hover:bg-background/80"
|
||||||
|
>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="text-sm font-medium">{device.name}</span>
|
<span className="text-sm font-medium">{device.name}</span>
|
||||||
<Badge className="bg-blue-500/10 text-blue-500 border-blue-500/20 px-2.5 py-0.5">{device.type}</Badge>
|
<Badge className="bg-blue-500/10 text-blue-500 border-blue-500/20 px-2.5 py-0.5">{device.type}</Badge>
|
||||||
</div>
|
</div>
|
||||||
{device.size && <p className="text-sm font-medium">{device.size}</p>}
|
{device.size && <p className="text-sm font-medium">{device.size}</p>}
|
||||||
{device.model && <p className="text-xs text-muted-foreground truncate">{device.model}</p>}
|
{device.model && <p className="text-xs text-muted-foreground truncate">{device.model}</p>}
|
||||||
|
<div className="mt-2 flex items-center gap-1 text-xs text-muted-foreground">
|
||||||
|
<Info className="h-3 w-3" />
|
||||||
|
<span>Click for details</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Dialog open={selectedDisk !== null} onOpenChange={(open) => !open && setSelectedDisk(null)}>
|
||||||
|
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<HardDrive className="h-5 w-5" />
|
||||||
|
Disk: {selectedDisk}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>Detailed disk information</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{diskDetails && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Basic Information */}
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-2 text-sm font-semibold">Basic Information</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{diskDetails.type && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Type</span>
|
||||||
|
<Badge className="bg-blue-500/10 text-blue-500 border-blue-500/20">{diskDetails.type}</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{diskDetails.driver && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Driver</span>
|
||||||
|
<span className="font-mono text-sm">{diskDetails.driver}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{diskDetails.model && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Model</span>
|
||||||
|
<span className="text-sm">{diskDetails.model}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{diskDetails.serial && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Serial</span>
|
||||||
|
<span className="font-mono text-sm">{diskDetails.serial}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{diskDetails.size && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Size</span>
|
||||||
|
<span className="text-sm font-medium">{diskDetails.size}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Technical Details */}
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-2 text-sm font-semibold">Technical Details</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{diskDetails.rotational !== undefined && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Rotational</span>
|
||||||
|
<Badge variant={diskDetails.rotational ? "default" : "outline"}>
|
||||||
|
{diskDetails.rotational ? "Yes (HDD)" : "No (SSD)"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{diskDetails.block_size && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Block Size</span>
|
||||||
|
<span className="text-sm">{diskDetails.block_size} bytes</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{diskDetails.scheduler && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Scheduler</span>
|
||||||
|
<span className="text-sm">{diskDetails.scheduler}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{diskDetails.removable !== undefined && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Removable</span>
|
||||||
|
<Badge variant={diskDetails.removable ? "default" : "outline"}>
|
||||||
|
{diskDetails.removable ? "Yes" : "No"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{diskDetails.read_only !== undefined && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Read Only</span>
|
||||||
|
<Badge variant={diskDetails.read_only ? "destructive" : "default"}>
|
||||||
|
{diskDetails.read_only ? "Yes" : "No"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* SMART Information */}
|
||||||
|
{diskDetails.smart_available && (
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-2 text-sm font-semibold">SMART Information</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<span className="text-sm text-muted-foreground">SMART Enabled</span>
|
||||||
|
<Badge variant={diskDetails.smart_enabled ? "default" : "outline"}>
|
||||||
|
{diskDetails.smart_enabled ? "Yes" : "No"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
{diskDetails.smart_health && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Health Status</span>
|
||||||
|
<Badge variant={diskDetails.smart_health === "PASSED" ? "default" : "destructive"}>
|
||||||
|
{diskDetails.smart_health}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{diskDetails.temperature !== undefined && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Temperature</span>
|
||||||
|
<span className="text-sm font-semibold text-green-500">{diskDetails.temperature}°C</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{diskDetails.power_on_hours !== undefined && (
|
||||||
|
<div className="flex justify-between rounded-lg border border-border/30 bg-background/50 p-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Power On Hours</span>
|
||||||
|
<span className="text-sm font-medium">{diskDetails.power_on_hours.toLocaleString()} hours</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Partitions */}
|
||||||
|
{diskDetails.partitions && diskDetails.partitions.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-2 text-sm font-semibold">Partitions</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{diskDetails.partitions.map((partition, idx) => (
|
||||||
|
<div key={idx} className="rounded-lg border border-border/30 bg-background/50 p-3">
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<span className="font-mono text-sm font-medium">{partition.name}</span>
|
||||||
|
{partition.size && <span className="text-sm text-muted-foreground">{partition.size}</span>}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
{partition.fstype && <Badge variant="outline">{partition.fstype}</Badge>}
|
||||||
|
{partition.mountpoint && (
|
||||||
|
<span className="text-muted-foreground">→ {partition.mountpoint}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -1293,240 +1293,23 @@ def get_proxmox_vms():
|
|||||||
'vms': []
|
'vms': []
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_ipmi_fans():
|
# --- START OF UPDATED CODE ---
|
||||||
"""Get fan information from IPMI"""
|
|
||||||
fans = []
|
|
||||||
try:
|
|
||||||
result = subprocess.run(['ipmitool', 'sensor'], capture_output=True, text=True, timeout=10)
|
|
||||||
if result.returncode == 0:
|
|
||||||
for line in result.stdout.split('\n'):
|
|
||||||
if 'fan' in line.lower() and '|' in line:
|
|
||||||
parts = [p.strip() for p in line.split('|')]
|
|
||||||
if len(parts) >= 3:
|
|
||||||
name = parts[0]
|
|
||||||
value_str = parts[1]
|
|
||||||
unit = parts[2] if len(parts) > 2 else ''
|
|
||||||
|
|
||||||
# Skip "DutyCycle" and "Presence" entries
|
def get_gpu_detailed_info():
|
||||||
if 'dutycycle' in name.lower() or 'presence' in name.lower():
|
"""Get comprehensive GPU information using nvidia-smi, intel_gpu_top, and radeontop"""
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
value = float(value_str)
|
|
||||||
fans.append({
|
|
||||||
'name': name,
|
|
||||||
'speed': value,
|
|
||||||
'unit': unit
|
|
||||||
})
|
|
||||||
print(f"[v0] IPMI Fan: {name} = {value} {unit}")
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
print(f"[v0] Found {len(fans)} IPMI fans")
|
|
||||||
except FileNotFoundError:
|
|
||||||
print("[v0] ipmitool not found")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[v0] Error getting IPMI fans: {e}")
|
|
||||||
|
|
||||||
return fans
|
|
||||||
|
|
||||||
def get_ipmi_power():
|
|
||||||
"""Get power supply information from IPMI"""
|
|
||||||
power_supplies = []
|
|
||||||
power_meter = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = subprocess.run(['ipmitool', 'sensor'], capture_output=True, text=True, timeout=10)
|
|
||||||
if result.returncode == 0:
|
|
||||||
for line in result.stdout.split('\n'):
|
|
||||||
if ('power supply' in line.lower() or 'power meter' in line.lower()) and '|' in line:
|
|
||||||
parts = [p.strip() for p in line.split('|')]
|
|
||||||
if len(parts) >= 3:
|
|
||||||
name = parts[0]
|
|
||||||
value_str = parts[1]
|
|
||||||
unit = parts[2] if len(parts) > 2 else ''
|
|
||||||
|
|
||||||
try:
|
|
||||||
value = float(value_str)
|
|
||||||
|
|
||||||
if 'power meter' in name.lower():
|
|
||||||
power_meter = {
|
|
||||||
'name': name,
|
|
||||||
'watts': value,
|
|
||||||
'unit': unit
|
|
||||||
}
|
|
||||||
print(f"[v0] IPMI Power Meter: {value} {unit}")
|
|
||||||
else:
|
|
||||||
power_supplies.append({
|
|
||||||
'name': name,
|
|
||||||
'watts': value,
|
|
||||||
'unit': unit,
|
|
||||||
'status': 'ok' if value > 0 else 'off'
|
|
||||||
})
|
|
||||||
print(f"[v0] IPMI PSU: {name} = {value} {unit}")
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
print(f"[v0] Found {len(power_supplies)} IPMI power supplies")
|
|
||||||
except FileNotFoundError:
|
|
||||||
print("[v0] ipmitool not found")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[v0] Error getting IPMI power: {e}")
|
|
||||||
|
|
||||||
return {
|
|
||||||
'power_supplies': power_supplies,
|
|
||||||
'power_meter': power_meter
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_ups_info():
|
|
||||||
"""Get UPS information from NUT (upsc)"""
|
|
||||||
ups_data = {}
|
|
||||||
|
|
||||||
try:
|
|
||||||
# First, list available UPS devices
|
|
||||||
result = subprocess.run(['upsc', '-l'], capture_output=True, text=True, timeout=5)
|
|
||||||
if result.returncode == 0:
|
|
||||||
ups_list = result.stdout.strip().split('\n')
|
|
||||||
if ups_list and ups_list[0]:
|
|
||||||
ups_name = ups_list[0]
|
|
||||||
print(f"[v0] Found UPS: {ups_name}")
|
|
||||||
|
|
||||||
# Get detailed UPS info
|
|
||||||
result = subprocess.run(['upsc', ups_name], capture_output=True, text=True, timeout=5)
|
|
||||||
if result.returncode == 0:
|
|
||||||
for line in result.stdout.split('\n'):
|
|
||||||
if ':' in line:
|
|
||||||
key, value = line.split(':', 1)
|
|
||||||
key = key.strip()
|
|
||||||
value = value.strip()
|
|
||||||
|
|
||||||
# Map common UPS variables
|
|
||||||
if key == 'device.model':
|
|
||||||
ups_data['model'] = value
|
|
||||||
elif key == 'ups.status':
|
|
||||||
ups_data['status'] = value
|
|
||||||
elif key == 'battery.charge':
|
|
||||||
ups_data['battery_charge'] = f"{value}%"
|
|
||||||
elif key == 'battery.runtime':
|
|
||||||
# Convert seconds to minutes
|
|
||||||
try:
|
|
||||||
runtime_sec = int(value)
|
|
||||||
runtime_min = runtime_sec // 60
|
|
||||||
ups_data['time_left'] = f"{runtime_min} minutes"
|
|
||||||
except ValueError:
|
|
||||||
ups_data['time_left'] = value
|
|
||||||
elif key == 'ups.load':
|
|
||||||
ups_data['load_percent'] = f"{value}%"
|
|
||||||
elif key == 'input.voltage':
|
|
||||||
ups_data['line_voltage'] = f"{value}V"
|
|
||||||
elif key == 'ups.realpower':
|
|
||||||
ups_data['real_power'] = f"{value}W"
|
|
||||||
|
|
||||||
print(f"[v0] UPS data: {ups_data}")
|
|
||||||
except FileNotFoundError:
|
|
||||||
print("[v0] upsc not found")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[v0] Error getting UPS info: {e}")
|
|
||||||
|
|
||||||
return ups_data
|
|
||||||
|
|
||||||
def get_temperature_info():
|
|
||||||
"""Get detailed temperature information from sensors command"""
|
|
||||||
temperatures = []
|
|
||||||
power_meter = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = subprocess.run(['sensors'], capture_output=True, text=True, timeout=5)
|
|
||||||
if result.returncode == 0:
|
|
||||||
current_adapter = None
|
|
||||||
current_sensor = None
|
|
||||||
|
|
||||||
for line in result.stdout.split('\n'):
|
|
||||||
line = line.strip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Detect adapter line
|
|
||||||
if line.startswith('Adapter:'):
|
|
||||||
current_adapter = line.replace('Adapter:', '').strip()
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Detect sensor name (lines without ':' at the start are sensor names)
|
|
||||||
if ':' in line and not line.startswith(' '):
|
|
||||||
parts = line.split(':', 1)
|
|
||||||
sensor_name = parts[0].strip()
|
|
||||||
value_part = parts[1].strip()
|
|
||||||
|
|
||||||
if 'power' in sensor_name.lower() and 'W' in value_part:
|
|
||||||
try:
|
|
||||||
# Extract power value (e.g., "182.00 W" -> 182.00)
|
|
||||||
power_match = re.search(r'([\d.]+)\s*W', value_part)
|
|
||||||
if power_match:
|
|
||||||
power_value = float(power_match.group(1))
|
|
||||||
power_meter = {
|
|
||||||
'name': sensor_name,
|
|
||||||
'watts': power_value,
|
|
||||||
'adapter': current_adapter
|
|
||||||
}
|
|
||||||
print(f"[v0] Power meter sensor: {sensor_name} = {power_value}W")
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Parse temperature sensors
|
|
||||||
elif '°C' in value_part or 'C' in value_part:
|
|
||||||
try:
|
|
||||||
# Extract temperature value
|
|
||||||
temp_match = re.search(r'([+-]?[\d.]+)\s*°?C', value_part)
|
|
||||||
if temp_match:
|
|
||||||
temp_value = float(temp_match.group(1))
|
|
||||||
|
|
||||||
# Extract high and critical values if present
|
|
||||||
high_match = re.search(r'high\s*=\s*([+-]?[\d.]+)', value_part)
|
|
||||||
crit_match = re.search(r'crit\s*=\s*([+-]?[\d.]+)', value_part)
|
|
||||||
|
|
||||||
high_value = float(high_match.group(1)) if high_match else 0
|
|
||||||
crit_value = float(crit_match.group(1)) if crit_match else 0
|
|
||||||
|
|
||||||
temperatures.append({
|
|
||||||
'name': sensor_name,
|
|
||||||
'current': temp_value,
|
|
||||||
'high': high_value,
|
|
||||||
'critical': crit_value,
|
|
||||||
'adapter': current_adapter
|
|
||||||
})
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
print(f"[v0] Found {len(temperatures)} temperature sensors")
|
|
||||||
if power_meter:
|
|
||||||
print(f"[v0] Found power meter: {power_meter['watts']}W")
|
|
||||||
|
|
||||||
except FileNotFoundError:
|
|
||||||
print("[v0] sensors command not found")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[v0] Error getting temperature info: {e}")
|
|
||||||
|
|
||||||
return {
|
|
||||||
'temperatures': temperatures,
|
|
||||||
'power_meter': power_meter
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_gpu_info():
|
|
||||||
"""Get GPU information from lspci and enrich with temperature/fan data from sensors"""
|
|
||||||
gpus = []
|
gpus = []
|
||||||
|
|
||||||
|
# Get basic GPU info from lspci
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(['lspci'], capture_output=True, text=True, timeout=5)
|
result = subprocess.run(['lspci'], capture_output=True, text=True, timeout=5)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
for line in result.stdout.split('\n'):
|
for line in result.stdout.split('\n'):
|
||||||
# Match VGA, 3D, Display controllers
|
|
||||||
if any(keyword in line for keyword in ['VGA compatible controller', '3D controller', 'Display controller']):
|
if any(keyword in line for keyword in ['VGA compatible controller', '3D controller', 'Display controller']):
|
||||||
parts = line.split(':', 2)
|
parts = line.split(':', 2)
|
||||||
if len(parts) >= 3:
|
if len(parts) >= 3:
|
||||||
slot = parts[0].strip()
|
slot = parts[0].strip()
|
||||||
gpu_name = parts[2].strip()
|
gpu_name = parts[2].strip()
|
||||||
|
|
||||||
# Determine vendor
|
|
||||||
vendor = 'Unknown'
|
vendor = 'Unknown'
|
||||||
if 'NVIDIA' in gpu_name or 'nVidia' in gpu_name:
|
if 'NVIDIA' in gpu_name or 'nVidia' in gpu_name:
|
||||||
vendor = 'NVIDIA'
|
vendor = 'NVIDIA'
|
||||||
@@ -1539,7 +1322,8 @@ def get_gpu_info():
|
|||||||
'slot': slot,
|
'slot': slot,
|
||||||
'name': gpu_name,
|
'name': gpu_name,
|
||||||
'vendor': vendor,
|
'vendor': vendor,
|
||||||
'type': 'Discrete' if vendor in ['NVIDIA', 'AMD'] else 'Integrated'
|
'type': 'Discrete' if vendor in ['NVIDIA', 'AMD'] else 'Integrated',
|
||||||
|
'processes': []
|
||||||
}
|
}
|
||||||
|
|
||||||
gpus.append(gpu)
|
gpus.append(gpu)
|
||||||
@@ -1547,6 +1331,99 @@ def get_gpu_info():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[v0] Error detecting GPUs from lspci: {e}")
|
print(f"[v0] Error detecting GPUs from lspci: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get comprehensive NVIDIA GPU info
|
||||||
|
result = subprocess.run(
|
||||||
|
['nvidia-smi', '--query-gpu=index,name,memory.total,memory.used,memory.free,temperature.gpu,power.draw,power.limit,utilization.gpu,utilization.memory,clocks.gr,clocks.mem,driver_version,pcie.link.gen.current,pcie.link.width.current',
|
||||||
|
'--format=csv,noheader,nounits'],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
for line in result.stdout.strip().split('\n'):
|
||||||
|
if line:
|
||||||
|
parts = [p.strip() for p in line.split(',')]
|
||||||
|
if len(parts) >= 15:
|
||||||
|
nvidia_info = {
|
||||||
|
'index': int(parts[0]),
|
||||||
|
'memory_total': f"{parts[2]} MB",
|
||||||
|
'memory_used': f"{parts[3]} MB",
|
||||||
|
'memory_free': f"{parts[4]} MB",
|
||||||
|
'temperature': int(float(parts[5])) if parts[5] != '[N/A]' else 0,
|
||||||
|
'power_draw': f"{parts[6]} W" if parts[6] != '[N/A]' else 'N/A',
|
||||||
|
'power_limit': f"{parts[7]} W" if parts[7] != '[N/A]' else 'N/A',
|
||||||
|
'utilization': int(float(parts[8])) if parts[8] != '[N/A]' else 0,
|
||||||
|
'memory_utilization': int(float(parts[9])) if parts[9] != '[N/A]' else 0,
|
||||||
|
'clock_graphics': f"{parts[10]} MHz" if parts[10] != '[N/A]' else 'N/A',
|
||||||
|
'clock_memory': f"{parts[11]} MHz" if parts[11] != '[N/A]' else 'N/A',
|
||||||
|
'driver_version': parts[12],
|
||||||
|
'pcie_gen': parts[13],
|
||||||
|
'pcie_width': f"x{parts[14]}" if parts[14] != '[N/A]' else 'N/A'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Match with existing GPU
|
||||||
|
for gpu in gpus:
|
||||||
|
if gpu['vendor'] == 'NVIDIA':
|
||||||
|
gpu.update(nvidia_info)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Get NVIDIA GPU processes
|
||||||
|
result = subprocess.run(
|
||||||
|
['nvidia-smi', '--query-compute-apps=pid,process_name,used_memory', '--format=csv,noheader,nounits'],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
processes = []
|
||||||
|
for line in result.stdout.strip().split('\n'):
|
||||||
|
if line:
|
||||||
|
parts = [p.strip() for p in line.split(',')]
|
||||||
|
if len(parts) >= 3:
|
||||||
|
processes.append({
|
||||||
|
'pid': parts[0],
|
||||||
|
'name': parts[1],
|
||||||
|
'memory': f"{parts[2]} MB"
|
||||||
|
})
|
||||||
|
|
||||||
|
for gpu in gpus:
|
||||||
|
if gpu['vendor'] == 'NVIDIA':
|
||||||
|
gpu['processes'] = processes
|
||||||
|
break
|
||||||
|
|
||||||
|
print(f"[v0] Enriched NVIDIA GPU(s) with nvidia-smi detailed data")
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("[v0] nvidia-smi not found")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[v0] Error getting NVIDIA GPU detailed info: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['intel_gpu_top', '-l'], capture_output=True, text=True, timeout=2)
|
||||||
|
if result.returncode == 0:
|
||||||
|
for gpu in gpus:
|
||||||
|
if gpu['vendor'] == 'Intel':
|
||||||
|
gpu['intel_gpu_top_available'] = True
|
||||||
|
print(f"[v0] Intel GPU tools available for {gpu['name']}")
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("[v0] intel_gpu_top not found")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[v0] Error checking intel_gpu_top: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['radeontop', '-d', '-', '-l', '1'], capture_output=True, text=True, timeout=2)
|
||||||
|
if result.returncode == 0:
|
||||||
|
for gpu in gpus:
|
||||||
|
if gpu['vendor'] == 'AMD':
|
||||||
|
gpu['radeontop_available'] = True
|
||||||
|
print(f"[v0] AMD GPU tools available for {gpu['name']}")
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("[v0] radeontop not found")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[v0] Error checking radeontop: {e}")
|
||||||
|
|
||||||
|
return gpus
|
||||||
|
|
||||||
|
def identify_fan_sensors():
|
||||||
|
"""Identify and categorize fan sensors (CPU, Chassis, GPU)"""
|
||||||
|
fans = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(['sensors'], capture_output=True, text=True, timeout=5)
|
result = subprocess.run(['sensors'], capture_output=True, text=True, timeout=5)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
@@ -1557,118 +1434,433 @@ def get_gpu_info():
|
|||||||
if not line:
|
if not line:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Detect adapter line
|
|
||||||
if line.startswith('Adapter:'):
|
if line.startswith('Adapter:'):
|
||||||
current_adapter = line.replace('Adapter:', '').strip()
|
current_adapter = line.replace('Adapter:', '').strip()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Look for GPU-related sensors (nouveau, amdgpu, radeon, i915, etc.)
|
|
||||||
if ':' in line and not line.startswith(' '):
|
if ':' in line and not line.startswith(' '):
|
||||||
parts = line.split(':', 1)
|
parts = line.split(':', 1)
|
||||||
sensor_name = parts[0].strip()
|
sensor_name = parts[0].strip()
|
||||||
value_part = parts[1].strip()
|
value_part = parts[1].strip()
|
||||||
|
|
||||||
# Check if this is a GPU sensor
|
if 'RPM' in value_part:
|
||||||
gpu_sensor_keywords = ['nouveau', 'amdgpu', 'radeon', 'i915']
|
|
||||||
is_gpu_sensor = any(keyword in current_adapter.lower() if current_adapter else False for keyword in gpu_sensor_keywords)
|
|
||||||
|
|
||||||
if is_gpu_sensor:
|
|
||||||
# Try to match this sensor to a GPU
|
|
||||||
for gpu in gpus:
|
|
||||||
# Match nouveau to NVIDIA, amdgpu/radeon to AMD, i915 to Intel
|
|
||||||
if (('nouveau' in current_adapter.lower() and gpu['vendor'] == 'NVIDIA') or
|
|
||||||
(('amdgpu' in current_adapter.lower() or 'radeon' in current_adapter.lower()) and gpu['vendor'] == 'AMD') or
|
|
||||||
('i915' in current_adapter.lower() and gpu['vendor'] == 'Intel')):
|
|
||||||
|
|
||||||
# Parse temperature
|
|
||||||
if '°C' in value_part or 'C' in value_part:
|
|
||||||
temp_match = re.search(r'([+-]?[\d.]+)\s*°?C', value_part)
|
|
||||||
if temp_match:
|
|
||||||
gpu['temperature'] = float(temp_match.group(1))
|
|
||||||
print(f"[v0] GPU {gpu['name']}: Temperature = {gpu['temperature']}°C")
|
|
||||||
|
|
||||||
# Parse fan speed
|
|
||||||
elif 'RPM' in value_part:
|
|
||||||
rpm_match = re.search(r'([\d.]+)\s*RPM', value_part)
|
rpm_match = re.search(r'([\d.]+)\s*RPM', value_part)
|
||||||
if rpm_match:
|
if rpm_match:
|
||||||
gpu['fan_speed'] = int(float(rpm_match.group(1)))
|
fan_speed = int(float(rpm_match.group(1)))
|
||||||
gpu['fan_unit'] = 'RPM'
|
|
||||||
print(f"[v0] GPU {gpu['name']}: Fan = {gpu['fan_speed']} RPM")
|
fan_type = 'Unknown'
|
||||||
|
if 'cpu' in sensor_name.lower() or 'processor' in sensor_name.lower():
|
||||||
|
fan_type = 'CPU Fan'
|
||||||
|
elif 'chassis' in sensor_name.lower() or 'case' in sensor_name.lower() or 'sys' in sensor_name.lower():
|
||||||
|
fan_type = 'Chassis Fan'
|
||||||
|
elif 'gpu' in sensor_name.lower() or 'video' in sensor_name.lower():
|
||||||
|
fan_type = 'GPU Fan'
|
||||||
|
elif current_adapter and ('nouveau' in current_adapter.lower() or 'nvidia' in current_adapter.lower() or 'amdgpu' in current_adapter.lower()):
|
||||||
|
fan_type = 'GPU Fan'
|
||||||
|
else:
|
||||||
|
# Try to infer from fan number
|
||||||
|
if 'fan1' in sensor_name.lower():
|
||||||
|
fan_type = 'CPU Fan'
|
||||||
|
elif 'fan2' in sensor_name.lower():
|
||||||
|
fan_type = 'Chassis Fan'
|
||||||
|
elif 'fan3' in sensor_name.lower():
|
||||||
|
fan_type = 'GPU Fan'
|
||||||
|
else:
|
||||||
|
fan_type = 'System Fan'
|
||||||
|
|
||||||
|
fans.append({
|
||||||
|
'name': sensor_name,
|
||||||
|
'type': fan_type,
|
||||||
|
'speed': fan_speed,
|
||||||
|
'unit': 'RPM',
|
||||||
|
'adapter': current_adapter
|
||||||
|
})
|
||||||
|
print(f"[v0] Fan sensor: {sensor_name} ({fan_type}) = {fan_speed} RPM")
|
||||||
|
|
||||||
|
print(f"[v0] Found {len(fans)} fan sensor(s)")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[v0] Error enriching GPU data from sensors: {e}")
|
print(f"[v0] Error identifying fan sensors: {e}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(['ipmitool', 'sensor'], capture_output=True, text=True, timeout=10)
|
||||||
['nvidia-smi', '--query-gpu=index,name,memory.total,memory.used,memory.free,temperature.gpu,power.draw,utilization.gpu,driver_version',
|
|
||||||
'--format=csv,noheader,nounits'],
|
|
||||||
capture_output=True, text=True, timeout=5
|
|
||||||
)
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
nvidia_gpus = []
|
for line in result.stdout.split('\n'):
|
||||||
for line in result.stdout.strip().split('\n'):
|
if 'fan' in line.lower() and '|' in line:
|
||||||
if line:
|
parts = [p.strip() for p in line.split('|')]
|
||||||
parts = [p.strip() for p in line.split(',')]
|
if len(parts) >= 3:
|
||||||
if len(parts) >= 9:
|
name = parts[0]
|
||||||
nvidia_gpu = {
|
value_str = parts[1]
|
||||||
'index': int(parts[0]),
|
unit = parts[2] if len(parts) > 2 else ''
|
||||||
'name': parts[1],
|
|
||||||
'vendor': 'NVIDIA',
|
|
||||||
'type': 'Discrete',
|
|
||||||
'memory_total': f"{parts[2]} MB",
|
|
||||||
'memory_used': f"{parts[3]} MB",
|
|
||||||
'memory_free': f"{parts[4]} MB",
|
|
||||||
'temperature': int(float(parts[5])) if parts[5] != '[N/A]' else 0,
|
|
||||||
'power_draw': f"{parts[6]} W" if parts[6] != '[N/A]' else 'N/A',
|
|
||||||
'utilization': int(float(parts[7])) if parts[7] != '[N/A]' else 0,
|
|
||||||
'driver_version': parts[8]
|
|
||||||
}
|
|
||||||
nvidia_gpus.append(nvidia_gpu)
|
|
||||||
|
|
||||||
# Merge nvidia-smi data with existing GPU data
|
if 'dutycycle' in name.lower() or 'presence' in name.lower():
|
||||||
for nvidia_gpu in nvidia_gpus:
|
continue
|
||||||
# Find matching GPU in gpus list
|
|
||||||
matched = False
|
|
||||||
for gpu in gpus:
|
|
||||||
if gpu['vendor'] == 'NVIDIA' and nvidia_gpu['name'] in gpu['name']:
|
|
||||||
# Enrich with nvidia-smi data
|
|
||||||
gpu.update(nvidia_gpu)
|
|
||||||
matched = True
|
|
||||||
break
|
|
||||||
|
|
||||||
# If no match found, add as new GPU
|
try:
|
||||||
if not matched:
|
value = float(value_str)
|
||||||
gpus.append(nvidia_gpu)
|
|
||||||
|
|
||||||
print(f"[v0] Enriched {len(nvidia_gpus)} NVIDIA GPU(s) with nvidia-smi data")
|
fan_type = 'System Fan'
|
||||||
|
if 'cpu' in name.lower():
|
||||||
|
fan_type = 'CPU Fan'
|
||||||
|
elif 'sys' in name.lower() or 'chassis' in name.lower():
|
||||||
|
fan_type = 'Chassis Fan'
|
||||||
|
|
||||||
|
fans.append({
|
||||||
|
'name': name,
|
||||||
|
'type': fan_type,
|
||||||
|
'speed': value,
|
||||||
|
'unit': unit,
|
||||||
|
'adapter': 'IPMI'
|
||||||
|
})
|
||||||
|
print(f"[v0] IPMI Fan: {name} ({fan_type}) = {value} {unit}")
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
print("[v0] nvidia-smi not found")
|
print("[v0] ipmitool not found")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[v0] Error getting NVIDIA GPU info from nvidia-smi: {e}")
|
print(f"[v0] Error getting IPMI fans: {e}")
|
||||||
|
|
||||||
return gpus
|
return fans
|
||||||
|
|
||||||
|
def identify_temperature_sensors():
|
||||||
|
"""Identify and categorize temperature sensors with better labeling"""
|
||||||
|
temperatures = []
|
||||||
|
power_meter = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['sensors'], capture_output=True, text=True, timeout=5)
|
||||||
|
if result.returncode == 0:
|
||||||
|
current_adapter = None
|
||||||
|
current_chip = None
|
||||||
|
|
||||||
|
for line in result.stdout.split('\n'):
|
||||||
|
line_stripped = line.strip()
|
||||||
|
if not line_stripped:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Detect chip name (lines without indentation before "Adapter:")
|
||||||
|
if not line.startswith(' ') and not line.startswith('Adapter:') and ':' not in line:
|
||||||
|
current_chip = line_stripped
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line_stripped.startswith('Adapter:'):
|
||||||
|
current_adapter = line_stripped.replace('Adapter:', '').strip()
|
||||||
|
continue
|
||||||
|
|
||||||
|
if ':' in line_stripped and not line_stripped.startswith(' '):
|
||||||
|
parts = line_stripped.split(':', 1)
|
||||||
|
sensor_name = parts[0].strip()
|
||||||
|
value_part = parts[1].strip()
|
||||||
|
|
||||||
|
if 'power' in sensor_name.lower() and 'W' in value_part:
|
||||||
|
try:
|
||||||
|
power_match = re.search(r'([\d.]+)\s*W', value_part)
|
||||||
|
if power_match:
|
||||||
|
power_value = float(power_match.group(1))
|
||||||
|
power_meter = {
|
||||||
|
'name': sensor_name,
|
||||||
|
'watts': power_value,
|
||||||
|
'adapter': current_adapter
|
||||||
|
}
|
||||||
|
print(f"[v0] Power meter sensor: {sensor_name} = {power_value}W")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif '°C' in value_part or 'C' in value_part:
|
||||||
|
try:
|
||||||
|
temp_match = re.search(r'([+-]?[\d.]+)\s*°?C', value_part)
|
||||||
|
if temp_match:
|
||||||
|
temp_value = float(temp_match.group(1))
|
||||||
|
|
||||||
|
high_match = re.search(r'high\s*=\s*([+-]?[\d.]+)', value_part)
|
||||||
|
crit_match = re.search(r'crit\s*=\s*([+-]?[\d.]+)', value_part)
|
||||||
|
|
||||||
|
high_value = float(high_match.group(1)) if high_match else 0
|
||||||
|
crit_value = float(crit_match.group(1)) if crit_match else 0
|
||||||
|
|
||||||
|
# Identify sensor type
|
||||||
|
identified_name = sensor_name
|
||||||
|
if sensor_name.lower() == 'temp1':
|
||||||
|
if current_adapter and 'pci' in current_adapter.lower():
|
||||||
|
identified_name = 'PCI adapter'
|
||||||
|
elif current_adapter and 'isa' in current_adapter.lower():
|
||||||
|
identified_name = 'ISA adapter'
|
||||||
|
elif current_chip:
|
||||||
|
identified_name = f'{current_chip} sensor'
|
||||||
|
else:
|
||||||
|
identified_name = 'System temperature'
|
||||||
|
elif 'composite' in sensor_name.lower():
|
||||||
|
identified_name = 'Composite'
|
||||||
|
elif 'package' in sensor_name.lower():
|
||||||
|
identified_name = 'Package id 0'
|
||||||
|
elif 'core' in sensor_name.lower():
|
||||||
|
identified_name = sensor_name
|
||||||
|
|
||||||
|
temperatures.append({
|
||||||
|
'name': identified_name,
|
||||||
|
'original_name': sensor_name,
|
||||||
|
'current': temp_value,
|
||||||
|
'high': high_value,
|
||||||
|
'critical': crit_value,
|
||||||
|
'adapter': current_adapter,
|
||||||
|
'chip': current_chip
|
||||||
|
})
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print(f"[v0] Found {len(temperatures)} temperature sensors")
|
||||||
|
if power_meter:
|
||||||
|
print(f"[v0] Found power meter: {power_meter['watts']}W")
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("[v0] sensors command not found")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[v0] Error identifying temperature sensors: {e}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'temperatures': temperatures,
|
||||||
|
'power_meter': power_meter
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_network_interface_details(interface_name):
|
||||||
|
"""Get detailed information about a network interface"""
|
||||||
|
details = {
|
||||||
|
'name': interface_name,
|
||||||
|
'driver': None,
|
||||||
|
'driver_version': None,
|
||||||
|
'firmware_version': None,
|
||||||
|
'bus_info': None,
|
||||||
|
'link_detected': None,
|
||||||
|
'speed': None,
|
||||||
|
'duplex': None,
|
||||||
|
'mtu': None,
|
||||||
|
'mac_address': None,
|
||||||
|
'ip_addresses': [],
|
||||||
|
'statistics': {}
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get ethtool information
|
||||||
|
result = subprocess.run(['ethtool', interface_name], capture_output=True, text=True, timeout=5)
|
||||||
|
if result.returncode == 0:
|
||||||
|
for line in result.stdout.split('\n'):
|
||||||
|
line = line.strip()
|
||||||
|
if 'Speed:' in line:
|
||||||
|
details['speed'] = line.split(':', 1)[1].strip()
|
||||||
|
elif 'Duplex:' in line:
|
||||||
|
details['duplex'] = line.split(':', 1)[1].strip()
|
||||||
|
elif 'Link detected:' in line:
|
||||||
|
details['link_detected'] = line.split(':', 1)[1].strip()
|
||||||
|
|
||||||
|
# Get driver information
|
||||||
|
result = subprocess.run(['ethtool', '-i', interface_name], capture_output=True, text=True, timeout=5)
|
||||||
|
if result.returncode == 0:
|
||||||
|
for line in result.stdout.split('\n'):
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith('driver:'):
|
||||||
|
details['driver'] = line.split(':', 1)[1].strip()
|
||||||
|
elif line.startswith('version:'):
|
||||||
|
details['driver_version'] = line.split(':', 1)[1].strip()
|
||||||
|
elif line.startswith('firmware-version:'):
|
||||||
|
details['firmware_version'] = line.split(':', 1)[1].strip()
|
||||||
|
elif line.startswith('bus-info:'):
|
||||||
|
details['bus_info'] = line.split(':', 1)[1].strip()
|
||||||
|
|
||||||
|
# Get IP addresses and MAC
|
||||||
|
result = subprocess.run(['ip', 'addr', 'show', interface_name], capture_output=True, text=True, timeout=5)
|
||||||
|
if result.returncode == 0:
|
||||||
|
for line in result.stdout.split('\n'):
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith('link/ether'):
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 2:
|
||||||
|
details['mac_address'] = parts[1]
|
||||||
|
elif line.startswith('inet '):
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 2:
|
||||||
|
details['ip_addresses'].append({
|
||||||
|
'type': 'IPv4',
|
||||||
|
'address': parts[1]
|
||||||
|
})
|
||||||
|
elif line.startswith('inet6 '):
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 2:
|
||||||
|
details['ip_addresses'].append({
|
||||||
|
'type': 'IPv6',
|
||||||
|
'address': parts[1]
|
||||||
|
})
|
||||||
|
elif 'mtu' in line.lower():
|
||||||
|
mtu_match = re.search(r'mtu\s+(\d+)', line)
|
||||||
|
if mtu_match:
|
||||||
|
details['mtu'] = mtu_match.group(1)
|
||||||
|
|
||||||
|
# Get statistics
|
||||||
|
result = subprocess.run(['ip', '-s', 'link', 'show', interface_name], capture_output=True, text=True, timeout=5)
|
||||||
|
if result.returncode == 0:
|
||||||
|
lines = result.stdout.split('\n')
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if 'RX:' in line and i + 1 < len(lines):
|
||||||
|
rx_line = lines[i + 1].strip().split()
|
||||||
|
if len(rx_line) >= 2:
|
||||||
|
details['statistics']['rx_bytes'] = rx_line[0]
|
||||||
|
details['statistics']['rx_packets'] = rx_line[1]
|
||||||
|
elif 'TX:' in line and i + 1 < len(lines):
|
||||||
|
tx_line = lines[i + 1].strip().split()
|
||||||
|
if len(tx_line) >= 2:
|
||||||
|
details['statistics']['tx_bytes'] = tx_line[0]
|
||||||
|
details['statistics']['tx_packets'] = tx_line[1]
|
||||||
|
|
||||||
|
print(f"[v0] Got detailed info for network interface {interface_name}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[v0] Error getting network interface details for {interface_name}: {e}")
|
||||||
|
|
||||||
|
return details
|
||||||
|
|
||||||
|
def get_disk_details(disk_name):
|
||||||
|
"""Get detailed information about a disk"""
|
||||||
|
details = {
|
||||||
|
'name': disk_name,
|
||||||
|
'type': None,
|
||||||
|
'driver': None,
|
||||||
|
'model': None,
|
||||||
|
'serial': None,
|
||||||
|
'size': None,
|
||||||
|
'block_size': None,
|
||||||
|
'scheduler': None,
|
||||||
|
'rotational': None,
|
||||||
|
'removable': None,
|
||||||
|
'read_only': None,
|
||||||
|
'smart_available': False,
|
||||||
|
'smart_enabled': False,
|
||||||
|
'smart_health': None,
|
||||||
|
'temperature': None,
|
||||||
|
'power_on_hours': None,
|
||||||
|
'partitions': []
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get basic disk information from lsblk
|
||||||
|
result = subprocess.run(['lsblk', '-J', '-o', 'NAME,TYPE,SIZE,MODEL,SERIAL,FSTYPE,MOUNTPOINT', f'/dev/{disk_name}'],
|
||||||
|
capture_output=True, text=True, timeout=5)
|
||||||
|
if result.returncode == 0:
|
||||||
|
import json
|
||||||
|
data = json.loads(result.stdout)
|
||||||
|
if 'blockdevices' in data and len(data['blockdevices']) > 0:
|
||||||
|
device = data['blockdevices'][0]
|
||||||
|
details['type'] = device.get('type', 'disk')
|
||||||
|
details['size'] = device.get('size')
|
||||||
|
details['model'] = device.get('model')
|
||||||
|
details['serial'] = device.get('serial')
|
||||||
|
|
||||||
|
if 'children' in device:
|
||||||
|
for child in device['children']:
|
||||||
|
details['partitions'].append({
|
||||||
|
'name': child.get('name'),
|
||||||
|
'size': child.get('size'),
|
||||||
|
'fstype': child.get('fstype'),
|
||||||
|
'mountpoint': child.get('mountpoint')
|
||||||
|
})
|
||||||
|
|
||||||
|
# Get sys information
|
||||||
|
sys_path = f'/sys/block/{disk_name}'
|
||||||
|
try:
|
||||||
|
with open(f'{sys_path}/queue/rotational', 'r') as f:
|
||||||
|
details['rotational'] = f.read().strip() == '1'
|
||||||
|
with open(f'{sys_path}/removable', 'r') as f:
|
||||||
|
details['removable'] = f.read().strip() == '1'
|
||||||
|
with open(f'{sys_path}/ro', 'r') as f:
|
||||||
|
details['read_only'] = f.read().strip() == '1'
|
||||||
|
with open(f'{sys_path}/queue/scheduler', 'r') as f:
|
||||||
|
scheduler_line = f.read().strip()
|
||||||
|
scheduler_match = re.search(r'\[([^\]]+)\]', scheduler_line)
|
||||||
|
if scheduler_match:
|
||||||
|
details['scheduler'] = scheduler_match.group(1)
|
||||||
|
with open(f'{sys_path}/queue/physical_block_size', 'r') as f:
|
||||||
|
details['block_size'] = f.read().strip()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Determine disk type
|
||||||
|
if disk_name.startswith('nvme'):
|
||||||
|
details['type'] = 'NVMe'
|
||||||
|
details['driver'] = 'nvme'
|
||||||
|
elif disk_name.startswith('sd'):
|
||||||
|
if details['rotational']:
|
||||||
|
details['type'] = 'HDD'
|
||||||
|
else:
|
||||||
|
details['type'] = 'SSD'
|
||||||
|
details['driver'] = 'sd'
|
||||||
|
elif disk_name.startswith('hd'):
|
||||||
|
details['type'] = 'IDE/PATA'
|
||||||
|
details['driver'] = 'ide'
|
||||||
|
elif disk_name.startswith('mmcblk'):
|
||||||
|
details['type'] = 'MMC/SD'
|
||||||
|
details['driver'] = 'mmc'
|
||||||
|
|
||||||
|
# Try to get SMART data
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['smartctl', '-i', f'/dev/{disk_name}'], capture_output=True, text=True, timeout=5)
|
||||||
|
if result.returncode == 0:
|
||||||
|
details['smart_available'] = True
|
||||||
|
for line in result.stdout.split('\n'):
|
||||||
|
if 'SMART support is:' in line and 'Enabled' in line:
|
||||||
|
details['smart_enabled'] = True
|
||||||
|
|
||||||
|
if details['smart_enabled']:
|
||||||
|
result = subprocess.run(['smartctl', '-H', f'/dev/{disk_name}'], capture_output=True, text=True, timeout=5)
|
||||||
|
if result.returncode == 0:
|
||||||
|
for line in result.stdout.split('\n'):
|
||||||
|
if 'SMART overall-health' in line or 'SMART Health Status' in line:
|
||||||
|
if 'PASSED' in line or 'OK' in line:
|
||||||
|
details['smart_health'] = 'PASSED'
|
||||||
|
else:
|
||||||
|
details['smart_health'] = 'FAILED'
|
||||||
|
|
||||||
|
result = subprocess.run(['smartctl', '-A', f'/dev/{disk_name}'], capture_output=True, text=True, timeout=5)
|
||||||
|
if result.returncode == 0:
|
||||||
|
for line in result.stdout.split('\n'):
|
||||||
|
if 'Temperature' in line or 'Airflow_Temperature' in line:
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 10:
|
||||||
|
try:
|
||||||
|
details['temperature'] = int(parts[9])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
elif 'Power_On_Hours' in line:
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 10:
|
||||||
|
try:
|
||||||
|
details['power_on_hours'] = int(parts[9])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"[v0] smartctl not found")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[v0] Error getting SMART data for {disk_name}: {e}")
|
||||||
|
|
||||||
|
print(f"[v0] Got detailed info for disk {disk_name}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[v0] Error getting disk details for {disk_name}: {e}")
|
||||||
|
|
||||||
|
return details
|
||||||
|
|
||||||
|
# --- END OF UPDATED CODE ---
|
||||||
|
|
||||||
def get_hardware_info():
|
def get_hardware_info():
|
||||||
"""Get comprehensive hardware information"""
|
"""Get comprehensive hardware information"""
|
||||||
try:
|
|
||||||
# Initialize with default structure, including the new power_meter field
|
|
||||||
hardware_data = {
|
hardware_data = {
|
||||||
'cpu': {},
|
'cpu': {},
|
||||||
'motherboard': {},
|
'motherboard': {},
|
||||||
'memory_modules': [],
|
'memory_modules': [],
|
||||||
'storage_devices': [],
|
|
||||||
'network_cards': [],
|
|
||||||
'graphics_cards': [],
|
|
||||||
'gpus': [], # Added dedicated GPU array
|
|
||||||
'pci_devices': [],
|
|
||||||
'sensors': {
|
|
||||||
'temperatures': [],
|
'temperatures': [],
|
||||||
'fans': []
|
'power_meter': None,
|
||||||
},
|
'gpus': [],
|
||||||
'power': {}, # This might be overwritten by ipmi_power or ups
|
'pci_devices': [],
|
||||||
'ipmi_fans': [], # Added IPMI fans
|
'fans': [],
|
||||||
'ipmi_power': {}, # Added IPMI power
|
'power_supplies': [],
|
||||||
'ups': {}, # Added UPS info
|
'ups': {},
|
||||||
'power_meter': None # Added placeholder for sensors power meter
|
'storage_devices': [],
|
||||||
|
'ipmi_fans': [],
|
||||||
|
'ipmi_power': {}
|
||||||
}
|
}
|
||||||
|
|
||||||
# CPU Information
|
# CPU Information
|
||||||
@@ -1811,7 +2003,7 @@ def get_hardware_info():
|
|||||||
result = subprocess.run(['lspci'], capture_output=True, text=True, timeout=5)
|
result = subprocess.run(['lspci'], capture_output=True, text=True, timeout=5)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
for line in result.stdout.split('\n'):
|
for line in result.stdout.split('\n'):
|
||||||
# Match VGA, 3D, Display controllers, and specific GPU keywords
|
# Match VGA, 3D, Display controllers
|
||||||
if any(keyword in line for keyword in ['VGA compatible controller', '3D controller', 'Display controller']):
|
if any(keyword in line for keyword in ['VGA compatible controller', '3D controller', 'Display controller']):
|
||||||
parts = line.split(':', 2)
|
parts = line.split(':', 2)
|
||||||
if len(parts) >= 3:
|
if len(parts) >= 3:
|
||||||
@@ -1955,14 +2147,14 @@ def get_hardware_info():
|
|||||||
if temps:
|
if temps:
|
||||||
for sensor_name, entries in temps.items():
|
for sensor_name, entries in temps.items():
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
hardware_data['sensors']['temperatures'].append({
|
hardware_data['temperatures'].append({
|
||||||
'name': f"{sensor_name} - {entry.label}" if entry.label else sensor_name,
|
'name': f"{sensor_name} - {entry.label}" if entry.label else sensor_name,
|
||||||
'current': entry.current,
|
'current': entry.current,
|
||||||
'high': entry.high if entry.high else 0,
|
'high': entry.high if entry.high else 0,
|
||||||
'critical': entry.critical if entry.critical else 0
|
'critical': entry.critical if entry.critical else 0
|
||||||
})
|
})
|
||||||
|
|
||||||
print(f"[v0] Temperature sensors: {len(hardware_data['sensors']['temperatures'])} found")
|
print(f"[v0] Temperature sensors: {len(hardware_data['temperatures'])} found")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(['sensors'], capture_output=True, text=True, timeout=5)
|
result = subprocess.run(['sensors'], capture_output=True, text=True, timeout=5)
|
||||||
@@ -1975,18 +2167,15 @@ def get_hardware_info():
|
|||||||
if not line:
|
if not line:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Detect adapter line
|
|
||||||
if line.startswith('Adapter:'):
|
if line.startswith('Adapter:'):
|
||||||
current_adapter = line.replace('Adapter:', '').strip()
|
current_adapter = line.replace('Adapter:', '').strip()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Parse fan sensors
|
|
||||||
if ':' in line and not line.startswith(' '):
|
if ':' in line and not line.startswith(' '):
|
||||||
parts = line.split(':', 1)
|
parts = line.split(':', 1)
|
||||||
sensor_name = parts[0].strip()
|
sensor_name = parts[0].strip()
|
||||||
value_part = parts[1].strip()
|
value_part = parts[1].strip()
|
||||||
|
|
||||||
# Look for fan sensors (RPM)
|
|
||||||
if 'RPM' in value_part:
|
if 'RPM' in value_part:
|
||||||
rpm_match = re.search(r'([\d.]+)\s*RPM', value_part)
|
rpm_match = re.search(r'([\d.]+)\s*RPM', value_part)
|
||||||
if rpm_match:
|
if rpm_match:
|
||||||
@@ -2038,8 +2227,8 @@ def get_hardware_info():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[v0] Error getting UPS info: {e}")
|
print(f"[v0] Error getting UPS info: {e}")
|
||||||
|
|
||||||
temp_info = get_temperature_info()
|
temp_info = identify_temperature_sensors()
|
||||||
hardware_data['sensors']['temperatures'] = temp_info['temperatures']
|
hardware_data['temperatures'] = temp_info['temperatures']
|
||||||
hardware_data['power_meter'] = temp_info['power_meter']
|
hardware_data['power_meter'] = temp_info['power_meter']
|
||||||
|
|
||||||
ipmi_fans = get_ipmi_fans()
|
ipmi_fans = get_ipmi_fans()
|
||||||
@@ -2054,17 +2243,12 @@ def get_hardware_info():
|
|||||||
if ups_info:
|
if ups_info:
|
||||||
hardware_data['ups'] = ups_info
|
hardware_data['ups'] = ups_info
|
||||||
|
|
||||||
hardware_data['gpus'] = get_gpu_info()
|
hardware_data['gpus'] = get_gpu_detailed_info()
|
||||||
|
|
||||||
|
hardware_data['fans'] = identify_fan_sensors()
|
||||||
|
|
||||||
return hardware_data
|
return hardware_data
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[v0] Error in get_hardware_info: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/system', methods=['GET'])
|
@app.route('/api/system', methods=['GET'])
|
||||||
def api_system():
|
def api_system():
|
||||||
"""Get system information"""
|
"""Get system information"""
|
||||||
@@ -2215,10 +2399,8 @@ def api_hardware():
|
|||||||
'cpu': hardware_info.get('cpu', {}),
|
'cpu': hardware_info.get('cpu', {}),
|
||||||
'motherboard': hardware_info.get('motherboard', {}),
|
'motherboard': hardware_info.get('motherboard', {}),
|
||||||
'memory_modules': hardware_info.get('memory_modules', []),
|
'memory_modules': hardware_info.get('memory_modules', []),
|
||||||
'storage_devices': hardware_info.get('storage_devices', []),
|
'temperatures': hardware_info.get('temperatures', []),
|
||||||
'pci_devices': hardware_info.get('pci_devices', []),
|
'fans': hardware_info.get('fans', []), # Use sensors fans
|
||||||
'temperatures': hardware_info.get('sensors', {}).get('temperatures', []),
|
|
||||||
'fans': hardware_info.get('sensors', {}).get('fans', []), # Use sensors fans
|
|
||||||
'power_supplies': hardware_info.get('ipmi_power', {}).get('power_supplies', []), # Use IPMI power supplies
|
'power_supplies': hardware_info.get('ipmi_power', {}).get('power_supplies', []), # Use IPMI power supplies
|
||||||
'power_meter': hardware_info.get('power_meter'),
|
'power_meter': hardware_info.get('power_meter'),
|
||||||
'ups': hardware_info.get('ups') if hardware_info.get('ups') else None,
|
'ups': hardware_info.get('ups') if hardware_info.get('ups') else None,
|
||||||
@@ -2375,9 +2557,30 @@ def api_vm_control(vmid):
|
|||||||
print(f"Error controlling VM: {e}")
|
print(f"Error controlling VM: {e}")
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/hardware/network/<interface_name>')
|
||||||
|
def api_network_interface_details(interface_name):
|
||||||
|
"""API endpoint for detailed network interface information"""
|
||||||
|
try:
|
||||||
|
details = get_network_interface_details(interface_name)
|
||||||
|
return jsonify(details)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[v0] Error in network interface details API: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/hardware/disk/<disk_name>')
|
||||||
|
def api_disk_details(disk_name):
|
||||||
|
"""API endpoint for detailed disk information"""
|
||||||
|
try:
|
||||||
|
details = get_disk_details(disk_name)
|
||||||
|
return jsonify(details)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[v0] Error in disk details API: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
print("Starting ProxMenux Flask Server on port 8008...")
|
print("Starting ProxMenux Flask Server on port 8008...")
|
||||||
print("Server will be accessible on all network interfaces (0.0.0.0:8008)")
|
print("Server will be accessible on all network interfaces (0.0.0.0:8008)")
|
||||||
print("API endpoints available at: /api/system, /api/storage, /api/network, /api/vms, /api/logs, /api/health, /api/hardware")
|
print("API endpoints available at: /api/system, /api/storage, /api/network, /api/vms, /api/logs, /api/health, /api/hardware")
|
||||||
|
|
||||||
app.run(host='0.0.0.0', port=8008, debug=False)
|
app.run(host='0.0.0.0', port=8008, debug=False)
|
||||||
|
.0.0.0', port=8008, debug=False)
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
export interface Temperature {
|
export interface Temperature {
|
||||||
name: string
|
name: string
|
||||||
|
original_name?: string
|
||||||
current: number
|
current: number
|
||||||
high?: number
|
high?: number
|
||||||
critical?: number
|
critical?: number
|
||||||
adapter?: string
|
adapter?: string
|
||||||
|
chip?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PowerMeter {
|
export interface PowerMeter {
|
||||||
@@ -51,10 +53,42 @@ export interface PCIDevice {
|
|||||||
gpu_memory_clock?: string
|
gpu_memory_clock?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GPUProcess {
|
||||||
|
pid: string
|
||||||
|
name: string
|
||||||
|
memory: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GPU {
|
||||||
|
slot: string
|
||||||
|
name: string
|
||||||
|
vendor: string
|
||||||
|
type: string
|
||||||
|
index?: number
|
||||||
|
memory_total?: string
|
||||||
|
memory_used?: string
|
||||||
|
memory_free?: string
|
||||||
|
temperature?: number
|
||||||
|
power_draw?: string
|
||||||
|
power_limit?: string
|
||||||
|
utilization?: number
|
||||||
|
memory_utilization?: number
|
||||||
|
clock_graphics?: string
|
||||||
|
clock_memory?: string
|
||||||
|
driver_version?: string
|
||||||
|
pcie_gen?: string
|
||||||
|
pcie_width?: string
|
||||||
|
processes?: GPUProcess[]
|
||||||
|
intel_gpu_top_available?: boolean
|
||||||
|
radeontop_available?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface Fan {
|
export interface Fan {
|
||||||
name: string
|
name: string
|
||||||
|
type: string
|
||||||
speed: number
|
speed: number
|
||||||
unit: string
|
unit: string
|
||||||
|
adapter?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PowerSupply {
|
export interface PowerSupply {
|
||||||
@@ -73,15 +107,69 @@ export interface UPS {
|
|||||||
output_voltage?: number
|
output_voltage?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DiskPartition {
|
||||||
|
name: string
|
||||||
|
size?: string
|
||||||
|
fstype?: string
|
||||||
|
mountpoint?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiskDetails {
|
||||||
|
name: string
|
||||||
|
type?: string
|
||||||
|
driver?: string
|
||||||
|
model?: string
|
||||||
|
serial?: string
|
||||||
|
size?: string
|
||||||
|
block_size?: string
|
||||||
|
scheduler?: string
|
||||||
|
rotational?: boolean
|
||||||
|
removable?: boolean
|
||||||
|
read_only?: boolean
|
||||||
|
smart_available?: boolean
|
||||||
|
smart_enabled?: boolean
|
||||||
|
smart_health?: string
|
||||||
|
temperature?: number
|
||||||
|
power_on_hours?: number
|
||||||
|
partitions?: DiskPartition[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkInterfaceDetails {
|
||||||
|
name: string
|
||||||
|
driver?: string
|
||||||
|
driver_version?: string
|
||||||
|
firmware_version?: string
|
||||||
|
bus_info?: string
|
||||||
|
link_detected?: string
|
||||||
|
speed?: string
|
||||||
|
duplex?: string
|
||||||
|
mtu?: string
|
||||||
|
mac_address?: string
|
||||||
|
ip_addresses?: Array<{
|
||||||
|
type: string
|
||||||
|
address: string
|
||||||
|
}>
|
||||||
|
statistics?: {
|
||||||
|
rx_bytes?: string
|
||||||
|
rx_packets?: string
|
||||||
|
tx_bytes?: string
|
||||||
|
tx_packets?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface HardwareData {
|
export interface HardwareData {
|
||||||
temperatures?: Temperature[]
|
temperatures?: Temperature[]
|
||||||
power_meter?: PowerMeter
|
power_meter?: PowerMeter
|
||||||
network_cards?: NetworkInterface[]
|
network_cards?: NetworkInterface[]
|
||||||
storage_devices?: StorageDevice[]
|
storage_devices?: StorageDevice[]
|
||||||
pci_devices?: PCIDevice[]
|
pci_devices?: PCIDevice[]
|
||||||
|
gpus?: GPU[]
|
||||||
fans?: Fan[]
|
fans?: Fan[]
|
||||||
power_supplies?: PowerSupply[]
|
power_supplies?: PowerSupply[]
|
||||||
ups?: UPS
|
ups?: UPS
|
||||||
|
cpu?: any
|
||||||
|
motherboard?: any
|
||||||
|
memory_modules?: any[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetcher = (url: string) => fetch(url).then((res) => res.json())
|
export const fetcher = (url: string) => fetch(url).then((res) => res.json())
|
||||||
|
Reference in New Issue
Block a user