Update AppImage

This commit is contained in:
MacRimi
2025-10-14 15:34:19 +02:00
parent c1b578350d
commit f49ffe3cb0
4 changed files with 262 additions and 1303 deletions

View File

@@ -17,7 +17,6 @@ import {
MemoryStick, MemoryStick,
Cpu as Gpu, Cpu as Gpu,
Loader2, Loader2,
Info,
} from "lucide-react" } from "lucide-react"
import useSWR from "swr" import useSWR from "swr"
import { useState, useEffect } from "react" import { useState, useEffect } from "react"
@@ -98,7 +97,7 @@ const getMonitoringToolRecommendation = (vendor: string): string => {
} }
if (lowerVendor.includes("amd") || lowerVendor.includes("ati")) { if (lowerVendor.includes("amd") || lowerVendor.includes("ati")) {
return "To get extended GPU monitoring information, please install amdgpu_top package. Download from: https://github.com/Umio-Yasuno/amdgpu_top/releases" return "To get extended GPU monitoring information, please install radeontop package."
} }
return "To get extended GPU monitoring information, please install the appropriate GPU monitoring tools for your hardware." return "To get extended GPU monitoring information, please install the appropriate GPU monitoring tools for your hardware."
} }
@@ -114,7 +113,6 @@ export default function Hardware() {
const [selectedPCIDevice, setSelectedPCIDevice] = useState<PCIDevice | null>(null) const [selectedPCIDevice, setSelectedPCIDevice] = useState<PCIDevice | null>(null)
const [selectedDisk, setSelectedDisk] = useState<StorageDevice | null>(null) const [selectedDisk, setSelectedDisk] = useState<StorageDevice | null>(null)
const [selectedNetwork, setSelectedNetwork] = useState<PCIDevice | null>(null) const [selectedNetwork, setSelectedNetwork] = useState<PCIDevice | null>(null)
const [selectedUPS, setSelectedUPS] = useState<any>(null) // Added state for UPS modal
useEffect(() => { useEffect(() => {
if (!selectedGPU) return if (!selectedGPU) return
@@ -122,24 +120,14 @@ export default function Hardware() {
const pciDevice = findPCIDeviceForGPU(selectedGPU) const pciDevice = findPCIDeviceForGPU(selectedGPU)
const fullSlot = pciDevice?.slot || selectedGPU.slot const fullSlot = pciDevice?.slot || selectedGPU.slot
if (!fullSlot) { if (!fullSlot) return
setDetailsLoading(false)
setRealtimeGPUData({ has_monitoring_tool: false })
return
}
const abortController = new AbortController() const abortController = new AbortController()
let timeoutId: NodeJS.Timeout
const fetchRealtimeData = async () => { const fetchRealtimeData = async () => {
try { try {
const apiUrl = `http://${window.location.hostname}:8008/api/gpu/${fullSlot}/realtime` const apiUrl = `http://${window.location.hostname}:8008/api/gpu/${fullSlot}/realtime`
// Set a timeout of 5 seconds
timeoutId = setTimeout(() => {
abortController.abort()
}, 5000)
const response = await fetch(apiUrl, { const response = await fetch(apiUrl, {
method: "GET", method: "GET",
headers: { headers: {
@@ -148,8 +136,6 @@ export default function Hardware() {
signal: abortController.signal, signal: abortController.signal,
}) })
clearTimeout(timeoutId)
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`) throw new Error(`HTTP error! status: ${response.status}`)
} }
@@ -175,31 +161,9 @@ export default function Hardware() {
return () => { return () => {
clearInterval(interval) clearInterval(interval)
clearTimeout(timeoutId)
abortController.abort() abortController.abort()
} }
}, [selectedGPU, hardwareData?.pci_devices]) }, [selectedGPU])
if (!hardwareData && !error) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-2 text-primary" />
<p className="text-sm text-muted-foreground">Loading hardware information...</p>
</div>
</div>
)
}
if (error) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<p className="text-sm text-destructive">Error loading hardware information</p>
</div>
</div>
)
}
const handleGPUClick = async (gpu: GPU) => { const handleGPUClick = async (gpu: GPU) => {
setSelectedGPU(gpu) setSelectedGPU(gpu)
@@ -382,7 +346,7 @@ export default function Hardware() {
</Card> </Card>
)} )}
{/* Thermal Monitoring - Organized by Category */} {/* 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">
@@ -393,84 +357,33 @@ export default function Hardware() {
</Badge> </Badge>
</div> </div>
{(() => { <div className="grid gap-4 md:grid-cols-2">
// Group temperatures by category {hardwareData.temperatures.map((temp, index) => {
const tempsByCategory: Record<string, typeof hardwareData.temperatures> = {} const percentage = temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
hardwareData.temperatures.forEach((temp) => { const isHot = temp.current > (temp.high || 80)
const category = (temp as any).category || "Other" const isCritical = temp.current > (temp.critical || 90)
if (!tempsByCategory[category]) {
tempsByCategory[category] = []
}
tempsByCategory[category].push(temp)
})
// Define category order and icons return (
const categoryOrder = ["CPU", "GPU", "NVMe", "Storage", "Motherboard", "Chipset", "PCI", "Other"] <div key={index} className="space-y-2">
const categoryIcons: Record<string, any> = { <div className="flex items-center justify-between">
CPU: CpuIcon, <span className="text-sm font-medium">{temp.name}</span>
GPU: Gpu, <span
NVMe: HardDrive, className={`text-sm font-semibold ${isCritical ? "text-red-500" : isHot ? "text-orange-500" : "text-green-500"}`}
Storage: HardDrive, >
Motherboard: Cpu, {temp.current.toFixed(1)}°C
Chipset: Cpu, </span>
PCI: Network, </div>
Other: Thermometer, <div className="h-2 w-full overflow-hidden rounded-full bg-secondary">
} <div
className="h-full bg-blue-500 transition-all"
return ( style={{ width: `${Math.min(percentage, 100)}%` }}
<div className="space-y-6"> />
{categoryOrder.map((category) => { </div>
const temps = tempsByCategory[category] {temp.adapter && <span className="text-xs text-muted-foreground">{temp.adapter}</span>}
if (!temps || temps.length === 0) return null </div>
)
const CategoryIcon = categoryIcons[category] || Thermometer })}
</div>
return (
<div key={category}>
<div className="mb-3 flex items-center gap-2">
<CategoryIcon className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
{category}
</h3>
<Badge variant="outline" className="text-xs">
{temps.length}
</Badge>
</div>
<div className="grid gap-4 md:grid-cols-2">
{temps.map((temp, index) => {
const percentage =
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
const isHot = temp.current > (temp.high || 80)
const isCritical = temp.current > (temp.critical || 90)
return (
<div key={index} className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">{temp.name}</span>
<span
className={`text-sm font-semibold ${isCritical ? "text-red-500" : isHot ? "text-orange-500" : "text-green-500"}`}
>
{temp.current.toFixed(1)}°C
</span>
</div>
<div className="h-2 w-full overflow-hidden rounded-full bg-secondary">
<div
className={`h-full transition-all ${isCritical ? "bg-red-500" : isHot ? "bg-orange-500" : "bg-blue-500"}`}
style={{ width: `${Math.min(percentage, 100)}%` }}
/>
</div>
{temp.adapter && <span className="text-xs text-muted-foreground">{temp.adapter}</span>}
</div>
)
})}
</div>
</div>
)
})}
</div>
)
})()}
</Card> </Card>
)} )}
@@ -1040,7 +953,7 @@ export default function Hardware() {
{/* Power Supplies */} {/* Power Supplies */}
{/* This section was moved to be grouped with Power Consumption */} {/* This section was moved to be grouped with Power Consumption */}
{/* UPS - Enhanced with detailed modal */} {/* 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">
@@ -1051,16 +964,7 @@ export default function Hardware() {
<div className="space-y-4"> <div className="space-y-4">
<div className="rounded-lg border border-border/30 bg-background/50 p-4"> <div className="rounded-lg border border-border/30 bg-background/50 p-4">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2"> <span className="text-sm font-medium">{hardwareData.ups.model}</span>
<span className="text-sm font-medium">{hardwareData.ups.model}</span>
<button
onClick={() => setSelectedUPS(hardwareData.ups)}
className="p-1 rounded-md hover:bg-accent transition-colors"
title="View detailed UPS information"
>
<Info className="h-4 w-4 text-muted-foreground hover:text-primary" />
</button>
</div>
<Badge <Badge
variant={hardwareData.ups.status === "OL" ? "default" : "destructive"} variant={hardwareData.ups.status === "OL" ? "default" : "destructive"}
className={ className={
@@ -1125,367 +1029,6 @@ export default function Hardware() {
</Card> </Card>
)} )}
<Dialog open={selectedUPS !== null} onOpenChange={() => setSelectedUPS(null)}>
<DialogContent className="max-w-4xl max-h-[85vh] overflow-y-auto">
{selectedUPS && (
<>
<DialogHeader className="pb-4 border-b border-border">
<DialogTitle>{selectedUPS.model || selectedUPS.name}</DialogTitle>
<DialogDescription>Comprehensive UPS Information</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
{/* Device Information */}
{(selectedUPS.model || selectedUPS.manufacturer || selectedUPS.serial || selectedUPS.device_type) && (
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
Device Information
</h3>
<div className="grid gap-2">
{selectedUPS.model && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Model</span>
<span className="text-sm font-medium text-right">{selectedUPS.model}</span>
</div>
)}
{selectedUPS.manufacturer && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Manufacturer</span>
<span className="text-sm font-medium">{selectedUPS.manufacturer}</span>
</div>
)}
{selectedUPS.serial && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Serial Number</span>
<span className="font-mono text-sm">{selectedUPS.serial}</span>
</div>
)}
{selectedUPS.device_type && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Device Type</span>
<span className="text-sm font-medium">{selectedUPS.device_type}</span>
</div>
)}
{selectedUPS.firmware && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Firmware</span>
<span className="font-mono text-sm">{selectedUPS.firmware}</span>
</div>
)}
</div>
</div>
)}
{/* Status */}
{(selectedUPS.status || selectedUPS.beeper_status || selectedUPS.test_result) && (
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">Status</h3>
<div className="grid gap-2">
{selectedUPS.status && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">UPS Status</span>
<Badge
variant={selectedUPS.status === "OL" ? "default" : "destructive"}
className={
selectedUPS.status === "OL" ? "bg-green-500/10 text-green-500 border-green-500/20" : ""
}
>
{selectedUPS.status}
</Badge>
</div>
)}
{selectedUPS.beeper_status && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Beeper Status</span>
<span className="text-sm font-medium">{selectedUPS.beeper_status}</span>
</div>
)}
{selectedUPS.test_result && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Test Result</span>
<span className="text-sm font-medium">{selectedUPS.test_result}</span>
</div>
)}
</div>
</div>
)}
{/* Battery */}
{(selectedUPS.battery_charge ||
selectedUPS.time_left ||
selectedUPS.battery_voltage ||
selectedUPS.battery_type) && (
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
Battery
</h3>
<div className="grid gap-2">
{selectedUPS.battery_charge && (
<div className="space-y-1">
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">Charge Level</span>
<span className="text-sm font-medium">{selectedUPS.battery_charge}</span>
</div>
<Progress
value={Number.parseInt(selectedUPS.battery_charge.replace("%", ""))}
className="h-2 [&>div]:bg-blue-500"
/>
</div>
)}
{selectedUPS.battery_charge_low && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Low Battery Threshold</span>
<span className="text-sm font-medium">{selectedUPS.battery_charge_low}</span>
</div>
)}
{selectedUPS.time_left && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Runtime Remaining</span>
<span className="text-sm font-medium text-green-500">{selectedUPS.time_left}</span>
</div>
)}
{selectedUPS.battery_runtime_low && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Low Runtime Threshold</span>
<span className="text-sm font-medium">{selectedUPS.battery_runtime_low}</span>
</div>
)}
{selectedUPS.battery_voltage && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Battery Voltage</span>
<span className="text-sm font-medium">{selectedUPS.battery_voltage}</span>
</div>
)}
{selectedUPS.battery_voltage_nominal && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Nominal Voltage</span>
<span className="text-sm font-medium">{selectedUPS.battery_voltage_nominal}</span>
</div>
)}
{selectedUPS.battery_type && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Battery Type</span>
<span className="text-sm font-medium">{selectedUPS.battery_type}</span>
</div>
)}
{selectedUPS.battery_mfr_date && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Battery Manufacture Date</span>
<span className="text-sm font-medium">{selectedUPS.battery_mfr_date}</span>
</div>
)}
</div>
</div>
)}
{/* Power */}
{(selectedUPS.load_percent || selectedUPS.real_power || selectedUPS.apparent_power) && (
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">Power</h3>
<div className="grid gap-2">
{selectedUPS.load_percent && (
<div className="space-y-1">
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">Load</span>
<span className="text-sm font-medium">{selectedUPS.load_percent}</span>
</div>
<Progress
value={Number.parseInt(selectedUPS.load_percent.replace("%", ""))}
className="h-2 [&>div]:bg-blue-500"
/>
</div>
)}
{selectedUPS.real_power && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Real Power</span>
<span className="text-sm font-medium text-blue-500">{selectedUPS.real_power}</span>
</div>
)}
{selectedUPS.realpower_nominal && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Nominal Real Power</span>
<span className="text-sm font-medium">{selectedUPS.realpower_nominal}</span>
</div>
)}
{selectedUPS.apparent_power && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Apparent Power</span>
<span className="text-sm font-medium">{selectedUPS.apparent_power}</span>
</div>
)}
{selectedUPS.power_nominal && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Nominal Apparent Power</span>
<span className="text-sm font-medium">{selectedUPS.power_nominal}</span>
</div>
)}
</div>
</div>
)}
{/* Input/Output */}
{(selectedUPS.input_voltage || selectedUPS.output_voltage || selectedUPS.input_frequency) && (
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
Input / Output
</h3>
<div className="grid gap-2 md:grid-cols-2">
{selectedUPS.input_voltage && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Input Voltage</span>
<span className="text-sm font-medium text-green-500">{selectedUPS.input_voltage}</span>
</div>
)}
{selectedUPS.input_voltage_nominal && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Nominal Input Voltage</span>
<span className="text-sm font-medium">{selectedUPS.input_voltage_nominal}</span>
</div>
)}
{selectedUPS.input_frequency && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Input Frequency</span>
<span className="text-sm font-medium">{selectedUPS.input_frequency}</span>
</div>
)}
{selectedUPS.input_transfer_high && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Transfer High</span>
<span className="text-sm font-medium">{selectedUPS.input_transfer_high}</span>
</div>
)}
{selectedUPS.input_transfer_low && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Transfer Low</span>
<span className="text-sm font-medium">{selectedUPS.input_transfer_low}</span>
</div>
)}
{selectedUPS.transfer_reason && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Last Transfer Reason</span>
<span className="text-sm font-medium">{selectedUPS.transfer_reason}</span>
</div>
)}
{selectedUPS.output_voltage && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Output Voltage</span>
<span className="text-sm font-medium">{selectedUPS.output_voltage}</span>
</div>
)}
{selectedUPS.output_voltage_nominal && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Nominal Output Voltage</span>
<span className="text-sm font-medium">{selectedUPS.output_voltage_nominal}</span>
</div>
)}
{selectedUPS.output_frequency && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Output Frequency</span>
<span className="text-sm font-medium">{selectedUPS.output_frequency}</span>
</div>
)}
</div>
</div>
)}
{/* Driver */}
{(selectedUPS.driver_name || selectedUPS.driver_version) && (
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">Driver</h3>
<div className="grid gap-2">
{selectedUPS.driver_name && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Driver Name</span>
<span className="font-mono text-sm text-green-500">{selectedUPS.driver_name}</span>
</div>
)}
{selectedUPS.driver_version && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Driver Version</span>
<span className="font-mono text-sm">{selectedUPS.driver_version}</span>
</div>
)}
{selectedUPS.driver_version_internal && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Internal Version</span>
<span className="font-mono text-sm">{selectedUPS.driver_version_internal}</span>
</div>
)}
{selectedUPS.driver_poll_freq && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Poll Frequency</span>
<span className="text-sm font-medium">{selectedUPS.driver_poll_freq}</span>
</div>
)}
{selectedUPS.driver_poll_interval && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Poll Interval</span>
<span className="text-sm font-medium">{selectedUPS.driver_poll_interval}</span>
</div>
)}
</div>
</div>
)}
{/* Configuration */}
{(selectedUPS.delay_shutdown || selectedUPS.delay_start || selectedUPS.timer_shutdown) && (
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
Configuration
</h3>
<div className="grid gap-2">
{selectedUPS.delay_shutdown && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Shutdown Delay</span>
<span className="text-sm font-medium">{selectedUPS.delay_shutdown}</span>
</div>
)}
{selectedUPS.delay_start && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Start Delay</span>
<span className="text-sm font-medium">{selectedUPS.delay_start}</span>
</div>
)}
{selectedUPS.timer_shutdown && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Shutdown Timer</span>
<span className="text-sm font-medium">{selectedUPS.timer_shutdown}</span>
</div>
)}
{selectedUPS.timer_reboot && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Reboot Timer</span>
<span className="text-sm font-medium">{selectedUPS.timer_reboot}</span>
</div>
)}
</div>
</div>
)}
{/* Raw Variables - Collapsible section with all UPS variables */}
{selectedUPS.raw_variables && Object.keys(selectedUPS.raw_variables).length > 0 && (
<details className="rounded-lg border border-border/30 bg-background/50">
<summary className="cursor-pointer p-4 font-medium text-sm hover:bg-accent/50 transition-colors">
All UPS Variables ({Object.keys(selectedUPS.raw_variables).length})
</summary>
<div className="p-4 pt-0 max-h-[300px] overflow-y-auto">
<div className="grid gap-1">
{Object.entries(selectedUPS.raw_variables).map(([key, value]) => (
<div key={key} className="flex justify-between border-b border-border/50 pb-1 text-xs">
<span className="font-mono text-muted-foreground">{key}</span>
<span className="font-mono">{value as string}</span>
</div>
))}
</div>
</div>
</details>
)}
</div>
</>
)}
</DialogContent>
</Dialog>
{/* Network Summary - Clickable */} {/* Network Summary - Clickable */}
{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 && (

View File

@@ -4,19 +4,7 @@ import { useState, useEffect } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card" import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Progress } from "./ui/progress" import { Progress } from "./ui/progress"
import { Badge } from "./ui/badge" import { Badge } from "./ui/badge"
import { Button } from "./ui/button" import { HardDrive, Database, Archive, AlertTriangle, CheckCircle, Activity, AlertCircle } from "lucide-react"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog"
import {
HardDrive,
Database,
Archive,
AlertTriangle,
CheckCircle,
Activity,
AlertCircle,
Thermometer,
Info,
} from "lucide-react"
interface StorageData { interface StorageData {
total: number total: number
@@ -35,70 +23,11 @@ interface DiskInfo {
usage_percent: number usage_percent: number
health: string health: string
temperature: number temperature: number
disk_type?: string
model?: string
serial?: string
smart_status?: string
power_on_hours?: number
power_cycles?: number
reallocated_sectors?: number
pending_sectors?: number
crc_errors?: number
percentage_used?: number // NVMe
ssd_life_left?: number // SSD
wear_leveling_count?: number // SSD
media_wearout_indicator?: number // SSD
total_lbas_written?: number // Both
}
const TEMP_THRESHOLDS = {
HDD: { safe: 45, warning: 55 },
SSD: { safe: 55, warning: 65 },
NVMe: { safe: 60, warning: 70 },
}
const getTempStatus = (temp: number, diskType: string): "safe" | "warning" | "critical" => {
const thresholds = TEMP_THRESHOLDS[diskType as keyof typeof TEMP_THRESHOLDS] || TEMP_THRESHOLDS.HDD
if (temp <= thresholds.safe) return "safe"
if (temp <= thresholds.warning) return "warning"
return "critical"
}
const getTempColor = (status: "safe" | "warning" | "critical"): string => {
switch (status) {
case "safe":
return "text-green-500"
case "warning":
return "text-yellow-500"
case "critical":
return "text-red-500"
default:
return "text-muted-foreground"
}
}
const getDiskTypeBadgeColor = (diskType: string): string => {
switch (diskType) {
case "HDD":
return "bg-blue-500/10 text-blue-500 border-blue-500/20"
case "SSD":
return "bg-purple-500/10 text-purple-500 border-purple-500/20"
case "NVMe":
return "bg-orange-500/10 text-orange-500 border-orange-500/20"
default:
return "bg-gray-500/10 text-gray-500 border-gray-500/20"
}
}
const getWearStatus = (lifeLeft: number): { status: string; color: string } => {
if (lifeLeft >= 80) return { status: "Excellent", color: "text-green-500" }
if (lifeLeft >= 50) return { status: "Good", color: "text-yellow-500" }
if (lifeLeft >= 20) return { status: "Fair", color: "text-orange-500" }
return { status: "Poor", color: "text-red-500" }
} }
const fetchStorageData = async (): Promise<StorageData | null> => { const fetchStorageData = async (): Promise<StorageData | null> => {
try { try {
console.log("[v0] Fetching storage data from Flask server...")
const response = await fetch("/api/storage", { const response = await fetch("/api/storage", {
method: "GET", method: "GET",
headers: { headers: {
@@ -112,9 +41,10 @@ const fetchStorageData = async (): Promise<StorageData | null> => {
} }
const data = await response.json() const data = await response.json()
console.log("[v0] Successfully fetched storage data from Flask:", data)
return data return data
} catch (error) { } catch (error) {
console.error("Failed to fetch storage data from Flask server:", error) console.error("[v0] Failed to fetch storage data from Flask server:", error)
return null return null
} }
} }
@@ -123,9 +53,6 @@ export function StorageMetrics() {
const [storageData, setStorageData] = useState<StorageData | null>(null) const [storageData, setStorageData] = useState<StorageData | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [selectedDisk, setSelectedDisk] = useState<DiskInfo | null>(null)
const [showDiskDetails, setShowDiskDetails] = useState(false)
const [showTempInfo, setShowTempInfo] = useState(false)
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
@@ -179,26 +106,6 @@ export function StorageMetrics() {
const usagePercent = storageData.total > 0 ? (storageData.used / storageData.total) * 100 : 0 const usagePercent = storageData.total > 0 ? (storageData.used / storageData.total) * 100 : 0
const disksByType = storageData.disks.reduce(
(acc, disk) => {
const type = disk.disk_type || "Unknown"
if (!acc[type]) {
acc[type] = []
}
acc[type].push(disk)
return acc
},
{} as Record<string, DiskInfo[]>,
)
const tempByType = Object.entries(disksByType)
.map(([type, disks]) => {
const avgTemp = disks.reduce((sum, disk) => sum + disk.temperature, 0) / disks.length
const status = getTempStatus(avgTemp, type)
return { type, avgTemp: Math.round(avgTemp), status, count: disks.length }
})
.filter((item) => item.type !== "Unknown")
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Storage Overview Cards */} {/* Storage Overview Cards */}
@@ -266,54 +173,6 @@ export function StorageMetrics() {
</Card> </Card>
</div> </div>
{/* Temperature cards by disk type */}
{tempByType.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{tempByType.map(({ type, avgTemp, status, count }) => {
return (
<Card key={type} className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center justify-between">
<div className="flex items-center">
<Thermometer className="h-5 w-5 mr-2" />
Avg Temperature
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className={getDiskTypeBadgeColor(type)}>
{type}
</Badge>
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setShowTempInfo(true)}>
<Info className="h-4 w-4" />
</Button>
</div>
</CardTitle>
</CardHeader>
<CardContent>
<div className={`text-3xl font-bold ${getTempColor(status)}`}>{avgTemp}°C</div>
<p className="text-xs text-muted-foreground mt-2">
{count} {type} disk{count > 1 ? "s" : ""}
</p>
<div className="mt-3">
<Badge
variant="outline"
className={
status === "safe"
? "bg-green-500/10 text-green-500 border-green-500/20"
: status === "warning"
? "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
: "bg-red-500/10 text-red-500 border-red-500/20"
}
>
{status === "safe" ? "Optimal" : status === "warning" ? "Warning" : "Critical"}
</Badge>
</div>
</CardContent>
</Card>
)
})}
</div>
) : null}
{/* Disk Details */} {/* Disk Details */}
<Card className="bg-card border-border"> <Card className="bg-card border-border">
<CardHeader> <CardHeader>
@@ -324,326 +183,55 @@ export function StorageMetrics() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <div className="space-y-4">
{storageData.disks.map((disk, index) => { {storageData.disks.map((disk, index) => (
const diskType = disk.disk_type || "HDD" <div
const tempStatus = getTempStatus(disk.temperature, diskType) key={index}
className="flex items-center justify-between p-4 rounded-lg border border-border bg-card/50"
let lifeLeft: number | null = null >
let wearLabel = "" <div className="flex items-center space-x-4">
<HardDrive className="h-5 w-5 text-muted-foreground" />
if (diskType === "NVMe" && disk.percentage_used !== undefined && disk.percentage_used !== null) { <div>
lifeLeft = 100 - disk.percentage_used <div className="font-medium text-foreground">{disk.name}</div>
wearLabel = "Life Left" <div className="text-sm text-muted-foreground">
} else if (diskType === "SSD") { {disk.fstype} {disk.mountpoint}
if (disk.ssd_life_left !== undefined && disk.ssd_life_left !== null) {
lifeLeft = disk.ssd_life_left
wearLabel = "Life Left"
} else if (disk.media_wearout_indicator !== undefined && disk.media_wearout_indicator !== null) {
lifeLeft = disk.media_wearout_indicator
wearLabel = "Health"
} else if (disk.wear_leveling_count !== undefined && disk.wear_leveling_count !== null) {
lifeLeft = disk.wear_leveling_count
wearLabel = "Wear Level"
}
}
return (
<div
key={index}
className="flex items-center justify-between p-4 rounded-lg border border-border bg-card/50 cursor-pointer hover:bg-card/80 transition-colors"
onClick={() => {
setSelectedDisk(disk)
setShowDiskDetails(true)
}}
>
<div className="flex items-center space-x-4">
<HardDrive className="h-5 w-5 text-muted-foreground" />
<div>
<div className="font-medium text-foreground flex items-center gap-2">
{disk.name}
{disk.disk_type && (
<Badge variant="outline" className={getDiskTypeBadgeColor(disk.disk_type)}>
{disk.disk_type}
</Badge>
)}
</div>
<div className="text-sm text-muted-foreground">
{disk.fstype} {disk.mountpoint}
</div>
</div> </div>
</div> </div>
<div className="flex items-center space-x-6">
<div className="text-right">
<div className="text-sm font-medium text-foreground">
{disk.used.toFixed(1)} GB / {disk.total.toFixed(1)} GB
</div>
<Progress value={disk.usage_percent} className="w-24 mt-1" />
</div>
<div className="text-center">
<div className="text-sm text-muted-foreground">Temp</div>
<div className={`text-sm font-medium ${getTempColor(tempStatus)}`}>{disk.temperature}°C</div>
</div>
{lifeLeft !== null && (diskType === "SSD" || diskType === "NVMe") && (
<div className="text-center">
<div className="text-sm text-muted-foreground">{wearLabel}</div>
<div className={`text-sm font-medium ${getWearStatus(lifeLeft).color}`}>
{lifeLeft.toFixed(0)}%
</div>
</div>
)}
<Badge
variant="outline"
className={
disk.health === "healthy"
? "bg-green-500/10 text-green-500 border-green-500/20"
: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
}
>
{disk.health === "healthy" ? (
<CheckCircle className="h-3 w-3 mr-1" />
) : (
<AlertTriangle className="h-3 w-3 mr-1" />
)}
{disk.health}
</Badge>
</div>
</div> </div>
)
})} <div className="flex items-center space-x-6">
<div className="text-right">
<div className="text-sm font-medium text-foreground">
{disk.used.toFixed(1)} GB / {disk.total.toFixed(1)} GB
</div>
<Progress value={disk.usage_percent} className="w-24 mt-1" />
</div>
<div className="text-center">
<div className="text-sm text-muted-foreground">Temp</div>
<div className="text-sm font-medium text-foreground">{disk.temperature}°C</div>
</div>
<Badge
variant="outline"
className={
disk.health === "healthy"
? "bg-green-500/10 text-green-500 border-green-500/20"
: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
}
>
{disk.health === "healthy" ? (
<CheckCircle className="h-3 w-3 mr-1" />
) : (
<AlertTriangle className="h-3 w-3 mr-1" />
)}
{disk.health}
</Badge>
</div>
</div>
))}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Dialog open={showTempInfo} onOpenChange={setShowTempInfo}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Temperature Thresholds by Disk Type</DialogTitle>
<DialogDescription>
Recommended operating temperature ranges for different storage devices
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="border-b border-border">
<th className="text-left p-3 font-semibold">Disk Type</th>
<th className="text-left p-3 font-semibold">Safe Zone</th>
<th className="text-left p-3 font-semibold">Warning Zone</th>
<th className="text-left p-3 font-semibold">Critical Zone</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-border">
<td className="p-3">
<Badge variant="outline" className={getDiskTypeBadgeColor("HDD")}>
HDD
</Badge>
</td>
<td className="p-3 text-green-500"> 45°C</td>
<td className="p-3 text-yellow-500">46 55°C</td>
<td className="p-3 text-red-500">&gt; 55°C</td>
</tr>
<tr className="border-b border-border">
<td className="p-3">
<Badge variant="outline" className={getDiskTypeBadgeColor("SSD")}>
SSD
</Badge>
</td>
<td className="p-3 text-green-500"> 55°C</td>
<td className="p-3 text-yellow-500">56 65°C</td>
<td className="p-3 text-red-500">&gt; 65°C</td>
</tr>
<tr>
<td className="p-3">
<Badge variant="outline" className={getDiskTypeBadgeColor("NVMe")}>
NVMe
</Badge>
</td>
<td className="p-3 text-green-500"> 60°C</td>
<td className="p-3 text-yellow-500">61 70°C</td>
<td className="p-3 text-red-500">&gt; 70°C</td>
</tr>
</tbody>
</table>
</div>
<p className="text-sm text-muted-foreground">
These thresholds are based on industry standards and manufacturer recommendations. Operating within the
safe zone ensures optimal performance and longevity.
</p>
</div>
</DialogContent>
</Dialog>
<Dialog open={showDiskDetails} onOpenChange={setShowDiskDetails}>
<DialogContent className="max-w-3xl">
{selectedDisk && (
<>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<HardDrive className="h-5 w-5" />
Disk Details: {selectedDisk.name}
</DialogTitle>
<DialogDescription>Complete SMART information and health status</DialogDescription>
</DialogHeader>
<div className="space-y-6">
{/* Basic Info */}
<div className="grid grid-cols-2 gap-4">
<div>
<div className="text-sm text-muted-foreground">Model</div>
<div className="font-medium">{selectedDisk.model || "Unknown"}</div>
</div>
<div>
<div className="text-sm text-muted-foreground">Serial Number</div>
<div className="font-medium">{selectedDisk.serial || "Unknown"}</div>
</div>
<div>
<div className="text-sm text-muted-foreground">Capacity</div>
<div className="font-medium">{selectedDisk.total.toFixed(1)}G</div>
</div>
<div>
<div className="text-sm text-muted-foreground">Health Status</div>
<Badge
variant="outline"
className={
selectedDisk.health === "healthy"
? "bg-green-500/10 text-green-500 border-green-500/20"
: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
}
>
{selectedDisk.health === "healthy" ? "Healthy" : "Warning"}
</Badge>
</div>
</div>
{(selectedDisk.disk_type === "SSD" || selectedDisk.disk_type === "NVMe") && (
<div className="border-t border-border pt-4">
<h3 className="font-semibold mb-3">Wear & Life Indicators</h3>
<div className="grid grid-cols-2 gap-4">
{selectedDisk.disk_type === "NVMe" &&
selectedDisk.percentage_used !== undefined &&
selectedDisk.percentage_used !== null && (
<>
<div>
<div className="text-sm text-muted-foreground">Percentage Used</div>
<div
className={`text-lg font-bold ${getWearStatus(100 - selectedDisk.percentage_used).color}`}
>
{selectedDisk.percentage_used}%
</div>
</div>
<div>
<div className="text-sm text-muted-foreground">Life Remaining</div>
<div
className={`text-lg font-bold ${getWearStatus(100 - selectedDisk.percentage_used).color}`}
>
{(100 - selectedDisk.percentage_used).toFixed(0)}%
</div>
<Progress value={100 - selectedDisk.percentage_used} className="mt-2" />
</div>
</>
)}
{selectedDisk.disk_type === "SSD" && (
<>
{selectedDisk.ssd_life_left !== undefined && selectedDisk.ssd_life_left !== null && (
<div>
<div className="text-sm text-muted-foreground">SSD Life Left</div>
<div className={`text-lg font-bold ${getWearStatus(selectedDisk.ssd_life_left).color}`}>
{selectedDisk.ssd_life_left}%
</div>
<Progress value={selectedDisk.ssd_life_left} className="mt-2" />
</div>
)}
{selectedDisk.wear_leveling_count !== undefined &&
selectedDisk.wear_leveling_count !== null && (
<div>
<div className="text-sm text-muted-foreground">Wear Leveling Count</div>
<div
className={`text-lg font-bold ${getWearStatus(selectedDisk.wear_leveling_count).color}`}
>
{selectedDisk.wear_leveling_count}
</div>
</div>
)}
{selectedDisk.media_wearout_indicator !== undefined &&
selectedDisk.media_wearout_indicator !== null && (
<div>
<div className="text-sm text-muted-foreground">Media Wearout Indicator</div>
<div
className={`text-lg font-bold ${getWearStatus(selectedDisk.media_wearout_indicator).color}`}
>
{selectedDisk.media_wearout_indicator}%
</div>
<Progress value={selectedDisk.media_wearout_indicator} className="mt-2" />
</div>
)}
</>
)}
{selectedDisk.total_lbas_written !== undefined && selectedDisk.total_lbas_written !== null && (
<div>
<div className="text-sm text-muted-foreground">Total Data Written</div>
<div className="font-medium">{(selectedDisk.total_lbas_written / 1000000).toFixed(2)} TB</div>
</div>
)}
</div>
</div>
)}
{/* SMART Attributes */}
<div className="border-t border-border pt-4">
<h3 className="font-semibold mb-3">SMART Attributes</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<div className="text-sm text-muted-foreground">Temperature</div>
<div
className={`font-medium ${getTempColor(getTempStatus(selectedDisk.temperature, selectedDisk.disk_type || "HDD"))}`}
>
{selectedDisk.temperature}°C
</div>
</div>
<div>
<div className="text-sm text-muted-foreground">Power On Hours</div>
<div className="font-medium">
{selectedDisk.power_on_hours
? `${selectedDisk.power_on_hours}h (${Math.floor(selectedDisk.power_on_hours / 24)}d)`
: "N/A"}
</div>
</div>
<div>
<div className="text-sm text-muted-foreground">Rotation Rate</div>
<div className="font-medium">{selectedDisk.disk_type || "Unknown"}</div>
</div>
<div>
<div className="text-sm text-muted-foreground">Power Cycles</div>
<div className="font-medium">{selectedDisk.power_cycles || 0}</div>
</div>
<div>
<div className="text-sm text-muted-foreground">SMART Status</div>
<div className="font-medium">{selectedDisk.smart_status === "passed" ? "Passed" : "Unknown"}</div>
</div>
<div>
<div className="text-sm text-muted-foreground">Reallocated Sectors</div>
<div className="font-medium">{selectedDisk.reallocated_sectors || 0}</div>
</div>
<div>
<div className="text-sm text-muted-foreground">Pending Sectors</div>
<div className="font-medium">{selectedDisk.pending_sectors || 0}</div>
</div>
<div>
<div className="text-sm text-muted-foreground">CRC Errors</div>
<div className="font-medium">{selectedDisk.crc_errors || 0}</div>
</div>
</div>
</div>
</div>
</>
)}
</DialogContent>
</Dialog>
</div> </div>
) )
} }

View File

@@ -536,14 +536,7 @@ def get_storage_info():
'pending_sectors': smart_data.get('pending_sectors', 0), 'pending_sectors': smart_data.get('pending_sectors', 0),
'crc_errors': smart_data.get('crc_errors', 0), 'crc_errors': smart_data.get('crc_errors', 0),
'rotation_rate': smart_data.get('rotation_rate', 0), # Added 'rotation_rate': smart_data.get('rotation_rate', 0), # Added
'power_cycles': smart_data.get('power_cycles', 0), # Added 'power_cycles': smart_data.get('power_cycles', 0) # Added
'disk_type': smart_data.get('disk_type', 'Unknown'), # Added from get_smart_data
# Added wear indicators
'percentage_used': smart_data.get('percentage_used'),
'ssd_life_left': smart_data.get('ssd_life_left'),
'wear_leveling_count': smart_data.get('wear_leveling_count'),
'media_wearout_indicator': smart_data.get('media_wearout_indicator'),
'total_lbas_written': smart_data.get('total_lbas_written'),
} }
storage_data['disk_count'] += 1 storage_data['disk_count'] += 1
@@ -662,20 +655,10 @@ def get_smart_data(disk_name):
'crc_errors': 0, 'crc_errors': 0,
'rotation_rate': 0, # Added rotation rate (RPM) 'rotation_rate': 0, # Added rotation rate (RPM)
'power_cycles': 0, # Added power cycle count 'power_cycles': 0, # Added power cycle count
'disk_type': 'Unknown', # Will be 'HDD', 'SSD', or 'NVMe'
'percentage_used': None, # NVMe specific
'ssd_life_left': None, # SSD specific (percentage remaining)
'wear_leveling_count': None, # SSD specific
'media_wearout_indicator': None, # SSD specific
'total_lbas_written': None, # Both SSD and NVMe
} }
print(f"[v0] ===== Starting SMART data collection for /dev/{disk_name} =====") print(f"[v0] ===== Starting SMART data collection for /dev/{disk_name} =====")
if 'nvme' in disk_name.lower():
smart_data['disk_type'] = 'NVMe'
print(f"[v0] Detected NVMe disk based on device name")
try: try:
commands_to_try = [ commands_to_try = [
['smartctl', '-a', '-j', f'/dev/{disk_name}'], # JSON output (preferred) ['smartctl', '-a', '-j', f'/dev/{disk_name}'], # JSON output (preferred)
@@ -738,15 +721,6 @@ def get_smart_data(disk_name):
smart_data['rotation_rate'] = data['rotation_rate'] smart_data['rotation_rate'] = data['rotation_rate']
print(f"[v0] Rotation Rate: {smart_data['rotation_rate']} RPM") print(f"[v0] Rotation Rate: {smart_data['rotation_rate']} RPM")
# Classify disk type based on rotation rate
if smart_data['disk_type'] == 'Unknown':
if data['rotation_rate'] == 0 or 'Solid State Device' in str(data.get('rotation_rate', '')):
smart_data['disk_type'] = 'SSD'
print(f"[v0] Detected SSD based on rotation rate")
elif isinstance(data['rotation_rate'], int) and data['rotation_rate'] > 0:
smart_data['disk_type'] = 'HDD'
print(f"[v0] Detected HDD based on rotation rate")
# Extract SMART status # Extract SMART status
if 'smart_status' in data and 'passed' in data['smart_status']: if 'smart_status' in data and 'passed' in data['smart_status']:
smart_data['smart_status'] = 'passed' if data['smart_status']['passed'] else 'failed' smart_data['smart_status'] = 'passed' if data['smart_status']['passed'] else 'failed'
@@ -764,7 +738,6 @@ def get_smart_data(disk_name):
for attr in data['ata_smart_attributes']['table']: for attr in data['ata_smart_attributes']['table']:
attr_id = attr.get('id') attr_id = attr.get('id')
raw_value = attr.get('raw', {}).get('value', 0) raw_value = attr.get('raw', {}).get('value', 0)
normalized_value = attr.get('value', 0)
if attr_id == 9: # Power_On_Hours if attr_id == 9: # Power_On_Hours
smart_data['power_on_hours'] = raw_value smart_data['power_on_hours'] = raw_value
@@ -789,22 +762,6 @@ def get_smart_data(disk_name):
elif attr_id == 199: # UDMA_CRC_Error_Count elif attr_id == 199: # UDMA_CRC_Error_Count
smart_data['crc_errors'] = raw_value smart_data['crc_errors'] = raw_value
print(f"[v0] CRC Errors (ID 199): {raw_value}") print(f"[v0] CRC Errors (ID 199): {raw_value}")
elif attr_id == 177: # Wear_Leveling_Count
smart_data['wear_leveling_count'] = normalized_value
print(f"[v0] Wear Leveling Count (ID 177): {normalized_value}")
elif attr_id == 231: # SSD_Life_Left or Temperature
if normalized_value <= 100: # Likely life left percentage
smart_data['ssd_life_left'] = normalized_value
print(f"[v0] SSD Life Left (ID 231): {normalized_value}%")
elif attr_id == 233: # Media_Wearout_Indicator
smart_data['media_wearout_indicator'] = normalized_value
print(f"[v0] Media Wearout Indicator (ID 233): {normalized_value}")
elif attr_id == 202: # Percent_Lifetime_Remain
smart_data['ssd_life_left'] = normalized_value
print(f"[v0] Percent Lifetime Remain (ID 202): {normalized_value}%")
elif attr_id == 241: # Total_LBAs_Written
smart_data['total_lbas_written'] = raw_value
print(f"[v0] Total LBAs Written (ID 241): {raw_value}")
# Parse NVMe SMART data # Parse NVMe SMART data
if 'nvme_smart_health_information_log' in data: if 'nvme_smart_health_information_log' in data:
@@ -819,12 +776,6 @@ def get_smart_data(disk_name):
if 'power_cycles' in nvme_data: if 'power_cycles' in nvme_data:
smart_data['power_cycles'] = nvme_data['power_cycles'] smart_data['power_cycles'] = nvme_data['power_cycles']
print(f"[v0] NVMe Power Cycles: {smart_data['power_cycles']}") print(f"[v0] NVMe Power Cycles: {smart_data['power_cycles']}")
if 'percentage_used' in nvme_data:
smart_data['percentage_used'] = nvme_data['percentage_used']
print(f"[v0] NVMe Percentage Used: {smart_data['percentage_used']}%")
if 'data_units_written' in nvme_data:
smart_data['total_lbas_written'] = nvme_data['data_units_written']
print(f"[v0] NVMe Data Units Written: {smart_data['total_lbas_written']}")
# If we got good data, break out of the loop # If we got good data, break out of the loop
if smart_data['model'] != 'Unknown' and smart_data['serial'] != 'Unknown': if smart_data['model'] != 'Unknown' and smart_data['serial'] != 'Unknown':
@@ -861,19 +812,11 @@ def get_smart_data(disk_name):
try: try:
smart_data['rotation_rate'] = int(rate_str.split()[0]) smart_data['rotation_rate'] = int(rate_str.split()[0])
print(f"[v0] Found rotation rate: {smart_data['rotation_rate']} RPM") print(f"[v0] Found rotation rate: {smart_data['rotation_rate']} RPM")
# Classify as HDD
if smart_data['disk_type'] == 'Unknown':
smart_data['disk_type'] = 'HDD'
print(f"[v0] Detected HDD based on rotation rate")
except (ValueError, IndexError): except (ValueError, IndexError):
pass pass
elif 'Solid State Device' in rate_str: elif 'Solid State Device' in rate_str:
smart_data['rotation_rate'] = 0 # SSD smart_data['rotation_rate'] = 0 # SSD
print(f"[v0] Found SSD (no rotation)") print(f"[v0] Found SSD (no rotation)")
# Classify as SSD
if smart_data['disk_type'] == 'Unknown':
smart_data['disk_type'] = 'SSD'
print(f"[v0] Detected SSD based on rotation rate")
# SMART status detection # SMART status detection
elif 'SMART overall-health self-assessment test result:' in line: elif 'SMART overall-health self-assessment test result:' in line:
@@ -961,6 +904,7 @@ def get_smart_data(disk_name):
break break
elif smart_data['model'] != 'Unknown' or smart_data['serial'] != 'Unknown': elif smart_data['model'] != 'Unknown' or smart_data['serial'] != 'Unknown':
print(f"[v0] Extracted partial data from text output, continuing to next attempt...") print(f"[v0] Extracted partial data from text output, continuing to next attempt...")
else: else:
print(f"[v0] No usable output (return code {result_code}), trying next command...") print(f"[v0] No usable output (return code {result_code}), trying next command...")
@@ -1403,7 +1347,7 @@ def get_network_info():
} }
def get_proxmox_vms(): def get_proxmox_vms():
"""Get Proxmox VM and LXC information using pvesh command - only from local node""" """Get Proxmox VM and LXC information (requires pvesh command) - only from local node"""
try: try:
all_vms = [] all_vms = []
@@ -1547,54 +1491,20 @@ def get_ipmi_power():
'power_meter': power_meter 'power_meter': power_meter
} }
#
def get_ups_info(): def get_ups_info():
"""Get UPS information from NUT (upsc) - supports both local and remote UPS""" """Get UPS information from NUT (upsc)"""
ups_list = [] ups_data = {}
try: try:
configured_ups = [] # First, list available UPS devices
try: result = subprocess.run(['upsc', '-l'], capture_output=True, text=True, timeout=5)
with open('/etc/nut/upsmon.conf', 'r') as f: if result.returncode == 0:
for line in f: ups_list = result.stdout.strip().split('\n')
line = line.strip() if ups_list and ups_list[0]:
# Parse MONITOR lines: MONITOR <upsname>@<hostname>:<port> <powervalue> <username> <password> <type> ups_name = ups_list[0]
if line.startswith('MONITOR') and not line.startswith('#'): print(f"[v0] Found UPS: {ups_name}")
parts = line.split()
if len(parts) >= 2:
ups_identifier = parts[1] # e.g., "apc@localhost" or "ups@192.168.1.10"
configured_ups.append(ups_identifier)
print(f"[v0] Found configured UPS in upsmon.conf: {ups_identifier}")
except FileNotFoundError:
print("[v0] /etc/nut/upsmon.conf not found, will try local detection only")
except Exception as e:
print(f"[v0] Error reading upsmon.conf: {e}")
all_ups_names = set(configured_ups) # Get detailed UPS info
# Also try to list local UPS devices
try:
result = subprocess.run(['upsc', '-l'], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
local_ups = result.stdout.strip().split('\n')
for ups in local_ups:
if ups:
all_ups_names.add(ups)
print(f"[v0] Found local UPS: {ups}")
except Exception as e:
print(f"[v0] Error listing local UPS: {e}")
for ups_name in all_ups_names:
if not ups_name:
continue
ups_data = {
'name': ups_name,
'raw_variables': {} # Store all raw variables for the modal
}
try:
print(f"[v0] Querying UPS: {ups_name}")
result = subprocess.run(['upsc', ups_name], capture_output=True, text=True, timeout=5) result = subprocess.run(['upsc', ups_name], 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'):
@@ -1603,220 +1513,69 @@ def get_ups_info():
key = key.strip() key = key.strip()
value = value.strip() value = value.strip()
# Store all raw variables # Map common UPS variables
ups_data['raw_variables'][key] = value
# Device Information
if key == 'device.model': if key == 'device.model':
ups_data['model'] = value ups_data['model'] = value
elif key == 'device.mfr':
ups_data['manufacturer'] = value
elif key == 'device.serial':
ups_data['serial'] = value
elif key == 'device.type':
ups_data['device_type'] = value
# Status
elif key == 'ups.status': elif key == 'ups.status':
ups_data['status'] = value ups_data['status'] = value
elif key == 'ups.beeper.status':
ups_data['beeper_status'] = value
elif key == 'ups.test.result':
ups_data['test_result'] = value
# Battery
elif key == 'battery.charge': elif key == 'battery.charge':
ups_data['battery_charge'] = f"{value}%" ups_data['battery_charge'] = f"{value}%"
ups_data['battery_charge_raw'] = float(value)
elif key == 'battery.charge.low':
ups_data['battery_charge_low'] = f"{value}%"
elif key == 'battery.runtime': elif key == 'battery.runtime':
# Convert seconds to minutes
try: try:
runtime_sec = int(value) runtime_sec = int(value)
runtime_min = runtime_sec // 60 runtime_min = runtime_sec // 60
ups_data['time_left'] = f"{runtime_min} minutes" ups_data['time_left'] = f"{runtime_min} minutes"
ups_data['battery_runtime_seconds'] = runtime_sec
except ValueError: except ValueError:
ups_data['time_left'] = value ups_data['time_left'] = value
elif key == 'battery.runtime.low':
ups_data['battery_runtime_low'] = f"{value}s"
elif key == 'battery.voltage':
ups_data['battery_voltage'] = f"{value}V"
elif key == 'battery.voltage.nominal':
ups_data['battery_voltage_nominal'] = f"{value}V"
elif key == 'battery.type':
ups_data['battery_type'] = value
elif key == 'battery.mfr.date':
ups_data['battery_mfr_date'] = value
# Power
elif key == 'ups.load': elif key == 'ups.load':
ups_data['load_percent'] = f"{value}%" ups_data['load_percent'] = f"{value}%"
ups_data['load_raw'] = float(value)
elif key == 'ups.realpower':
ups_data['real_power'] = f"{value}W"
elif key == 'ups.realpower.nominal':
ups_data['realpower_nominal'] = f"{value}W"
elif key == 'ups.power':
ups_data['apparent_power'] = f"{value}VA"
elif key == 'ups.power.nominal':
ups_data['power_nominal'] = f"{value}VA"
# Input
elif key == 'input.voltage': elif key == 'input.voltage':
ups_data['line_voltage'] = f"{value}V" ups_data['line_voltage'] = f"{value}V"
ups_data['input_voltage'] = f"{value}V" elif key == 'ups.realpower':
elif key == 'input.voltage.nominal': ups_data['real_power'] = f"{value}W"
ups_data['input_voltage_nominal'] = f"{value}V"
elif key == 'input.frequency':
ups_data['input_frequency'] = f"{value}Hz"
elif key == 'input.transfer.reason':
ups_data['transfer_reason'] = value
elif key == 'input.transfer.high':
ups_data['input_transfer_high'] = f"{value}V"
elif key == 'input.transfer.low':
ups_data['input_transfer_low'] = f"{value}V"
# Output
elif key == 'output.voltage':
ups_data['output_voltage'] = f"{value}V"
elif key == 'output.voltage.nominal':
ups_data['output_voltage_nominal'] = f"{value}V"
elif key == 'output.frequency':
ups_data['output_frequency'] = f"{value}Hz"
# Driver
elif key == 'driver.name':
ups_data['driver_name'] = value
elif key == 'driver.version':
ups_data['driver_version'] = value
elif key == 'driver.version.internal':
ups_data['driver_version_internal'] = value
elif key == 'driver.parameter.pollfreq':
ups_data['driver_poll_freq'] = value
elif key == 'driver.parameter.pollinterval':
ups_data['driver_poll_interval'] = value
# Firmware
elif key == 'ups.firmware':
ups_data['firmware'] = value
elif key == 'ups.mfr':
ups_data['ups_manufacturer'] = value
elif key == 'ups.mfr.date':
ups_data['ups_mfr_date'] = value
elif key == 'ups.productid':
ups_data['product_id'] = value
elif key == 'ups.vendorid':
ups_data['vendor_id'] = value
# Timers
elif key == 'ups.delay.shutdown':
ups_data['delay_shutdown'] = f"{value}s"
elif key == 'ups.delay.start':
ups_data['delay_start'] = f"{value}s"
elif key == 'ups.timer.shutdown':
ups_data['timer_shutdown'] = f"{value}s"
elif key == 'ups.timer.reboot':
ups_data['timer_reboot'] = f"{value}s"
ups_list.append(ups_data)
print(f"[v0] Successfully queried UPS: {ups_name}")
except subprocess.TimeoutExpired:
print(f"[v0] Timeout querying UPS: {ups_name}")
except Exception as e:
print(f"[v0] Error querying UPS {ups_name}: {e}")
print(f"[v0] UPS data: {ups_data}")
except FileNotFoundError: except FileNotFoundError:
print("[v0] upsc command not found - NUT client not installed") print("[v0] upsc not found")
except Exception as e: except Exception as e:
print(f"[v0] Error in get_ups_info: {e}") print(f"[v0] Error getting UPS info: {e}")
# Return first UPS for backward compatibility, or None if no UPS found return ups_data
return ups_list[0] if ups_list else None
# </CHANGE> def identify_temperature_sensor(sensor_name, adapter):
"""Identify what a temperature sensor corresponds to"""
sensor_lower = sensor_name.lower()
adapter_lower = adapter.lower() if adapter else ""
# Moved helper functions for system info up # CPU/Package temperatures
# def get_system_info(): ... (moved up) if "package" in sensor_lower or "tctl" in sensor_lower or "tccd" in sensor_lower:
return "CPU Package"
if "core" in sensor_lower:
core_num = re.search(r'(\d+)', sensor_name)
return f"CPU Core {core_num.group(1)}" if core_num else "CPU Core"
# New function for identifying GPU types # Motherboard/Chipset
def identify_gpu_type(gpu_name, vendor): if "temp1" in sensor_lower and ("isa" in adapter_lower or "acpi" in adapter_lower):
""" return "Motherboard/Chipset"
Identify GPU type with more granular classification: if "pch" in sensor_lower or "chipset" in sensor_lower:
- Integrated: GPUs integrated in CPU/chipset (Intel HD/UHD, AMD APU) return "Chipset"
- PCI - BMC: Management GPUs (Matrox G200, ASPEED)
- PCI - Professional: Professional GPUs (Quadro, FirePro, Radeon Pro)
- PCI - Gaming: Gaming GPUs (GeForce, Radeon RX)
- PCI - Compute: Compute GPUs (Tesla, Instinct)
- PCI - Discrete: Generic discrete GPU (fallback)
"""
gpu_name_lower = gpu_name.lower()
# Check for BMC/Management GPUs first (these are PCI but for management) # Storage (NVMe, SATA)
bmc_keywords = ['g200', 'mga g200', 'ast1', 'ast2', 'aspeed'] if "nvme" in sensor_lower or "composite" in sensor_lower:
for keyword in bmc_keywords: return "NVMe SSD"
if keyword in gpu_name_lower: if "sata" in sensor_lower or "ata" in sensor_lower:
return 'PCI - BMC' return "SATA Drive"
# Check for truly integrated GPUs (in CPU/chipset) # GPU
integrated_keywords = [ if any(gpu in adapter_lower for gpu in ["nouveau", "amdgpu", "radeon", "i915"]):
'hd graphics', 'uhd graphics', 'iris graphics', 'iris xe graphics', return "GPU"
'radeon vega', 'radeon graphics', # AMD APU integrated
'tegra', # NVIDIA Tegra (rare)
]
for keyword in integrated_keywords:
if keyword in gpu_name_lower:
# Make sure it's not Arc (which is discrete)
if 'arc' not in gpu_name_lower:
return 'Integrated'
# Check for Professional GPUs # Network adapters
professional_keywords = ['quadro', 'firepro', 'radeon pro', 'firegl'] if "pci" in adapter_lower and "temp" in sensor_lower:
for keyword in professional_keywords: return "PCI Device"
if keyword in gpu_name_lower:
return 'PCI - Professional'
# Check for Compute GPUs return sensor_name
compute_keywords = ['tesla', 'instinct', 'mi100', 'mi200', 'mi300']
for keyword in compute_keywords:
if keyword in gpu_name_lower:
return 'PCI - Compute'
# Check for Gaming GPUs
gaming_keywords = [
'geforce', 'rtx', 'gtx', 'titan', # NVIDIA gaming
'radeon rx', 'radeon r9', 'radeon r7', 'radeon r5', 'radeon vii', # AMD gaming
'arc a' # Intel Arc gaming
]
for keyword in gaming_keywords:
if keyword in gpu_name_lower:
return 'PCI - Gaming'
# Fallback logic based on vendor
if vendor == 'Intel':
# Intel Arc is discrete gaming, everything else is typically integrated
if 'arc' in gpu_name_lower:
return 'PCI - Gaming'
else:
return 'Integrated'
elif vendor == 'NVIDIA':
# Most NVIDIA GPUs are discrete
return 'PCI - Discrete'
elif vendor == 'AMD':
# AMD APUs are integrated, others are discrete
if 'ryzen' in gpu_name_lower or 'athlon' in gpu_name_lower:
return 'Integrated'
return 'PCI - Discrete'
elif vendor == 'Matrox':
# Matrox G200 series are BMC
return 'Integrated'
elif vendor == 'ASPEED':
# ASPEED AST series are BMC
return 'Integrated'
# Default to PCI - Discrete if we can't determine
return 'PCI - Discrete'
def get_temperature_info(): def get_temperature_info():
"""Get detailed temperature information from sensors command""" """Get detailed temperature information from sensors command"""
@@ -1875,12 +1634,11 @@ def get_temperature_info():
high_value = float(high_match.group(1)) if high_match else 0 high_value = float(high_match.group(1)) if high_match else 0
crit_value = float(crit_match.group(1)) if crit_match else 0 crit_value = float(crit_match.group(1)) if crit_match else 0
sensor_info = identify_temperature_sensor(sensor_name, current_adapter) identified_name = identify_temperature_sensor(sensor_name, current_adapter)
temperatures.append({ temperatures.append({
'name': sensor_info['display_name'], 'name': identified_name,
'original_name': sensor_name, 'original_name': sensor_name,
'category': sensor_info['category'],
'current': temp_value, 'current': temp_value,
'high': high_value, 'high': high_value,
'critical': crit_value, 'critical': crit_value,
@@ -2797,6 +2555,7 @@ def get_detailed_gpu_info(gpu):
print(f"[v0] ===== Exiting get_detailed_gpu_info for GPU {slot} =====", flush=True) print(f"[v0] ===== Exiting get_detailed_gpu_info for GPU {slot} =====", flush=True)
return detailed_info return detailed_info
def get_pci_device_info(pci_slot): def get_pci_device_info(pci_slot):
"""Get detailed PCI device information for a given slot""" """Get detailed PCI device information for a given slot"""
pci_info = {} pci_info = {}
@@ -2896,6 +2655,111 @@ def get_network_hardware_info(pci_slot):
return net_info return net_info
def get_gpu_info():
"""Detect and return information about GPUs in the system"""
gpus = []
try:
result = subprocess.run(['lspci'], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
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']):
parts = line.split(' ', 1)
if len(parts) >= 2:
slot = parts[0].strip()
remaining = parts[1]
if ':' in remaining:
class_and_name = remaining.split(':', 1)
gpu_name = class_and_name[1].strip() if len(class_and_name) > 1 else remaining.strip()
else:
gpu_name = remaining.strip()
# Determine vendor
vendor = 'Unknown'
if 'NVIDIA' in gpu_name or 'nVidia' in gpu_name:
vendor = 'NVIDIA'
elif 'AMD' in gpu_name or 'ATI' in gpu_name or 'Radeon' in gpu_name:
vendor = 'AMD'
elif 'Intel' in gpu_name:
vendor = 'Intel'
gpu = {
'slot': slot,
'name': gpu_name,
'vendor': vendor,
'type': 'Discrete' if vendor in ['NVIDIA', 'AMD'] else 'Integrated'
}
pci_info = get_pci_device_info(slot)
if pci_info:
gpu['pci_class'] = pci_info.get('class', '')
gpu['pci_driver'] = pci_info.get('driver', '')
gpu['pci_kernel_module'] = pci_info.get('kernel_module', '')
# detailed_info = get_detailed_gpu_info(gpu) # Removed this call here
# gpu.update(detailed_info) # It will be called later in api_gpu_realtime
gpus.append(gpu)
print(f"[v0] Found GPU: {gpu_name} ({vendor}) at slot {slot}")
except Exception as e:
print(f"[v0] Error detecting GPUs from lspci: {e}")
try:
result = subprocess.run(['sensors'], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
current_adapter = 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
# Look for GPU-related sensors (nouveau, amdgpu, radeon, i915)
if ':' in line and not line.startswith(' '):
parts = line.split(':', 1)
sensor_name = parts[0].strip()
value_part = parts[1].strip()
# Check if this is a GPU sensor
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 (only if not already set by nvidia-smi)
if 'temperature' not in gpu or gpu['temperature'] is None:
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)
if rpm_match:
gpu['fan_speed'] = int(float(rpm_match.group(1)))
gpu['fan_unit'] = 'RPM'
print(f"[v0] GPU {gpu['name']}: Fan = {gpu['fan_speed']} RPM")
except Exception as e:
print(f"[v0] Error enriching GPU data from sensors: {e}")
return gpus
def get_disk_hardware_info(disk_name): def get_disk_hardware_info(disk_name):
"""Get detailed hardware information for a disk""" """Get detailed hardware information for a disk"""
@@ -3310,16 +3174,15 @@ def get_hardware_info():
# Use identify_temperature_sensor to make names more user-friendly # Use identify_temperature_sensor to make names more user-friendly
identified_name = identify_temperature_sensor(entry.label if entry.label else sensor_name, sensor_name) identified_name = identify_temperature_sensor(entry.label if entry.label else sensor_name, sensor_name)
temperatures.append({ hardware_data['sensors']['temperatures'].append({
'name': identified_name['display_name'], 'name': identified_name,
'original_name': entry.label if entry.label else sensor_name, 'original_name': entry.label if entry.label else sensor_name,
'category': identified_name['category'],
'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(temperatures)} found") print(f"[v0] Temperature sensors: {len(hardware_data['sensors']['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)
@@ -3556,7 +3419,9 @@ def api_logs():
'level': level, 'level': level,
'service': log_entry.get('_SYSTEMD_UNIT', log_entry.get('SYSLOG_IDENTIFIER', 'system')), 'service': log_entry.get('_SYSTEMD_UNIT', log_entry.get('SYSLOG_IDENTIFIER', 'system')),
'message': log_entry.get('MESSAGE', ''), 'message': log_entry.get('MESSAGE', ''),
'source': 'journal' 'source': 'journalctl',
'pid': log_entry.get('_PID', ''),
'hostname': log_entry.get('_HOSTNAME', '')
}) })
except (json.JSONDecodeError, ValueError) as e: except (json.JSONDecodeError, ValueError) as e:
continue continue
@@ -3821,7 +3686,7 @@ def api_backups():
try: try:
# Get content of storage # Get content of storage
content_result = subprocess.run( content_result = subprocess.run(
['pvesh', 'get', f'/nodes/{storage.get("node", "localhost")}/storage/{storage_id}/content', '--output-format', 'json'], ['pvesh', 'get', f'/nodes/localhost/storage/{storage_id}/content', '--output-format', 'json'],
capture_output=True, text=True, timeout=10) capture_output=True, text=True, timeout=10)
if content_result.returncode == 0: if content_result.returncode == 0:
@@ -3861,7 +3726,7 @@ def api_backups():
'timestamp': ctime 'timestamp': ctime
}) })
except Exception as e: except Exception as e:
print(f"Error getting content for storage {storage_id} on node {storage.get('node', 'localhost')}: {e}") print(f"Error getting content for storage {storage_id}: {e}")
continue continue
except Exception as e: except Exception as e:
print(f"Error getting storage list: {e}") print(f"Error getting storage list: {e}")
@@ -4202,7 +4067,7 @@ def api_prometheus():
# GPU metrics # GPU metrics
pci_devices = hardware_info.get('pci_devices', []) pci_devices = hardware_info.get('pci_devices', [])
for device in pci_devices: for device in pci_devices:
if device.get('type') == 'Graphics Card': # Changed from 'GPU' to 'Graphics Card' to match pci_devices categorization if device.get('type') == 'GPU':
gpu_name = device.get('device', 'unknown').replace(' ', '_') gpu_name = device.get('device', 'unknown').replace(' ', '_')
gpu_vendor = device.get('vendor', 'unknown') gpu_vendor = device.get('vendor', 'unknown')
@@ -4277,22 +4142,27 @@ def api_prometheus():
if ups.get('battery_charge') is not None: if ups.get('battery_charge') is not None:
metrics.append(f'# HELP proxmox_ups_battery_charge_percent UPS battery charge percentage') metrics.append(f'# HELP proxmox_ups_battery_charge_percent UPS battery charge percentage')
metrics.append(f'# TYPE proxmox_ups_battery_charge_percent gauge') metrics.append(f'# TYPE proxmox_ups_battery_charge_percent gauge')
metrics.append(f'proxmox_ups_battery_charge_percent{{node="{node}",ups="{ups_name}"}} {ups["battery_charge_raw"]} {timestamp}') # Use raw value for metric metrics.append(f'proxmox_ups_battery_charge_percent{{node="{node}",ups="{ups_name}"}} {ups["battery_charge"]} {timestamp}')
if ups.get('load_raw') is not None: # Changed from 'load' to 'load_percent' if ups.get('load') is not None:
metrics.append(f'# HELP proxmox_ups_load_percent UPS load percentage') metrics.append(f'# HELP proxmox_ups_load_percent UPS load percentage')
metrics.append(f'# TYPE proxmox_ups_load_percent gauge') metrics.append(f'# TYPE proxmox_ups_load_percent gauge')
metrics.append(f'proxmox_ups_load_percent{{node="{node}",ups="{ups_name}"}} {ups["load_raw"]} {timestamp}') metrics.append(f'proxmox_ups_load_percent{{node="{node}",ups="{ups_name}"}} {ups["load"]} {timestamp}')
if ups.get('battery_runtime_seconds') is not None: # Use seconds for metric if ups.get('runtime'):
# Convert runtime to seconds
runtime_str = ups['runtime']
runtime_seconds = 0
if 'minutes' in runtime_str:
runtime_seconds = int(runtime_str.split()[0]) * 60
metrics.append(f'# HELP proxmox_ups_runtime_seconds UPS runtime in seconds') metrics.append(f'# HELP proxmox_ups_runtime_seconds UPS runtime in seconds')
metrics.append(f'# TYPE proxmox_ups_runtime_seconds gauge') metrics.append(f'# TYPE proxmox_ups_runtime_seconds gauge')
metrics.append(f'proxmox_ups_runtime_seconds{{node="{node}",ups="{ups_name}"}} {ups["battery_runtime_seconds"]} {timestamp}') metrics.append(f'proxmox_ups_runtime_seconds{{node="{node}",ups="{ups_name}"}} {runtime_seconds} {timestamp}')
if ups.get('input_voltage') is not None: if ups.get('input_voltage') is not None:
metrics.append(f'# HELP proxmox_ups_input_voltage_volts UPS input voltage in volts') metrics.append(f'# HELP proxmox_ups_input_voltage_volts UPS input voltage in volts')
metrics.append(f'# TYPE proxmox_ups_input_voltage_volts gauge') metrics.append(f'# TYPE proxmox_ups_input_voltage_volts gauge')
metrics.append(f'proxmox_ups_input_voltage_volts{{node="{node}",ups="{ups_name}"}} {float(ups["input_voltage"].replace("V", ""))} {timestamp}') # Extract numeric value metrics.append(f'proxmox_ups_input_voltage_volts{{node="{node}",ups="{ups_name}"}} {ups["input_voltage"]} {timestamp}')
except Exception as e: except Exception as e:
print(f"[v0] Error getting hardware metrics for Prometheus: {e}") print(f"[v0] Error getting hardware metrics for Prometheus: {e}")
@@ -4410,7 +4280,7 @@ def api_hardware():
print(f"[v0] /api/hardware returning data") print(f"[v0] /api/hardware returning data")
print(f"[v0] - CPU: {formatted_data['cpu'].get('model', 'Unknown')}") print(f"[v0] - CPU: {formatted_data['cpu'].get('model', 'Unknown')}")
print(f"[v0] - Temperatures: {len(formatted_data['temperatures'])} sensors") print(f"[v0] - Temperatures: {len(formatted_data['temperatures'])} sensors")
print(f"[v0] - Fans: {len(formatted_data['fans'])} fans") # Includes IPMI fans print(f"[v0] - Fans: {len(formatted_data['fans'])} fans") # Now includes IPMI fans
print(f"[v0] - Power supplies: {len(formatted_data['power_supplies'])} PSUs") print(f"[v0] - Power supplies: {len(formatted_data['power_supplies'])} PSUs")
print(f"[v0] - Power meter: {'Yes' if formatted_data['power_meter'] else 'No'}") print(f"[v0] - Power meter: {'Yes' if formatted_data['power_meter'] else 'No'}")
print(f"[v0] - UPS: {'Yes' if formatted_data['ups'] else 'No'}") print(f"[v0] - UPS: {'Yes' if formatted_data['ups'] else 'No'}")

View File

@@ -77,53 +77,11 @@ export interface PowerSupply {
export interface UPS { export interface UPS {
name: string name: string
status: string status: string
model?: string battery_charge?: number
manufacturer?: string battery_runtime?: number
serial?: string load?: number
device_type?: string input_voltage?: number
firmware?: string output_voltage?: number
battery_charge?: string
battery_charge_raw?: number
battery_charge_low?: string
battery_runtime_seconds?: number
battery_runtime_low?: string
battery_voltage?: string
battery_voltage_nominal?: string
battery_type?: string
battery_mfr_date?: string
time_left?: string
load_percent?: string
load_raw?: number
real_power?: string
realpower_nominal?: string
apparent_power?: string
power_nominal?: string
input_voltage?: string
input_voltage_nominal?: string
input_frequency?: string
input_transfer_high?: string
input_transfer_low?: string
transfer_reason?: string
line_voltage?: string
output_voltage?: string
output_voltage_nominal?: string
output_frequency?: string
driver_name?: string
driver_version?: string
driver_version_internal?: string
driver_poll_freq?: string
driver_poll_interval?: string
ups_manufacturer?: string
ups_mfr_date?: string
product_id?: string
vendor_id?: string
beeper_status?: string
test_result?: string
delay_shutdown?: string
delay_start?: string
timer_shutdown?: string
timer_reboot?: string
raw_variables?: Record<string, string>
} }
export interface GPU { export interface GPU {