Update AppImage

This commit is contained in:
MacRimi
2025-10-14 18:51:39 +02:00
parent d3de7b95aa
commit 699c7df798
3 changed files with 395 additions and 85 deletions

View File

@@ -97,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 radeontop package." return "To get extended GPU monitoring information for AMD GPUs, please install amdgpu_top. You can download it from: https://github.com/Umio-Yasuno/amdgpu_top"
} }
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."
} }
@@ -142,6 +142,7 @@ 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)
useEffect(() => { useEffect(() => {
if (!selectedGPU) return if (!selectedGPU) return
@@ -1164,84 +1165,287 @@ export default function Hardware() {
</Card> </Card>
)} )}
{/* Power Supplies */}
{/* This section was moved to be grouped with Power Consumption */}
{/* UPS */} {/* UPS */}
{hardwareData?.ups && Object.keys(hardwareData.ups).length > 0 && hardwareData.ups.model && ( {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">
{hardwareData.ups.length} UPS
</Badge>
</div> </div>
<div className="space-y-4"> <div className="grid gap-4 md:grid-cols-2">
<div className="rounded-lg border border-border/30 bg-background/50 p-4"> {hardwareData.ups.map((ups: any, index: number) => {
<div className="flex items-center justify-between mb-4"> const batteryCharge =
<span className="text-sm font-medium">{hardwareData.ups.model}</span> ups.battery_charge_raw || Number.parseFloat(ups.battery_charge?.replace("%", "") || "0")
<Badge const loadPercent = ups.load_percent_raw || Number.parseFloat(ups.load_percent?.replace("%", "") || "0")
variant={hardwareData.ups.status === "OL" ? "default" : "destructive"}
className={ // Determine status badge color
hardwareData.ups.status === "OL" ? "bg-green-500/10 text-green-500 border-green-500/20" : "" const getStatusColor = (status: string) => {
if (!status) return "bg-gray-500/10 text-gray-500 border-gray-500/20"
const statusUpper = status.toUpperCase()
if (statusUpper.includes("OL")) return "bg-green-500/10 text-green-500 border-green-500/20"
if (statusUpper.includes("OB")) return "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
if (statusUpper.includes("LB")) return "bg-red-500/10 text-red-500 border-red-500/20"
return "bg-blue-500/10 text-blue-500 border-blue-500/20"
} }
return (
<div
key={index}
onClick={() => setSelectedUPS(ups)}
className="cursor-pointer rounded-lg border border-border/30 bg-background/50 p-4 transition-colors hover:bg-background/80"
> >
{hardwareData.ups.status} <div className="flex items-center justify-between mb-4">
</Badge> <div className="flex-1 min-w-0">
<span className="text-sm font-medium block truncate">{ups.model || ups.name}</span>
{ups.is_remote && <span className="text-xs text-muted-foreground">Remote: {ups.host}</span>}
</div>
<Badge className={getStatusColor(ups.status)}>{ups.status || "Unknown"}</Badge>
</div> </div>
<div className="grid gap-3 md:grid-cols-2"> <div className="grid gap-3 md:grid-cols-2">
{hardwareData.ups.battery_charge && ( {ups.battery_charge && (
<div className="space-y-1"> <div className="space-y-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">Battery Charge</span> <span className="text-xs text-muted-foreground">Battery Charge</span>
<span className="text-sm font-semibold text-green-500">{hardwareData.ups.battery_charge}</span> <span className="text-sm font-semibold text-green-500">{ups.battery_charge}</span>
</div> </div>
<Progress <Progress value={batteryCharge} className="h-2 [&>div]:bg-blue-500" />
value={Number.parseInt(hardwareData.ups.battery_charge.replace("%", ""))}
className="h-2 [&>div]:bg-blue-500"
/>
</div> </div>
)} )}
{hardwareData.ups.load_percent && ( {ups.load_percent && (
<div className="space-y-1"> <div className="space-y-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">Load</span> <span className="text-xs text-muted-foreground">Load</span>
<span className="text-sm font-semibold text-green-500">{hardwareData.ups.load_percent}</span> <span className="text-sm font-semibold text-green-500">{ups.load_percent}</span>
</div>
<Progress value={loadPercent} className="h-2 [&>div]:bg-blue-500" />
</div>
)}
{ups.time_left && (
<div>
<span className="text-xs text-muted-foreground">Runtime</span>
<div className="mt-1">
<Badge className="bg-green-500/10 text-green-500 border-green-500/20">{ups.time_left}</Badge>
</div>
</div>
)}
{ups.input_voltage && (
<div>
<span className="text-xs text-muted-foreground">Input Voltage</span>
<div className="mt-1">
<span className="text-sm font-medium text-green-500">{ups.input_voltage}</span>
</div>
</div>
)}
</div>
</div>
)
})}
</div>
</Card>
)}
<Dialog open={selectedUPS !== null} onOpenChange={() => setSelectedUPS(null)}>
<DialogContent className="max-w-3xl max-h-[85vh] overflow-y-auto">
{selectedUPS && (
<>
<DialogHeader className="pb-4 border-b border-border">
<DialogTitle>{selectedUPS.model || selectedUPS.name}</DialogTitle>
<DialogDescription>
UPS Detailed Information
{selectedUPS.is_remote && ` • Remote: ${selectedUPS.host}`}
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
{/* Status Overview */}
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
Status Overview
</h3>
<div className="grid gap-2">
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Status</span>
<Badge
className={
selectedUPS.status?.includes("OL")
? "bg-green-500/10 text-green-500 border-green-500/20"
: selectedUPS.status?.includes("OB")
? "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
: selectedUPS.status?.includes("LB")
? "bg-red-500/10 text-red-500 border-red-500/20"
: "bg-blue-500/10 text-blue-500 border-blue-500/20"
}
>
{selectedUPS.status || "Unknown"}
</Badge>
</div>
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Connection</span>
<Badge variant="outline">{selectedUPS.connection_type}</Badge>
</div>
{selectedUPS.host && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Host</span>
<span className="text-sm font-medium">{selectedUPS.host}</span>
</div>
)}
</div>
</div>
{/* Battery Information */}
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
Battery Information
</h3>
<div className="grid gap-3">
{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-semibold text-green-500">{selectedUPS.battery_charge}</span>
</div> </div>
<Progress <Progress
value={Number.parseInt(hardwareData.ups.load_percent.replace("%", ""))} value={
selectedUPS.battery_charge_raw ||
Number.parseFloat(selectedUPS.battery_charge.replace("%", ""))
}
className="h-2 [&>div]:bg-blue-500" className="h-2 [&>div]:bg-blue-500"
/> />
</div> </div>
)} )}
{selectedUPS.time_left && (
{hardwareData.ups.time_left && ( <div className="flex justify-between border-b border-border/50 pb-2">
<div> <span className="text-sm text-muted-foreground">Runtime Remaining</span>
<span className="text-xs text-muted-foreground">Runtime</span>
<div className="mt-1">
<Badge className="bg-green-500/10 text-green-500 border-green-500/20"> <Badge className="bg-green-500/10 text-green-500 border-green-500/20">
{hardwareData.ups.time_left} {selectedUPS.time_left}
</Badge> </Badge>
</div> </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> </div>
)} )}
{selectedUPS.battery_date && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Battery Date</span>
<span className="text-sm font-medium">{selectedUPS.battery_date}</span>
</div>
)}
</div>
</div>
{hardwareData.ups.line_voltage && ( {/* Input/Output Information */}
<div> <div>
<span className="text-xs text-muted-foreground">Input Voltage</span> <h3 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
<div className="mt-1"> Power Information
<Badge className="bg-green-500/10 text-green-500 border-green-500/20"> </h3>
{hardwareData.ups.line_voltage} <div className="grid gap-2 md:grid-cols-2">
</Badge> {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> </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 text-green-500">{selectedUPS.output_voltage}</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.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>
)}
{selectedUPS.load_percent && (
<div className="md:col-span-2 space-y-1">
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">Load</span>
<span className="text-sm font-semibold text-green-500">{selectedUPS.load_percent}</span>
</div>
<Progress
value={
selectedUPS.load_percent_raw || Number.parseFloat(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.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 text-blue-500">{selectedUPS.apparent_power}</span>
</div>
)}
</div>
</div>
{/* Device Information */}
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
Device Information
</h3>
<div className="grid gap-2">
{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.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">{selectedUPS.model}</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.firmware && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Firmware</span>
<span className="text-sm font-medium">{selectedUPS.firmware}</span>
</div>
)}
{selectedUPS.driver && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Driver</span>
<span className="font-mono text-sm text-green-500">{selectedUPS.driver}</span>
</div> </div>
)} )}
</div> </div>
</div> </div>
</div> </div>
</Card> </>
)} )}
</DialogContent>
</Dialog>
{/* Network Summary - Clickable */} {/* Network Summary - Clickable */}
{hardwareData?.pci_devices && {hardwareData?.pci_devices &&

View File

@@ -1549,20 +1549,72 @@ def get_ipmi_power():
} }
def get_ups_info(): def get_ups_info():
"""Get UPS information from NUT (upsc)""" """Get UPS information from NUT (upsc) - supports both local and remote UPS"""
ups_data = {} ups_list = []
try: try:
# First, list available UPS devices configured_ups = {}
try:
with open('/etc/nut/upsmon.conf', 'r') as f:
for line in f:
line = line.strip()
# Look for MONITOR lines: MONITOR ups@host powervalue username password type
if line.startswith('MONITOR') and not line.startswith('#'):
parts = line.split()
if len(parts) >= 2:
ups_spec = parts[1] # Format: upsname@hostname or just upsname
if '@' in ups_spec:
ups_name, ups_host = ups_spec.split('@', 1)
configured_ups[ups_spec] = {
'name': ups_name,
'host': ups_host,
'is_remote': ups_host not in ['localhost', '127.0.0.1', '::1']
}
else:
configured_ups[ups_spec] = {
'name': ups_spec,
'host': 'localhost',
'is_remote': False
}
except FileNotFoundError:
print("[v0] /etc/nut/upsmon.conf not found")
except Exception as e:
print(f"[v0] Error reading upsmon.conf: {e}")
# Get list of locally available UPS
local_ups = []
try:
result = subprocess.run(['upsc', '-l'], capture_output=True, text=True, timeout=5) result = subprocess.run(['upsc', '-l'], capture_output=True, text=True, timeout=5)
if result.returncode == 0: if result.returncode == 0:
ups_list = result.stdout.strip().split('\n') local_ups = [ups.strip() for ups in result.stdout.strip().split('\n') if ups.strip()]
if ups_list and ups_list[0]: except Exception as e:
ups_name = ups_list[0] print(f"[v0] Error listing local UPS: {e}")
print(f"[v0] Found UPS: {ups_name}")
# Combine configured and local UPS
all_ups = set()
# Add configured UPS
for ups_spec, ups_info in configured_ups.items():
all_ups.add((ups_spec, ups_info['host'], ups_info['is_remote']))
# Add local UPS that might not be in config
for ups_name in local_ups:
all_ups.add((ups_name, 'localhost', False))
# Get detailed info for each UPS
for ups_spec, ups_host, is_remote in all_ups:
try:
ups_data = {
'name': ups_spec.split('@')[0] if '@' in ups_spec else ups_spec,
'host': ups_host,
'is_remote': is_remote,
'connection_type': 'Remote (NUT)' if is_remote else 'Local'
}
# Get detailed UPS info using upsc
cmd = ['upsc', ups_spec] if '@' in ups_spec else ['upsc', ups_spec, ups_host] if is_remote else ['upsc', ups_spec]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
# Get detailed UPS info
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'):
if ':' in line: if ':' in line:
@@ -1570,35 +1622,70 @@ def get_ups_info():
key = key.strip() key = key.strip()
value = value.strip() value = value.strip()
# Map common UPS variables # Store all UPS variables for detailed modal
ups_data[key] = value
# Map common variables for quick access
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
elif key == 'ups.status': elif key == 'ups.status':
ups_data['status'] = value ups_data['status'] = value
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.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['time_left_seconds'] = runtime_sec
except ValueError: except ValueError:
ups_data['time_left'] = value ups_data['time_left'] = value
elif key == 'battery.voltage':
ups_data['battery_voltage'] = f"{value}V"
elif key == 'battery.date':
ups_data['battery_date'] = value
elif key == 'ups.load': elif key == 'ups.load':
ups_data['load_percent'] = f"{value}%" ups_data['load_percent'] = f"{value}%"
ups_data['load_percent_raw'] = float(value)
elif key == 'input.voltage': elif key == 'input.voltage':
ups_data['line_voltage'] = f"{value}V" ups_data['input_voltage'] = f"{value}V"
elif key == 'input.frequency':
ups_data['input_frequency'] = f"{value}Hz"
elif key == 'output.voltage':
ups_data['output_voltage'] = f"{value}V"
elif key == 'output.frequency':
ups_data['output_frequency'] = f"{value}Hz"
elif key == 'ups.realpower': elif key == 'ups.realpower':
ups_data['real_power'] = f"{value}W" ups_data['real_power'] = f"{value}W"
elif key == 'ups.power':
ups_data['apparent_power'] = f"{value}VA"
elif key == 'ups.firmware':
ups_data['firmware'] = value
elif key == 'driver.name':
ups_data['driver'] = value
ups_list.append(ups_data)
print(f"[v0] UPS found: {ups_data.get('model', 'Unknown')} ({ups_data['connection_type']})")
else:
print(f"[v0] Failed to get info for UPS: {ups_spec}")
except Exception as e:
print(f"[v0] Error getting UPS info for {ups_spec}: {e}")
print(f"[v0] UPS data: {ups_data}")
except FileNotFoundError: except FileNotFoundError:
print("[v0] upsc not found") print("[v0] upsc not found")
except Exception as e: except Exception as e:
print(f"[v0] Error getting UPS info: {e}") print(f"[v0] Error in get_ups_info: {e}")
return ups_list
return ups_data
def identify_temperature_sensor(sensor_name, adapter): def identify_temperature_sensor(sensor_name, adapter):
"""Identify what a temperature sensor corresponds to""" """Identify what a temperature sensor corresponds to"""

View File

@@ -76,12 +76,31 @@ export interface PowerSupply {
export interface UPS { export interface UPS {
name: string name: string
host?: string
is_remote?: boolean
connection_type?: string
status: string status: string
battery_charge?: number model?: string
battery_runtime?: number manufacturer?: string
load?: number serial?: string
input_voltage?: number device_type?: string
output_voltage?: number firmware?: string
driver?: string
battery_charge?: string
battery_charge_raw?: number
battery_voltage?: string
battery_date?: string
time_left?: string
time_left_seconds?: number
load_percent?: string
load_percent_raw?: number
input_voltage?: string
input_frequency?: string
output_voltage?: string
output_frequency?: string
real_power?: string
apparent_power?: string
[key: string]: any
} }
export interface GPU { export interface GPU {
@@ -179,7 +198,7 @@ export interface HardwareData {
gpus?: GPU[] gpus?: GPU[]
fans?: Fan[] fans?: Fan[]
power_supplies?: PowerSupply[] power_supplies?: PowerSupply[]
ups?: UPS ups?: UPS | UPS[]
} }
export const fetcher = (url: string) => fetch(url).then((res) => res.json()) export const fetcher = (url: string) => fetch(url).then((res) => res.json())