From 699c7df7988c43581031c9fede1029aa2a1d4879 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Tue, 14 Oct 2025 18:51:39 +0200 Subject: [PATCH] Update AppImage --- AppImage/components/hardware.tsx | 328 +++++++++++++++++++++++++------ AppImage/scripts/flask_server.py | 121 ++++++++++-- AppImage/types/hardware.ts | 31 ++- 3 files changed, 395 insertions(+), 85 deletions(-) diff --git a/AppImage/components/hardware.tsx b/AppImage/components/hardware.tsx index 9bd0137..4ea0864 100644 --- a/AppImage/components/hardware.tsx +++ b/AppImage/components/hardware.tsx @@ -97,7 +97,7 @@ const getMonitoringToolRecommendation = (vendor: string): string => { } 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." } @@ -142,6 +142,7 @@ export default function Hardware() { const [selectedPCIDevice, setSelectedPCIDevice] = useState(null) const [selectedDisk, setSelectedDisk] = useState(null) const [selectedNetwork, setSelectedNetwork] = useState(null) + const [selectedUPS, setSelectedUPS] = useState(null) useEffect(() => { if (!selectedGPU) return @@ -1164,85 +1165,288 @@ export default function Hardware() { )} - {/* Power Supplies */} - {/* This section was moved to be grouped with Power Consumption */} - {/* UPS */} - {hardwareData?.ups && Object.keys(hardwareData.ups).length > 0 && hardwareData.ups.model && ( + {hardwareData?.ups && Array.isArray(hardwareData.ups) && hardwareData.ups.length > 0 && (

UPS Status

+ + {hardwareData.ups.length} UPS +
-
-
-
- {hardwareData.ups.model} - + {hardwareData.ups.map((ups: any, index: number) => { + const batteryCharge = + ups.battery_charge_raw || Number.parseFloat(ups.battery_charge?.replace("%", "") || "0") + const loadPercent = ups.load_percent_raw || Number.parseFloat(ups.load_percent?.replace("%", "") || "0") + + // Determine status badge color + 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 ( +
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} - -
- -
- {hardwareData.ups.battery_charge && ( -
-
- Battery Charge - {hardwareData.ups.battery_charge} +
+
+ {ups.model || ups.name} + {ups.is_remote && Remote: {ups.host}}
- + {ups.status || "Unknown"}
- )} - {hardwareData.ups.load_percent && ( -
-
- Load - {hardwareData.ups.load_percent} -
- -
- )} +
+ {ups.battery_charge && ( +
+
+ Battery Charge + {ups.battery_charge} +
+ +
+ )} - {hardwareData.ups.time_left && ( -
- Runtime -
- - {hardwareData.ups.time_left} - -
-
- )} + {ups.load_percent && ( +
+
+ Load + {ups.load_percent} +
+ +
+ )} - {hardwareData.ups.line_voltage && ( -
- Input Voltage -
- - {hardwareData.ups.line_voltage} - -
+ {ups.time_left && ( +
+ Runtime +
+ {ups.time_left} +
+
+ )} + + {ups.input_voltage && ( +
+ Input Voltage +
+ {ups.input_voltage} +
+
+ )}
- )} -
-
+
+ ) + })}
)} + setSelectedUPS(null)}> + + {selectedUPS && ( + <> + + {selectedUPS.model || selectedUPS.name} + + UPS Detailed Information + {selectedUPS.is_remote && ` • Remote: ${selectedUPS.host}`} + + + +
+ {/* Status Overview */} +
+

+ Status Overview +

+
+
+ Status + + {selectedUPS.status || "Unknown"} + +
+
+ Connection + {selectedUPS.connection_type} +
+ {selectedUPS.host && ( +
+ Host + {selectedUPS.host} +
+ )} +
+
+ + {/* Battery Information */} +
+

+ Battery Information +

+
+ {selectedUPS.battery_charge && ( +
+
+ Charge Level + {selectedUPS.battery_charge} +
+ +
+ )} + {selectedUPS.time_left && ( +
+ Runtime Remaining + + {selectedUPS.time_left} + +
+ )} + {selectedUPS.battery_voltage && ( +
+ Battery Voltage + {selectedUPS.battery_voltage} +
+ )} + {selectedUPS.battery_date && ( +
+ Battery Date + {selectedUPS.battery_date} +
+ )} +
+
+ + {/* Input/Output Information */} +
+

+ Power Information +

+
+ {selectedUPS.input_voltage && ( +
+ Input Voltage + {selectedUPS.input_voltage} +
+ )} + {selectedUPS.output_voltage && ( +
+ Output Voltage + {selectedUPS.output_voltage} +
+ )} + {selectedUPS.input_frequency && ( +
+ Input Frequency + {selectedUPS.input_frequency} +
+ )} + {selectedUPS.output_frequency && ( +
+ Output Frequency + {selectedUPS.output_frequency} +
+ )} + {selectedUPS.load_percent && ( +
+
+ Load + {selectedUPS.load_percent} +
+ +
+ )} + {selectedUPS.real_power && ( +
+ Real Power + {selectedUPS.real_power} +
+ )} + {selectedUPS.apparent_power && ( +
+ Apparent Power + {selectedUPS.apparent_power} +
+ )} +
+
+ + {/* Device Information */} +
+

+ Device Information +

+
+ {selectedUPS.manufacturer && ( +
+ Manufacturer + {selectedUPS.manufacturer} +
+ )} + {selectedUPS.model && ( +
+ Model + {selectedUPS.model} +
+ )} + {selectedUPS.serial && ( +
+ Serial Number + {selectedUPS.serial} +
+ )} + {selectedUPS.firmware && ( +
+ Firmware + {selectedUPS.firmware} +
+ )} + {selectedUPS.driver && ( +
+ Driver + {selectedUPS.driver} +
+ )} +
+
+
+ + )} +
+
+ {/* Network Summary - Clickable */} {hardwareData?.pci_devices && hardwareData.pci_devices.filter((d) => d.type.toLowerCase().includes("network")).length > 0 && ( diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py index 787e442..94748d0 100644 --- a/AppImage/scripts/flask_server.py +++ b/AppImage/scripts/flask_server.py @@ -1549,20 +1549,72 @@ def get_ipmi_power(): } def get_ups_info(): - """Get UPS information from NUT (upsc)""" - ups_data = {} + """Get UPS information from NUT (upsc) - supports both local and remote UPS""" + ups_list = [] try: - # First, list available UPS devices - result = subprocess.run(['upsc', '-l'], capture_output=True, text=True, timeout=5) - if result.returncode == 0: - ups_list = result.stdout.strip().split('\n') - if ups_list and ups_list[0]: - ups_name = ups_list[0] - print(f"[v0] Found UPS: {ups_name}") + 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) + if result.returncode == 0: + local_ups = [ups.strip() for ups in result.stdout.strip().split('\n') if ups.strip()] + except Exception as e: + print(f"[v0] Error listing local UPS: {e}") + + # 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: for line in result.stdout.split('\n'): if ':' in line: @@ -1570,35 +1622,70 @@ def get_ups_info(): key = key.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': 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': ups_data['status'] = value elif key == 'battery.charge': ups_data['battery_charge'] = f"{value}%" + ups_data['battery_charge_raw'] = float(value) elif key == 'battery.runtime': - # Convert seconds to minutes try: runtime_sec = int(value) runtime_min = runtime_sec // 60 ups_data['time_left'] = f"{runtime_min} minutes" + ups_data['time_left_seconds'] = runtime_sec except ValueError: 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': ups_data['load_percent'] = f"{value}%" + ups_data['load_percent_raw'] = float(value) 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': 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 - print(f"[v0] UPS data: {ups_data}") + 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}") + except FileNotFoundError: print("[v0] upsc not found") except Exception as e: - print(f"[v0] Error getting UPS info: {e}") + print(f"[v0] Error in get_ups_info: {e}") - return ups_data + return ups_list + def identify_temperature_sensor(sensor_name, adapter): """Identify what a temperature sensor corresponds to""" diff --git a/AppImage/types/hardware.ts b/AppImage/types/hardware.ts index 17b8596..b80d500 100644 --- a/AppImage/types/hardware.ts +++ b/AppImage/types/hardware.ts @@ -76,12 +76,31 @@ export interface PowerSupply { export interface UPS { name: string + host?: string + is_remote?: boolean + connection_type?: string status: string - battery_charge?: number - battery_runtime?: number - load?: number - input_voltage?: number - output_voltage?: number + model?: string + manufacturer?: string + serial?: string + device_type?: string + 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 { @@ -179,7 +198,7 @@ export interface HardwareData { gpus?: GPU[] fans?: Fan[] power_supplies?: PowerSupply[] - ups?: UPS + ups?: UPS | UPS[] } export const fetcher = (url: string) => fetch(url).then((res) => res.json())