Update AppImage

This commit is contained in:
MacRimi
2025-11-17 17:47:58 +01:00
parent 959433d737
commit 166fc6dad9
3 changed files with 120 additions and 132 deletions

View File

@@ -4,30 +4,11 @@ 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 { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { import { Thermometer, CpuIcon, Zap, HardDrive, Network, FanIcon, PowerIcon, Battery, Cpu, MemoryStick, Cpu as Gpu, Loader2 } from 'lucide-react'
Thermometer,
CpuIcon,
Zap,
HardDrive,
Network,
FanIcon,
PowerIcon,
Battery,
Cpu,
MemoryStick,
Cpu as Gpu,
Loader2,
} from "lucide-react"
import useSWR from "swr" import useSWR from "swr"
import { useState, useEffect } from "react" import { useState, useEffect } from "react"
import { import { type HardwareData, type GPU, type PCIDevice, type StorageDevice, fetcher } from "../types/hardware"
type HardwareData, import { API_PORT } from "@/lib/api-config"
type GPU,
type PCIDevice,
type StorageDevice,
fetcher as swrFetcher,
} from "../types/hardware"
import { fetchApi } from "@/lib/api-config"
const parseLsblkSize = (sizeStr: string | undefined): number => { const parseLsblkSize = (sizeStr: string | undefined): number => {
if (!sizeStr) return 0 if (!sizeStr) return 0
@@ -35,8 +16,11 @@ const parseLsblkSize = (sizeStr: string | undefined): number => {
// Remove spaces and convert to uppercase // Remove spaces and convert to uppercase
const cleaned = sizeStr.trim().toUpperCase() const cleaned = sizeStr.trim().toUpperCase()
// Replace comma with dot to normalize decimal separator (for European locales)
const normalized = cleaned.replace(',', '.')
// Extract number and unit // Extract number and unit
const match = cleaned.match(/^([\d.]+)([KMGT]?)$/) const match = normalized.match(/^([\d.]+)([KMGT]?)$/)
if (!match) return 0 if (!match) return 0
const value = Number.parseFloat(match[1]) const value = Number.parseFloat(match[1])
@@ -175,7 +159,7 @@ export default function Hardware() {
data: staticHardwareData, data: staticHardwareData,
error: staticError, error: staticError,
isLoading: staticLoading, isLoading: staticLoading,
} = useSWR<HardwareData>("/api/hardware", swrFetcher, { } = useSWR<HardwareData>("/api/hardware", fetcher, {
revalidateOnFocus: false, revalidateOnFocus: false,
revalidateOnReconnect: false, revalidateOnReconnect: false,
refreshInterval: 0, // No auto-refresh for static data refreshInterval: 0, // No auto-refresh for static data
@@ -186,7 +170,7 @@ export default function Hardware() {
data: dynamicHardwareData, data: dynamicHardwareData,
error: dynamicError, error: dynamicError,
isLoading: dynamicLoading, isLoading: dynamicLoading,
} = useSWR<HardwareData>("/api/hardware", swrFetcher, { } = useSWR<HardwareData>("/api/hardware", fetcher, {
refreshInterval: 7000, refreshInterval: 7000,
}) })
@@ -237,21 +221,6 @@ export default function Hardware() {
const [selectedNetwork, setSelectedNetwork] = useState<PCIDevice | null>(null) const [selectedNetwork, setSelectedNetwork] = useState<PCIDevice | null>(null)
const [selectedUPS, setSelectedUPS] = useState<any>(null) const [selectedUPS, setSelectedUPS] = useState<any>(null)
const fetcher = async (url: string) => {
const data = await fetchApi(url)
return data
}
const {
data: hardwareDataSWR,
error: swrError,
isLoading: swrLoading,
mutate,
} = useSWR<HardwareData>("/api/hardware", fetcher, {
refreshInterval: 30000,
revalidateOnFocus: false,
})
useEffect(() => { useEffect(() => {
if (!selectedGPU) return if (!selectedGPU) return
@@ -264,10 +233,30 @@ export default function Hardware() {
const fetchRealtimeData = async () => { const fetchRealtimeData = async () => {
try { try {
const data = await fetchApi(`/api/gpu/${fullSlot}/realtime`) const { protocol, hostname, port } = window.location
const isStandardPort = port === "" || port === "80" || port === "443"
const apiUrl = isStandardPort
? `/api/gpu/${fullSlot}/realtime`
: `${protocol}//${hostname}:${API_PORT}/api/gpu/${fullSlot}/realtime`
const response = await fetch(apiUrl, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
signal: abortController.signal,
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
setRealtimeGPUData(data) setRealtimeGPUData(data)
setDetailsLoading(false) setDetailsLoading(false)
} catch (error) { } catch (error) {
// Only log non-abort errors
if (error instanceof Error && error.name !== "AbortError") { if (error instanceof Error && error.name !== "AbortError") {
console.error("[v0] Error fetching GPU realtime data:", error) console.error("[v0] Error fetching GPU realtime data:", error)
} }
@@ -276,7 +265,10 @@ export default function Hardware() {
} }
} }
// Initial fetch
fetchRealtimeData() fetchRealtimeData()
// Poll every 3 seconds
const interval = setInterval(fetchRealtimeData, 3000) const interval = setInterval(fetchRealtimeData, 3000)
return () => { return () => {
@@ -292,14 +284,14 @@ export default function Hardware() {
} }
const findPCIDeviceForGPU = (gpu: GPU): PCIDevice | null => { const findPCIDeviceForGPU = (gpu: GPU): PCIDevice | null => {
if (!hardwareDataSWR?.pci_devices || !gpu.slot) return null if (!hardwareData?.pci_devices || !gpu.slot) return null
// Try to find exact match first (e.g., "00:02.0") // Try to find exact match first (e.g., "00:02.0")
let pciDevice = hardwareDataSWR.pci_devices.find((d) => d.slot === gpu.slot) let pciDevice = hardwareData.pci_devices.find((d) => d.slot === gpu.slot)
// If not found, try to match by partial slot (e.g., "00" matches "00:02.0") // If not found, try to match by partial slot (e.g., "00" matches "00:02.0")
if (!pciDevice && gpu.slot.length <= 2) { if (!pciDevice && gpu.slot.length <= 2) {
pciDevice = hardwareDataSWR.pci_devices.find( pciDevice = hardwareData.pci_devices.find(
(d) => (d) =>
d.slot.startsWith(gpu.slot + ":") && d.slot.startsWith(gpu.slot + ":") &&
(d.type.toLowerCase().includes("vga") || (d.type.toLowerCase().includes("vga") ||
@@ -318,7 +310,7 @@ export default function Hardware() {
return realtimeGPUData.has_monitoring_tool === true return realtimeGPUData.has_monitoring_tool === true
} }
if (swrLoading) { if (isLoading) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="text-center py-8"> <div className="text-center py-8">
@@ -331,7 +323,7 @@ export default function Hardware() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* System Information - CPU & Motherboard */} {/* System Information - CPU & Motherboard */}
{(hardwareDataSWR?.cpu || hardwareDataSWR?.motherboard) && ( {(hardwareData?.cpu || hardwareData?.motherboard) && (
<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">
<Cpu className="h-5 w-5 text-primary" /> <Cpu className="h-5 w-5 text-primary" />
@@ -340,44 +332,44 @@ export default function Hardware() {
<div className="grid gap-6 md:grid-cols-2"> <div className="grid gap-6 md:grid-cols-2">
{/* CPU Info */} {/* CPU Info */}
{hardwareDataSWR?.cpu && Object.keys(hardwareDataSWR.cpu).length > 0 && ( {hardwareData?.cpu && Object.keys(hardwareData.cpu).length > 0 && (
<div> <div>
<div className="mb-2 flex items-center gap-2"> <div className="mb-2 flex items-center gap-2">
<CpuIcon className="h-4 w-4 text-muted-foreground" /> <CpuIcon className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-semibold">CPU</h3> <h3 className="text-sm font-semibold">CPU</h3>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{hardwareDataSWR.cpu.model && ( {hardwareData.cpu.model && (
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-muted-foreground">Model</span> <span className="text-muted-foreground">Model</span>
<span className="font-medium text-right">{hardwareDataSWR.cpu.model}</span> <span className="font-medium text-right">{hardwareData.cpu.model}</span>
</div> </div>
)} )}
{hardwareDataSWR.cpu.cores_per_socket && hardwareDataSWR.cpu.sockets && ( {hardwareData.cpu.cores_per_socket && hardwareData.cpu.sockets && (
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-muted-foreground">Cores</span> <span className="text-muted-foreground">Cores</span>
<span className="font-medium"> <span className="font-medium">
{hardwareDataSWR.cpu.sockets} × {hardwareDataSWR.cpu.cores_per_socket} ={" "} {hardwareData.cpu.sockets} × {hardwareData.cpu.cores_per_socket} ={" "}
{hardwareDataSWR.cpu.sockets * hardwareDataSWR.cpu.cores_per_socket} cores {hardwareData.cpu.sockets * hardwareData.cpu.cores_per_socket} cores
</span> </span>
</div> </div>
)} )}
{hardwareDataSWR.cpu.total_threads && ( {hardwareData.cpu.total_threads && (
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-muted-foreground">Threads</span> <span className="text-muted-foreground">Threads</span>
<span className="font-medium">{hardwareDataSWR.cpu.total_threads}</span> <span className="font-medium">{hardwareData.cpu.total_threads}</span>
</div> </div>
)} )}
{hardwareDataSWR.cpu.l3_cache && ( {hardwareData.cpu.l3_cache && (
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-muted-foreground">L3 Cache</span> <span className="text-muted-foreground">L3 Cache</span>
<span className="font-medium">{hardwareDataSWR.cpu.l3_cache}</span> <span className="font-medium">{hardwareData.cpu.l3_cache}</span>
</div> </div>
)} )}
{hardwareDataSWR.cpu.virtualization && ( {hardwareData.cpu.virtualization && (
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-muted-foreground">Virtualization</span> <span className="text-muted-foreground">Virtualization</span>
<span className="font-medium">{hardwareDataSWR.cpu.virtualization}</span> <span className="font-medium">{hardwareData.cpu.virtualization}</span>
</div> </div>
)} )}
</div> </div>
@@ -385,41 +377,41 @@ export default function Hardware() {
)} )}
{/* Motherboard Info */} {/* Motherboard Info */}
{hardwareDataSWR?.motherboard && Object.keys(hardwareDataSWR.motherboard).length > 0 && ( {hardwareData?.motherboard && Object.keys(hardwareData.motherboard).length > 0 && (
<div> <div>
<div className="mb-2 flex items-center gap-2"> <div className="mb-2 flex items-center gap-2">
<Cpu className="h-4 w-4 text-muted-foreground" /> <Cpu className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-semibold">Motherboard</h3> <h3 className="text-sm font-semibold">Motherboard</h3>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{hardwareDataSWR.motherboard.manufacturer && ( {hardwareData.motherboard.manufacturer && (
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-muted-foreground">Manufacturer</span> <span className="text-muted-foreground">Manufacturer</span>
<span className="font-medium text-right">{hardwareDataSWR.motherboard.manufacturer}</span> <span className="font-medium text-right">{hardwareData.motherboard.manufacturer}</span>
</div> </div>
)} )}
{hardwareDataSWR.motherboard.model && ( {hardwareData.motherboard.model && (
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-muted-foreground">Model</span> <span className="text-muted-foreground">Model</span>
<span className="font-medium text-right">{hardwareDataSWR.motherboard.model}</span> <span className="font-medium text-right">{hardwareData.motherboard.model}</span>
</div> </div>
)} )}
{hardwareDataSWR.motherboard.bios?.vendor && ( {hardwareData.motherboard.bios?.vendor && (
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-muted-foreground">BIOS</span> <span className="text-muted-foreground">BIOS</span>
<span className="font-medium text-right">{hardwareDataSWR.motherboard.bios.vendor}</span> <span className="font-medium text-right">{hardwareData.motherboard.bios.vendor}</span>
</div> </div>
)} )}
{hardwareDataSWR.motherboard.bios?.version && ( {hardwareData.motherboard.bios?.version && (
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-muted-foreground">Version</span> <span className="text-muted-foreground">Version</span>
<span className="font-medium">{hardwareDataSWR.motherboard.bios.version}</span> <span className="font-medium">{hardwareData.motherboard.bios.version}</span>
</div> </div>
)} )}
{hardwareDataSWR.motherboard.bios?.date && ( {hardwareData.motherboard.bios?.date && (
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-muted-foreground">Date</span> <span className="text-muted-foreground">Date</span>
<span className="font-medium">{hardwareDataSWR.motherboard.bios.date}</span> <span className="font-medium">{hardwareData.motherboard.bios.date}</span>
</div> </div>
)} )}
</div> </div>
@@ -430,18 +422,18 @@ export default function Hardware() {
)} )}
{/* Memory Modules */} {/* Memory Modules */}
{hardwareDataSWR?.memory_modules && hardwareDataSWR.memory_modules.length > 0 && ( {hardwareData?.memory_modules && hardwareData.memory_modules.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">
<MemoryStick className="h-5 w-5 text-primary" /> <MemoryStick className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold">Memory Modules</h2> <h2 className="text-lg font-semibold">Memory Modules</h2>
<Badge variant="outline" className="ml-auto"> <Badge variant="outline" className="ml-auto">
{hardwareDataSWR.memory_modules.length} installed {hardwareData.memory_modules.length} installed
</Badge> </Badge>
</div> </div>
<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">
{hardwareDataSWR.memory_modules.map((module, index) => ( {hardwareData.memory_modules.map((module, index) => (
<div key={index} className="rounded-lg border border-border/30 bg-background/60 p-4"> <div key={index} className="rounded-lg border border-border/30 bg-background/60 p-4">
<div className="mb-2 font-medium text-sm">{module.slot}</div> <div className="mb-2 font-medium text-sm">{module.slot}</div>
<div className="space-y-1"> <div className="space-y-1">
@@ -477,29 +469,29 @@ export default function Hardware() {
)} )}
{/* Thermal Monitoring */} {/* Thermal Monitoring */}
{hardwareDataSWR?.temperatures && hardwareDataSWR.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">
<Thermometer className="h-5 w-5 text-primary" /> <Thermometer className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold">Thermal Monitoring</h2> <h2 className="text-lg font-semibold">Thermal Monitoring</h2>
<Badge variant="outline" className="ml-auto"> <Badge variant="outline" className="ml-auto">
{hardwareDataSWR.temperatures.length} sensors {hardwareData.temperatures.length} sensors
</Badge> </Badge>
</div> </div>
<div className="grid gap-6 md:grid-cols-2"> <div className="grid gap-6 md:grid-cols-2">
{/* CPU Sensors */} {/* CPU Sensors */}
{groupAndSortTemperatures(hardwareDataSWR.temperatures).CPU.length > 0 && ( {groupAndSortTemperatures(hardwareData.temperatures).CPU.length > 0 && (
<div className="md:col-span-2"> <div className="md:col-span-2">
<div className="mb-3 flex items-center gap-2"> <div className="mb-3 flex items-center gap-2">
<CpuIcon className="h-4 w-4 text-muted-foreground" /> <CpuIcon className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-semibold">CPU</h3> <h3 className="text-sm font-semibold">CPU</h3>
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
{groupAndSortTemperatures(hardwareDataSWR.temperatures).CPU.length} {groupAndSortTemperatures(hardwareData.temperatures).CPU.length}
</Badge> </Badge>
</div> </div>
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
{groupAndSortTemperatures(hardwareDataSWR.temperatures).CPU.map((temp, index) => { {groupAndSortTemperatures(hardwareData.temperatures).CPU.map((temp, index) => {
const percentage = const percentage =
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100 temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
const isHot = temp.current > (temp.high || 80) const isHot = temp.current > (temp.high || 80)
@@ -530,21 +522,21 @@ export default function Hardware() {
)} )}
{/* GPU Sensors */} {/* GPU Sensors */}
{groupAndSortTemperatures(hardwareDataSWR.temperatures).GPU.length > 0 && ( {groupAndSortTemperatures(hardwareData.temperatures).GPU.length > 0 && (
<div <div
className={groupAndSortTemperatures(hardwareDataSWR.temperatures).GPU.length > 1 ? "md:col-span-2" : ""} className={groupAndSortTemperatures(hardwareData.temperatures).GPU.length > 1 ? "md:col-span-2" : ""}
> >
<div className="mb-3 flex items-center gap-2"> <div className="mb-3 flex items-center gap-2">
<Gpu className="h-4 w-4 text-muted-foreground" /> <Gpu className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-semibold">GPU</h3> <h3 className="text-sm font-semibold">GPU</h3>
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
{groupAndSortTemperatures(hardwareDataSWR.temperatures).GPU.length} {groupAndSortTemperatures(hardwareData.temperatures).GPU.length}
</Badge> </Badge>
</div> </div>
<div <div
className={`grid gap-4 ${groupAndSortTemperatures(hardwareDataSWR.temperatures).GPU.length > 1 ? "md:grid-cols-2" : ""}`} className={`grid gap-4 ${groupAndSortTemperatures(hardwareData.temperatures).GPU.length > 1 ? "md:grid-cols-2" : ""}`}
> >
{groupAndSortTemperatures(hardwareDataSWR.temperatures).GPU.map((temp, index) => { {groupAndSortTemperatures(hardwareData.temperatures).GPU.map((temp, index) => {
const percentage = const percentage =
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100 temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
const isHot = temp.current > (temp.high || 80) const isHot = temp.current > (temp.high || 80)
@@ -575,23 +567,21 @@ export default function Hardware() {
)} )}
{/* NVME Sensors */} {/* NVME Sensors */}
{groupAndSortTemperatures(hardwareDataSWR.temperatures).NVME.length > 0 && ( {groupAndSortTemperatures(hardwareData.temperatures).NVME.length > 0 && (
<div <div
className={ className={groupAndSortTemperatures(hardwareData.temperatures).NVME.length > 1 ? "md:col-span-2" : ""}
groupAndSortTemperatures(hardwareDataSWR.temperatures).NVME.length > 1 ? "md:col-span-2" : ""
}
> >
<div className="mb-3 flex items-center gap-2"> <div className="mb-3 flex items-center gap-2">
<HardDrive className="h-4 w-4 text-muted-foreground" /> <HardDrive className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-semibold">NVME</h3> <h3 className="text-sm font-semibold">NVME</h3>
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
{groupAndSortTemperatures(hardwareDataSWR.temperatures).NVME.length} {groupAndSortTemperatures(hardwareData.temperatures).NVME.length}
</Badge> </Badge>
</div> </div>
<div <div
className={`grid gap-4 ${groupAndSortTemperatures(hardwareDataSWR.temperatures).NVME.length > 1 ? "md:grid-cols-2" : ""}`} className={`grid gap-4 ${groupAndSortTemperatures(hardwareData.temperatures).NVME.length > 1 ? "md:grid-cols-2" : ""}`}
> >
{groupAndSortTemperatures(hardwareDataSWR.temperatures).NVME.map((temp, index) => { {groupAndSortTemperatures(hardwareData.temperatures).NVME.map((temp, index) => {
const percentage = const percentage =
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100 temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
const isHot = temp.current > (temp.high || 80) const isHot = temp.current > (temp.high || 80)
@@ -622,21 +612,21 @@ export default function Hardware() {
)} )}
{/* PCI Sensors */} {/* PCI Sensors */}
{groupAndSortTemperatures(hardwareDataSWR.temperatures).PCI.length > 0 && ( {groupAndSortTemperatures(hardwareData.temperatures).PCI.length > 0 && (
<div <div
className={groupAndSortTemperatures(hardwareDataSWR.temperatures).PCI.length > 1 ? "md:col-span-2" : ""} className={groupAndSortTemperatures(hardwareData.temperatures).PCI.length > 1 ? "md:col-span-2" : ""}
> >
<div className="mb-3 flex items-center gap-2"> <div className="mb-3 flex items-center gap-2">
<CpuIcon className="h-4 w-4 text-muted-foreground" /> <CpuIcon className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-semibold">PCI</h3> <h3 className="text-sm font-semibold">PCI</h3>
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
{groupAndSortTemperatures(hardwareDataSWR.temperatures).PCI.length} {groupAndSortTemperatures(hardwareData.temperatures).PCI.length}
</Badge> </Badge>
</div> </div>
<div <div
className={`grid gap-4 ${groupAndSortTemperatures(hardwareDataSWR.temperatures).PCI.length > 1 ? "md:grid-cols-2" : ""}`} className={`grid gap-4 ${groupAndSortTemperatures(hardwareData.temperatures).PCI.length > 1 ? "md:grid-cols-2" : ""}`}
> >
{groupAndSortTemperatures(hardwareDataSWR.temperatures).PCI.map((temp, index) => { {groupAndSortTemperatures(hardwareData.temperatures).PCI.map((temp, index) => {
const percentage = const percentage =
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100 temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
const isHot = temp.current > (temp.high || 80) const isHot = temp.current > (temp.high || 80)
@@ -667,23 +657,21 @@ export default function Hardware() {
)} )}
{/* OTHER Sensors */} {/* OTHER Sensors */}
{groupAndSortTemperatures(hardwareDataSWR.temperatures).OTHER.length > 0 && ( {groupAndSortTemperatures(hardwareData.temperatures).OTHER.length > 0 && (
<div <div
className={ className={groupAndSortTemperatures(hardwareData.temperatures).OTHER.length > 1 ? "md:col-span-2" : ""}
groupAndSortTemperatures(hardwareDataSWR.temperatures).OTHER.length > 1 ? "md:col-span-2" : ""
}
> >
<div className="mb-3 flex items-center gap-2"> <div className="mb-3 flex items-center gap-2">
<Thermometer className="h-4 w-4 text-muted-foreground" /> <Thermometer className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-semibold">OTHER</h3> <h3 className="text-sm font-semibold">OTHER</h3>
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
{groupAndSortTemperatures(hardwareDataSWR.temperatures).OTHER.length} {groupAndSortTemperatures(hardwareData.temperatures).OTHER.length}
</Badge> </Badge>
</div> </div>
<div <div
className={`grid gap-4 ${groupAndSortTemperatures(hardwareDataSWR.temperatures).OTHER.length > 1 ? "md:grid-cols-2" : ""}`} className={`grid gap-4 ${groupAndSortTemperatures(hardwareData.temperatures).OTHER.length > 1 ? "md:grid-cols-2" : ""}`}
> >
{groupAndSortTemperatures(hardwareDataSWR.temperatures).OTHER.map((temp, index) => { {groupAndSortTemperatures(hardwareData.temperatures).OTHER.map((temp, index) => {
const percentage = const percentage =
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100 temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
const isHot = temp.current > (temp.high || 80) const isHot = temp.current > (temp.high || 80)
@@ -717,18 +705,18 @@ export default function Hardware() {
)} )}
{/* GPU Information - Enhanced with on-demand data fetching */} {/* GPU Information - Enhanced with on-demand data fetching */}
{hardwareDataSWR?.gpus && hardwareDataSWR.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">
<Gpu className="h-5 w-5 text-primary" /> <Gpu className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold">Graphics Cards</h2> <h2 className="text-lg font-semibold">Graphics Cards</h2>
<Badge variant="outline" className="ml-auto"> <Badge variant="outline" className="ml-auto">
{hardwareDataSWR.gpus.length} GPU{hardwareDataSWR.gpus.length > 1 ? "s" : ""} {hardwareData.gpus.length} GPU{hardwareData.gpus.length > 1 ? "s" : ""}
</Badge> </Badge>
</div> </div>
<div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-4 sm:grid-cols-2">
{hardwareDataSWR.gpus.map((gpu, index) => { {hardwareData.gpus.map((gpu, index) => {
const pciDevice = findPCIDeviceForGPU(gpu) const pciDevice = findPCIDeviceForGPU(gpu)
const fullSlot = pciDevice?.slot || gpu.slot const fullSlot = pciDevice?.slot || gpu.slot
@@ -1106,18 +1094,18 @@ export default function Hardware() {
</Dialog> </Dialog>
{/* PCI Devices - Changed to modal */} {/* PCI Devices - Changed to modal */}
{hardwareDataSWR?.pci_devices && hardwareDataSWR.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">
<CpuIcon className="h-5 w-5 text-primary" /> <CpuIcon className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold">PCI Devices</h2> <h2 className="text-lg font-semibold">PCI Devices</h2>
<Badge variant="outline" className="ml-auto"> <Badge variant="outline" className="ml-auto">
{hardwareDataSWR.pci_devices.length} devices {hardwareData.pci_devices.length} devices
</Badge> </Badge>
</div> </div>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{hardwareDataSWR.pci_devices.map((device, index) => ( {hardwareData.pci_devices.map((device, index) => (
<div <div
key={index} key={index}
onClick={() => setSelectedPCIDevice(device)} onClick={() => setSelectedPCIDevice(device)}
@@ -1192,7 +1180,7 @@ export default function Hardware() {
</Dialog> </Dialog>
{/* Power Consumption */} {/* Power Consumption */}
{hardwareDataSWR?.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">
<Zap className="h-5 w-5 text-blue-500" /> <Zap className="h-5 w-5 text-blue-500" />
@@ -1202,13 +1190,13 @@ export default function Hardware() {
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between rounded-lg border border-border/30 bg-background/60 p-4"> <div className="flex items-center justify-between rounded-lg border border-border/30 bg-background/60 p-4">
<div className="space-y-1"> <div className="space-y-1">
<p className="text-sm font-medium">{hardwareDataSWR.power_meter.name}</p> <p className="text-sm font-medium">{hardwareData.power_meter.name}</p>
{hardwareDataSWR.power_meter.adapter && ( {hardwareData.power_meter.adapter && (
<p className="text-xs text-muted-foreground">{hardwareDataSWR.power_meter.adapter}</p> <p className="text-xs text-muted-foreground">{hardwareData.power_meter.adapter}</p>
)} )}
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="text-2xl font-bold text-blue-500">{hardwareDataSWR.power_meter.watts.toFixed(1)} W</p> <p className="text-2xl font-bold text-blue-500">{hardwareData.power_meter.watts.toFixed(1)} W</p>
<p className="text-xs text-muted-foreground">Current Draw</p> <p className="text-xs text-muted-foreground">Current Draw</p>
</div> </div>
</div> </div>
@@ -1217,18 +1205,18 @@ export default function Hardware() {
)} )}
{/* Power Supplies */} {/* Power Supplies */}
{hardwareDataSWR?.power_supplies && hardwareDataSWR.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">
<PowerIcon className="h-5 w-5 text-green-500" /> <PowerIcon className="h-5 w-5 text-green-500" />
<h2 className="text-lg font-semibold">Power Supplies</h2> <h2 className="text-lg font-semibold">Power Supplies</h2>
<Badge variant="outline" className="ml-auto"> <Badge variant="outline" className="ml-auto">
{hardwareDataSWR.power_supplies.length} PSUs {hardwareData.power_supplies.length} PSUs
</Badge> </Badge>
</div> </div>
<div className="grid gap-3 md:grid-cols-2"> <div className="grid gap-3 md:grid-cols-2">
{hardwareDataSWR.power_supplies.map((psu, index) => ( {hardwareData.power_supplies.map((psu, index) => (
<div key={index} className="rounded-lg border border-border/30 bg-background/60 p-4"> <div key={index} className="rounded-lg border border-border/30 bg-background/60 p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm font-medium">{psu.name}</span> <span className="text-sm font-medium">{psu.name}</span>
@@ -1245,18 +1233,18 @@ export default function Hardware() {
)} )}
{/* Fans */} {/* Fans */}
{hardwareDataSWR?.fans && hardwareDataSWR.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">
<FanIcon className="h-5 w-5 text-primary" /> <FanIcon className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold">System Fans</h2> <h2 className="text-lg font-semibold">System Fans</h2>
<Badge variant="outline" className="ml-auto"> <Badge variant="outline" className="ml-auto">
{hardwareDataSWR.fans.length} fans {hardwareData.fans.length} fans
</Badge> </Badge>
</div> </div>
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
{hardwareDataSWR.fans.map((fan, index) => { {hardwareData.fans.map((fan, index) => {
const isPercentage = fan.unit === "percent" || fan.unit === "%" const isPercentage = fan.unit === "percent" || fan.unit === "%"
const percentage = isPercentage ? fan.speed : Math.min((fan.speed / 5000) * 100, 100) const percentage = isPercentage ? fan.speed : Math.min((fan.speed / 5000) * 100, 100)
@@ -1280,18 +1268,18 @@ export default function Hardware() {
)} )}
{/* UPS */} {/* UPS */}
{hardwareDataSWR?.ups && Array.isArray(hardwareDataSWR.ups) && hardwareDataSWR.ups.length > 0 && ( {hardwareData?.ups && Array.isArray(hardwareData.ups) && hardwareData.ups.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">
<Battery className="h-5 w-5 text-primary" /> <Battery className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold">UPS Status</h2> <h2 className="text-lg font-semibold">UPS Status</h2>
<Badge variant="outline" className="ml-auto"> <Badge variant="outline" className="ml-auto">
{hardwareDataSWR.ups.length} UPS {hardwareData.ups.length} UPS
</Badge> </Badge>
</div> </div>
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
{hardwareDataSWR.ups.map((ups: any, index: number) => { {hardwareData.ups.map((ups: any, index: number) => {
const batteryCharge = const batteryCharge =
ups.battery_charge_raw || Number.parseFloat(ups.battery_charge?.replace("%", "") || "0") ups.battery_charge_raw || Number.parseFloat(ups.battery_charge?.replace("%", "") || "0")
const loadPercent = ups.load_percent_raw || Number.parseFloat(ups.load_percent?.replace("%", "") || "0") const loadPercent = ups.load_percent_raw || Number.parseFloat(ups.load_percent?.replace("%", "") || "0")
@@ -1562,19 +1550,19 @@ export default function Hardware() {
</Dialog> </Dialog>
{/* Network Summary - Clickable */} {/* Network Summary - Clickable */}
{hardwareDataSWR?.pci_devices && {hardwareData?.pci_devices &&
hardwareDataSWR.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">
<div className="mb-4 flex items-center gap-2"> <div className="mb-4 flex items-center gap-2">
<Network className="h-5 w-5 text-primary" /> <Network className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold">Network Summary</h2> <h2 className="text-lg font-semibold">Network Summary</h2>
<Badge variant="outline" className="ml-auto"> <Badge variant="outline" className="ml-auto">
{hardwareDataSWR.pci_devices.filter((d) => d.type.toLowerCase().includes("network")).length} interfaces {hardwareData.pci_devices.filter((d) => d.type.toLowerCase().includes("network")).length} interfaces
</Badge> </Badge>
</div> </div>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{hardwareDataSWR.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 <div
@@ -1654,14 +1642,14 @@ export default function Hardware() {
</Dialog> </Dialog>
{/* Storage Summary - Clickable */} {/* Storage Summary - Clickable */}
{hardwareDataSWR?.storage_devices && hardwareDataSWR.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">
<HardDrive className="h-5 w-5 text-primary" /> <HardDrive className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold">Storage Summary</h2> <h2 className="text-lg font-semibold">Storage Summary</h2>
<Badge variant="outline" className="ml-auto"> <Badge variant="outline" className="ml-auto">
{ {
hardwareDataSWR.storage_devices.filter( hardwareData.storage_devices.filter(
(device) => (device) =>
device.type === "disk" && !device.name.startsWith("zd") && !device.name.startsWith("loop"), device.type === "disk" && !device.name.startsWith("zd") && !device.name.startsWith("loop"),
).length ).length
@@ -1671,7 +1659,7 @@ export default function Hardware() {
</div> </div>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{hardwareDataSWR.storage_devices {hardwareData.storage_devices
.filter( .filter(
(device) => device.type === "disk" && !device.name.startsWith("zd") && !device.name.startsWith("loop"), (device) => device.type === "disk" && !device.name.startsWith("zd") && !device.name.startsWith("loop"),
) )
@@ -1780,7 +1768,7 @@ export default function Hardware() {
<span className="text-sm font-medium truncate flex-1">{device.name}</span> <span className="text-sm font-medium truncate flex-1">{device.name}</span>
<Badge className={`${diskBadge.className} px-2.5 py-0.5 shrink-0`}>{diskBadge.label}</Badge> <Badge className={`${diskBadge.className} px-2.5 py-0.5 shrink-0`}>{diskBadge.label}</Badge>
</div> </div>
{device.size && <p className="text-sm font-medium">{formatMemory(parseLsblkSize(device.size))}</p>} {device.size && <p className="text-sm font-medium">{device.size}</p>}
{device.model && ( {device.model && (
<p className="text-xs text-muted-foreground line-clamp-2 break-words">{device.model}</p> <p className="text-xs text-muted-foreground line-clamp-2 break-words">{device.model}</p>
)} )}
@@ -1857,7 +1845,7 @@ export default function Hardware() {
{selectedDisk.size && ( {selectedDisk.size && (
<div className="flex justify-between border-b border-border/50 pb-2"> <div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Capacity</span> <span className="text-sm font-medium text-muted-foreground">Capacity</span>
<span className="text-sm font-medium">{formatMemory(parseLsblkSize(selectedDisk.size))}</span> <span className="text-sm font-medium">{selectedDisk.size}</span>
</div> </div>
)} )}

View File

@@ -9,7 +9,7 @@
* Can be changed to 8009 for beta testing * Can be changed to 8009 for beta testing
* This can also be set via NEXT_PUBLIC_API_PORT environment variable * This can also be set via NEXT_PUBLIC_API_PORT environment variable
*/ */
export const API_PORT = process.env.NEXT_PUBLIC_API_PORT || "8008" export const API_PORT = process.env.NEXT_PUBLIC_API_PORT || "8009"
/** /**
* Gets the base URL for API calls * Gets the base URL for API calls

View File

@@ -6110,4 +6110,4 @@ if __name__ == '__main__':
# Print only essential information # Print only essential information
# print("API endpoints available at: /api/system, /api/system-info, /api/storage, /api/proxmox-storage, /api/network, /api/vms, /api/logs, /api/health, /api/hardware, /api/prometheus, /api/node/metrics") # print("API endpoints available at: /api/system, /api/system-info, /api/storage, /api/proxmox-storage, /api/network, /api/vms, /api/logs, /api/health, /api/hardware, /api/prometheus, /api/node/metrics")
app.run(host='0.0.0.0', port=8008, debug=False) app.run(host='0.0.0.0', port=8009, debug=False)