Update AppImage

This commit is contained in:
MacRimi
2025-10-09 19:00:58 +02:00
parent 5221ad6da7
commit f6209b97e2
2 changed files with 157 additions and 132 deletions

View File

@@ -19,7 +19,7 @@ import {
Loader2, Loader2,
} from "lucide-react" } from "lucide-react"
import useSWR from "swr" import useSWR from "swr"
import { useState } from "react" import { useState, useEffect } from "react"
import { type HardwareData, type GPU, type PCIDevice, type StorageDevice, fetcher } from "../types/hardware" import { type HardwareData, type GPU, type PCIDevice, type StorageDevice, fetcher } from "../types/hardware"
const getDeviceTypeColor = (type: string): string => { const getDeviceTypeColor = (type: string): string => {
@@ -62,6 +62,7 @@ export default function Hardware() {
const [selectedGPU, setSelectedGPU] = useState<GPU | null>(null) const [selectedGPU, setSelectedGPU] = useState<GPU | null>(null)
const [realtimeGPUData, setRealtimeGPUData] = useState<any>(null) const [realtimeGPUData, setRealtimeGPUData] = useState<any>(null)
const [detailsLoading, setDetailsLoading] = useState(false) const [detailsLoading, setDetailsLoading] = useState(false)
const [isPolling, setIsPolling] = useState(false)
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)
@@ -78,12 +79,14 @@ export default function Hardware() {
setSelectedGPU(gpu) setSelectedGPU(gpu)
setDetailsLoading(true) setDetailsLoading(true)
setRealtimeGPUData(null) setRealtimeGPUData(null)
setIsPolling(true)
console.log("[v0] Modal opened, fetching realtime data...") console.log("[v0] Modal opened, fetching realtime data...")
if (!fullSlot) { if (!fullSlot) {
console.log("[v0] No slot found, showing basic info only") console.log("[v0] No slot found, showing basic info only")
setDetailsLoading(false) setDetailsLoading(false)
setIsPolling(false)
return return
} }
@@ -107,12 +110,49 @@ export default function Hardware() {
} catch (error) { } catch (error) {
console.error("[v0] Error fetching GPU realtime data:", error) console.error("[v0] Error fetching GPU realtime data:", error)
setRealtimeGPUData({ has_monitoring_tool: false }) setRealtimeGPUData({ has_monitoring_tool: false })
setIsPolling(false)
} finally { } finally {
setDetailsLoading(false) setDetailsLoading(false)
console.log("[v0] Finished loading GPU data") console.log("[v0] Finished loading GPU data")
} }
} }
useEffect(() => {
if (!isPolling || !selectedGPU || !realtimeGPUData?.has_monitoring_tool) {
return
}
const pciDevice = findPCIDeviceForGPU(selectedGPU)
const fullSlot = pciDevice?.slot || selectedGPU.slot
if (!fullSlot) {
return
}
const pollInterval = setInterval(async () => {
try {
const response = await fetch(`http://localhost:8008/api/gpu/${fullSlot}/realtime`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
signal: AbortSignal.timeout(5000),
})
if (response.ok) {
const data = await response.json()
setRealtimeGPUData(data)
}
} catch (error) {
console.error("[v0] Error polling GPU data:", error)
}
}, 2000) // Poll every 2 seconds
return () => {
clearInterval(pollInterval)
}
}, [isPolling, selectedGPU, realtimeGPUData?.has_monitoring_tool])
const findPCIDeviceForGPU = (gpu: GPU): PCIDevice | null => { const findPCIDeviceForGPU = (gpu: GPU): PCIDevice | null => {
if (!hardwareData?.pci_devices || !gpu.slot) return null if (!hardwareData?.pci_devices || !gpu.slot) return null
@@ -396,6 +436,7 @@ export default function Hardware() {
onOpenChange={() => { onOpenChange={() => {
setSelectedGPU(null) setSelectedGPU(null)
setRealtimeGPUData(null) setRealtimeGPUData(null)
setIsPolling(false)
}} }}
> >
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto"> <DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
@@ -404,7 +445,11 @@ export default function Hardware() {
<DialogHeader className="pb-4 border-b border-border"> <DialogHeader className="pb-4 border-b border-border">
<DialogTitle>{selectedGPU.name}</DialogTitle> <DialogTitle>{selectedGPU.name}</DialogTitle>
<DialogDescription> <DialogDescription>
{detailsLoading ? "Loading real-time monitoring data..." : "GPU Information"} {detailsLoading
? "Loading real-time monitoring data..."
: isPolling && realtimeGPUData?.has_monitoring_tool
? "Live monitoring (updates every 2s)"
: "GPU Information"}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -454,6 +499,13 @@ export default function Hardware() {
</div> </div>
) : realtimeGPUData?.has_monitoring_tool === true ? ( ) : realtimeGPUData?.has_monitoring_tool === true ? (
<> <>
{isPolling && (
<div className="flex items-center gap-2 text-xs text-green-500">
<div className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
<span>Live monitoring active</span>
</div>
)}
<div> <div>
<h3 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide"> <h3 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
Real-Time Metrics Real-Time Metrics
@@ -469,61 +521,37 @@ export default function Hardware() {
</div> </div>
<Progress value={(realtimeGPUData.temperature / 100) * 100} className="h-2" /> <Progress value={(realtimeGPUData.temperature / 100) * 100} className="h-2" />
</div> </div>
) : ( ) : null}
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Temperature</span>
<span className="text-sm font-medium text-muted-foreground">N/A</span>
</div>
)}
{realtimeGPUData.utilization_gpu !== undefined && realtimeGPUData.utilization_gpu !== null ? ( <div className="space-y-1">
<div className="space-y-1"> <div className="flex justify-between">
<div className="flex justify-between"> <span className="text-sm text-muted-foreground">GPU Utilization</span>
<span className="text-sm text-muted-foreground">GPU Utilization</span> <span className="text-sm font-medium">{realtimeGPUData.utilization_gpu || "0.0%"}</span>
<span className="text-sm font-medium">
{typeof realtimeGPUData.utilization_gpu === "string"
? realtimeGPUData.utilization_gpu
: `${realtimeGPUData.utilization_gpu}%`}
</span>
</div>
<Progress
value={
typeof realtimeGPUData.utilization_gpu === "string"
? Number.parseFloat(realtimeGPUData.utilization_gpu)
: realtimeGPUData.utilization_gpu
}
className="h-2"
/>
</div> </div>
) : ( <Progress
<div className="space-y-1"> value={
<div className="flex justify-between"> typeof realtimeGPUData.utilization_gpu === "string"
<span className="text-sm text-muted-foreground">GPU Utilization</span> ? Number.parseFloat(realtimeGPUData.utilization_gpu)
<span className="text-sm font-medium">0.0%</span> : realtimeGPUData.utilization_gpu || 0
</div> }
<Progress value={0} className="h-2" /> className="h-2"
</div> />
)} </div>
{realtimeGPUData.clock_graphics && ( {realtimeGPUData.clock_graphics !== undefined && realtimeGPUData.clock_graphics !== null && (
<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 text-muted-foreground">Graphics Clock</span> <span className="text-sm text-muted-foreground">Graphics Clock</span>
<span className="text-sm font-medium">{realtimeGPUData.clock_graphics}</span> <span className="text-sm font-medium">{realtimeGPUData.clock_graphics}</span>
</div> </div>
)} )}
{realtimeGPUData.clock_memory && (
<div className="flex justify-between border-b border-border/50 pb-2"> {realtimeGPUData.power_draw !== undefined && realtimeGPUData.power_draw !== null && (
<span className="text-sm text-muted-foreground">Memory Clock</span>
<span className="text-sm font-medium">{realtimeGPUData.clock_memory}</span>
</div>
)}
{realtimeGPUData.power_draw && realtimeGPUData.power_draw !== "N/A" && (
<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 text-muted-foreground">Power Draw</span> <span className="text-sm text-muted-foreground">Power Draw</span>
<span className="text-sm font-medium">{realtimeGPUData.power_draw}</span> <span className="text-sm font-medium">{realtimeGPUData.power_draw}</span>
</div> </div>
)} )}
{realtimeGPUData.power_limit && ( {realtimeGPUData.power_limit !== undefined && realtimeGPUData.power_limit !== null && (
<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 text-muted-foreground">Power Limit</span> <span className="text-sm text-muted-foreground">Power Limit</span>
<span className="text-sm font-medium">{realtimeGPUData.power_limit}</span> <span className="text-sm font-medium">{realtimeGPUData.power_limit}</span>
@@ -532,7 +560,7 @@ export default function Hardware() {
</div> </div>
</div> </div>
{/* Engine Utilization (Intel/AMD) */} {/* Engine Utilization (Intel/AMD) - Always show even if 0% */}
{(realtimeGPUData.engine_render !== undefined || {(realtimeGPUData.engine_render !== undefined ||
realtimeGPUData.engine_blitter !== undefined || realtimeGPUData.engine_blitter !== undefined ||
realtimeGPUData.engine_video !== undefined || realtimeGPUData.engine_video !== undefined ||
@@ -546,9 +574,20 @@ export default function Hardware() {
<div className="space-y-1"> <div className="space-y-1">
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-sm text-muted-foreground">Render/3D</span> <span className="text-sm text-muted-foreground">Render/3D</span>
<span className="text-sm font-medium">{realtimeGPUData.engine_render.toFixed(2)}%</span> <span className="text-sm font-medium">
{typeof realtimeGPUData.engine_render === "string"
? realtimeGPUData.engine_render
: `${realtimeGPUData.engine_render.toFixed(1)}%`}
</span>
</div> </div>
<Progress value={realtimeGPUData.engine_render} className="h-2" /> <Progress
value={
typeof realtimeGPUData.engine_render === "string"
? Number.parseFloat(realtimeGPUData.engine_render)
: realtimeGPUData.engine_render
}
className="h-2"
/>
</div> </div>
)} )}
{realtimeGPUData.engine_blitter !== undefined && ( {realtimeGPUData.engine_blitter !== undefined && (
@@ -556,19 +595,39 @@ export default function Hardware() {
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-sm text-muted-foreground">Blitter</span> <span className="text-sm text-muted-foreground">Blitter</span>
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{realtimeGPUData.engine_blitter.toFixed(2)}% {typeof realtimeGPUData.engine_blitter === "string"
? realtimeGPUData.engine_blitter
: `${realtimeGPUData.engine_blitter.toFixed(1)}%`}
</span> </span>
</div> </div>
<Progress value={realtimeGPUData.engine_blitter} className="h-2" /> <Progress
value={
typeof realtimeGPUData.engine_blitter === "string"
? Number.parseFloat(realtimeGPUData.engine_blitter)
: realtimeGPUData.engine_blitter
}
className="h-2"
/>
</div> </div>
)} )}
{realtimeGPUData.engine_video !== undefined && ( {realtimeGPUData.engine_video !== undefined && (
<div className="space-y-1"> <div className="space-y-1">
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-sm text-muted-foreground">Video</span> <span className="text-sm text-muted-foreground">Video</span>
<span className="text-sm font-medium">{realtimeGPUData.engine_video.toFixed(2)}%</span> <span className="text-sm font-medium">
{typeof realtimeGPUData.engine_video === "string"
? realtimeGPUData.engine_video
: `${realtimeGPUData.engine_video.toFixed(1)}%`}
</span>
</div> </div>
<Progress value={realtimeGPUData.engine_video} className="h-2" /> <Progress
value={
typeof realtimeGPUData.engine_video === "string"
? Number.parseFloat(realtimeGPUData.engine_video)
: realtimeGPUData.engine_video
}
className="h-2"
/>
</div> </div>
)} )}
{realtimeGPUData.engine_video_enhance !== undefined && ( {realtimeGPUData.engine_video_enhance !== undefined && (
@@ -576,10 +635,19 @@ export default function Hardware() {
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-sm text-muted-foreground">VideoEnhance</span> <span className="text-sm text-muted-foreground">VideoEnhance</span>
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{realtimeGPUData.engine_video_enhance.toFixed(2)}% {typeof realtimeGPUData.engine_video_enhance === "string"
? realtimeGPUData.engine_video_enhance
: `${realtimeGPUData.engine_video_enhance.toFixed(1)}%`}
</span> </span>
</div> </div>
<Progress value={realtimeGPUData.engine_video_enhance} className="h-2" /> <Progress
value={
typeof realtimeGPUData.engine_video_enhance === "string"
? Number.parseFloat(realtimeGPUData.engine_video_enhance)
: realtimeGPUData.engine_video_enhance
}
className="h-2"
/>
</div> </div>
)} )}
</div> </div>

View File

@@ -3,7 +3,7 @@
ProxMenux Flask Server ProxMenux Flask Server
Provides REST API endpoints for Proxmox monitoring data Provides REST API endpoints for Proxmox monitoring data
Runs on port 8008 and serves system metrics, storage info, network stats, etc. Runs on port 8008 and serves system metrics, storage info, network stats, etc.
Also serves the Next.js dashboard as static files. Also serves the Next.js dashboard as static files
""" """
from flask import Flask, jsonify, request, send_from_directory, send_file from flask import Flask, jsonify, request, send_from_directory, send_file
@@ -1626,25 +1626,46 @@ def get_detailed_gpu_info(gpu):
print(f"[v0] Current user: {os.getenv('USER', 'unknown')}", flush=True) print(f"[v0] Current user: {os.getenv('USER', 'unknown')}", flush=True)
print(f"[v0] Current working directory: {os.getcwd()}", flush=True) print(f"[v0] Current working directory: {os.getcwd()}", flush=True)
cmd = ['intel_gpu_top', '-J'] try:
print(f"[v0] Executing command: {' '.join(cmd)}", flush=True) version_result = subprocess.run(['intel_gpu_top', '--version'],
capture_output=True, text=True, timeout=2)
if version_result.returncode == 0:
version_info = version_result.stdout.strip()
print(f"[v0] intel_gpu_top version: {version_info}", flush=True)
except Exception as e:
print(f"[v0] Could not get intel_gpu_top version: {e}", flush=True)
# Check DRM device permissions
drm_devices = ['/dev/dri/card0', '/dev/dri/renderD128']
for drm_dev in drm_devices:
if os.path.exists(drm_dev):
can_read = os.access(drm_dev, os.R_OK)
can_write = os.access(drm_dev, os.W_OK)
print(f"[v0] {drm_dev}: read={can_read}, write={can_write}", flush=True)
else:
print(f"[v0] {drm_dev}: does not exist", flush=True)
cmd = 'intel_gpu_top -J'
print(f"[v0] Executing command: {cmd}", flush=True)
process = subprocess.Popen( process = subprocess.Popen(
cmd, cmd,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, text=True,
bufsize=1 bufsize=1,
shell=True,
env=os.environ.copy()
) )
print(f"[v0] Process started with PID: {process.pid}", flush=True) print(f"[v0] Process started with PID: {process.pid}", flush=True)
print(f"[v0] Waiting 1 second for intel_gpu_top to initialize...", flush=True) print(f"[v0] Waiting 2 seconds for intel_gpu_top to initialize...", flush=True)
time.sleep(1) time.sleep(2)
print(f"[v0] Starting to read JSON objects...", flush=True) print(f"[v0] Starting to read JSON objects...", flush=True)
start_time = time.time() start_time = time.time()
timeout = 8 # Increased timeout from 5 to 8 seconds timeout = 3 # Reduced timeout for faster has_monitoring_tool detection
json_objects = [] json_objects = []
buffer = "" buffer = ""
brace_count = 0 brace_count = 0
@@ -1709,6 +1730,14 @@ def get_detailed_gpu_info(gpu):
print(f"[v0] Collected {len(json_objects)} JSON objects total", flush=True) print(f"[v0] Collected {len(json_objects)} JSON objects total", flush=True)
if not any('clients' in obj for obj in json_objects):
try:
stderr_output = process.stderr.read()
if stderr_output:
print(f"[v0] intel_gpu_top stderr: {stderr_output[:500]}", flush=True)
except:
pass
best_json = None best_json = None
for json_obj in json_objects: for json_obj in json_objects:
if 'clients' in json_obj and len(json_obj['clients']) > 0: if 'clients' in json_obj and len(json_obj['clients']) > 0:
@@ -1825,10 +1854,6 @@ def get_detailed_gpu_info(gpu):
print(f"[v0] WARNING: No data retrieved from intel_gpu_top", flush=True) print(f"[v0] WARNING: No data retrieved from intel_gpu_top", flush=True)
else: else:
print(f"[v0] WARNING: No valid JSON objects found", flush=True) print(f"[v0] WARNING: No valid JSON objects found", flush=True)
# Check stderr for errors
stderr_output = process.stderr.read()
if stderr_output:
print(f"[v0] intel_gpu_top stderr: {stderr_output}", flush=True)
except Exception as e: except Exception as e:
print(f"[v0] Error running intel_gpu_top: {e}", flush=True) print(f"[v0] Error running intel_gpu_top: {e}", flush=True)
@@ -2144,74 +2169,6 @@ def get_gpu_info():
return gpus return gpus
def get_disk_hardware_info(disk_name):
"""Get detailed hardware information for a disk"""
disk_info = {}
try:
# Get disk type (HDD, SSD, NVMe)
result = subprocess.run(['lsblk', '-d', '-n', '-o', 'NAME,ROTA,TYPE', f'/dev/{disk_name}'],
capture_output=True, text=True, timeout=5)
if result.returncode == 0:
parts = result.stdout.strip().split()
if len(parts) >= 2:
rota = parts[1]
disk_info['type'] = 'HDD' if rota == '1' else 'SSD'
if disk_name.startswith('nvme'):
disk_info['type'] = 'NVMe SSD'
# Get driver/kernel module
try:
# For NVMe
if disk_name.startswith('nvme'):
disk_info['driver'] = 'nvme'
disk_info['interface'] = 'PCIe/NVMe'
# For SATA/SAS
else:
result = subprocess.run(['udevadm', 'info', '--query=property', f'/dev/{disk_name}'],
capture_output=True, text=True, timeout=5)
if result.returncode == 0:
for line in result.stdout.split('\n'):
if 'ID_BUS=' in line:
bus = line.split('=')[1].strip()
disk_info['interface'] = bus.upper()
if 'ID_MODEL=' in line:
model = line.split('=')[1].strip()
disk_info['model'] = model
if 'ID_SERIAL_SHORT=' in line:
serial = line.split('=')[1].strip()
disk_info['serial'] = serial
except Exception as e:
print(f"[v0] Error getting disk driver info: {e}")
# Get SMART data
try:
result = subprocess.run(['smartctl', '-i', f'/dev/{disk_name}'],
capture_output=True, text=True, timeout=5)
if result.returncode == 0:
for line in result.stdout.split('\n'):
if 'Model Family:' in line:
disk_info['family'] = line.split(':', 1)[1].strip()
elif 'Device Model:' in line or 'Model Number:' in line:
disk_info['model'] = line.split(':', 1)[1].strip()
elif 'Serial Number:' in line:
disk_info['serial'] = line.split(':', 1)[1].strip()
elif 'Firmware Version:' in line:
disk_info['firmware'] = line.split(':', 1)[1].strip()
elif 'Rotation Rate:' in line:
disk_info['rotation_rate'] = line.split(':', 1)[1].strip()
elif 'Form Factor:' in line:
disk_info['form_factor'] = line.split(':', 1)[1].strip()
elif 'SATA Version is:' in line:
disk_info['sata_version'] = line.split(':', 1)[1].strip()
except Exception as e:
print(f"[v0] Error getting SMART info: {e}")
except Exception as e:
print(f"[v0] Error getting disk hardware info: {e}")
return disk_info
def get_hardware_info(): def get_hardware_info():
"""Get comprehensive hardware information""" """Get comprehensive hardware information"""
try: try: