diff --git a/AppImage/components/storage-overview.tsx b/AppImage/components/storage-overview.tsx index a7b1182..ec396fa 100644 --- a/AppImage/components/storage-overview.tsx +++ b/AppImage/components/storage-overview.tsx @@ -191,8 +191,13 @@ export function StorageOverview() { ? Math.round(disksWithTemp.reduce((sum, disk) => sum + disk.temperature, 0) / disksWithTemp.length) : 0 + const totalProxmoxUsed = + proxmoxStorage && proxmoxStorage.storage + ? proxmoxStorage.storage.reduce((sum, storage) => sum + storage.used, 0) + : 0 + const usagePercent = - storageData.total > 0 ? ((storageData.used / (storageData.total * 1024)) * 100).toFixed(2) : "0.00" + storageData.total > 0 ? ((totalProxmoxUsed / (storageData.total * 1024)) * 100).toFixed(2) : "0.00" return (
@@ -215,7 +220,7 @@ export function StorageOverview() { -
{storageData.used.toFixed(1)} GB
+
{totalProxmoxUsed.toFixed(1)} GB

{usagePercent}% used

@@ -414,25 +419,25 @@ export function StorageOverview() {
{disk.size && (
-

Size

+

Size

{disk.size}

)} {disk.smart_status && disk.smart_status !== "unknown" && (
-

SMART Status

+

SMART Status

{disk.smart_status}

)} {disk.power_on_hours !== undefined && disk.power_on_hours > 0 && (
-

Power On Time

+

Power On Time

{formatHours(disk.power_on_hours)}

)} {disk.serial && disk.serial !== "Unknown" && (
-

Serial

+

Serial

{disk.serial}

)} diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py index b096a38..8b5e5eb 100644 --- a/AppImage/scripts/flask_server.py +++ b/AppImage/scripts/flask_server.py @@ -362,6 +362,11 @@ def get_storage_info(): parts = line.split() if len(parts) >= 3 and parts[2] == 'disk': disk_name = parts[0] + + if disk_name.startswith('zd'): + print(f"[v0] Skipping ZFS zvol device: {disk_name}") + continue + disk_size_bytes = int(parts[1]) disk_size_gb = disk_size_bytes / (1024**3) disk_size_tb = disk_size_bytes / (1024**4) @@ -516,17 +521,17 @@ def get_smart_data(disk_name): ['smartctl', '-a', '-j', f'/dev/{disk_name}'], # JSON output (preferred) ['smartctl', '-a', '-j', '-d', 'ata', f'/dev/{disk_name}'], # JSON with ATA device type ['smartctl', '-a', '-j', '-d', 'sat', f'/dev/{disk_name}'], # JSON with SAT device type + ['smartctl', '-a', f'/dev/{disk_name}'], # Text output (fallback) + ['smartctl', '-a', '-d', 'ata', f'/dev/{disk_name}'], # Text with ATA device type + ['smartctl', '-a', '-d', 'sat', f'/dev/{disk_name}'], # Text with SAT device type + ['smartctl', '-i', '-H', '-A', f'/dev/{disk_name}'], # Info + Health + Attributes + ['smartctl', '-i', '-H', '-A', '-d', 'ata', f'/dev/{disk_name}'], # With ATA + ['smartctl', '-i', '-H', '-A', '-d', 'sat', f'/dev/{disk_name}'], # With SAT ['smartctl', '-a', '-j', '-d', 'scsi', f'/dev/{disk_name}'], # JSON with SCSI device type ['smartctl', '-a', '-j', '-d', 'sat,12', f'/dev/{disk_name}'], # SAT with 12-byte commands ['smartctl', '-a', '-j', '-d', 'sat,16', f'/dev/{disk_name}'], # SAT with 16-byte commands - ['smartctl', '-a', f'/dev/{disk_name}'], # Text output - ['smartctl', '-a', '-d', 'ata', f'/dev/{disk_name}'], # Text with ATA device type - ['smartctl', '-a', '-d', 'sat', f'/dev/{disk_name}'], # Text with SAT device type ['smartctl', '-a', '-d', 'sat,12', f'/dev/{disk_name}'], # Text SAT with 12-byte commands ['smartctl', '-a', '-d', 'sat,16', f'/dev/{disk_name}'], # Text SAT with 16-byte commands - ['smartctl', '-i', '-H', f'/dev/{disk_name}'], # Basic info + health only - ['smartctl', '-i', '-H', '-d', 'ata', f'/dev/{disk_name}'], # Basic with ATA - ['smartctl', '-i', '-H', '-d', 'sat', f'/dev/{disk_name}'], # Basic with SAT ] for cmd_index, cmd in enumerate(commands_to_try): @@ -536,14 +541,18 @@ def get_smart_data(disk_name): print(f"[v0] Command return code: {result.returncode}") if result.stderr: - stderr_preview = result.stderr[:300].replace('\n', ' ') + stderr_preview = result.stderr[:200].replace('\n', ' ') print(f"[v0] stderr: {stderr_preview}") - # smartctl returns: 0=OK, 2=SMART disabled, 4=threshold exceeded (still valid), 8=error log has errors - if result.returncode in [0, 2, 4, 8] and result.stdout: - print(f"[v0] Got output ({len(result.stdout)} bytes)") + # smartctl returns: 0=OK, 2=SMART disabled, 4=threshold exceeded, 8=error log has errors + # 64=device open failed (but sometimes still has partial data) + # We'll try to parse ANY output if stdout is not empty + has_output = result.stdout and len(result.stdout.strip()) > 50 + + if has_output: + print(f"[v0] Got output ({len(result.stdout)} bytes), attempting to parse...") - # Try JSON parsing first + # Try JSON parsing first (if -j flag was used) if '-j' in cmd: try: print(f"[v0] Attempting JSON parse...") @@ -581,7 +590,6 @@ def get_smart_data(disk_name): attr_id = attr.get('id') raw_value = attr.get('raw', {}).get('value', 0) - # ID mapping from Home Assistant coordinator if attr_id == 9: # Power_On_Hours smart_data['power_on_hours'] = raw_value print(f"[v0] Power On Hours (ID 9): {raw_value}") @@ -615,77 +623,97 @@ def get_smart_data(disk_name): print(f"[v0] NVMe Power On Hours: {smart_data['power_on_hours']}") # If we got good data, break out of the loop - if smart_data['model'] != 'Unknown' or smart_data['serial'] != 'Unknown': - print(f"[v0] Successfully extracted data from JSON (attempt {cmd_index + 1})") + if smart_data['model'] != 'Unknown' and smart_data['serial'] != 'Unknown': + print(f"[v0] Successfully extracted complete data from JSON (attempt {cmd_index + 1})") break except json.JSONDecodeError as e: - print(f"[v0] JSON parse failed: {e}, will try next command...") + print(f"[v0] JSON parse failed: {e}, trying text parsing...") - # Text parsing fallback - if smart_data['model'] == 'Unknown' or smart_data['serial'] == 'Unknown': - print(f"[v0] Parsing text output...") + if smart_data['model'] == 'Unknown' or smart_data['serial'] == 'Unknown' or smart_data['temperature'] == 0: + print(f"[v0] Parsing text output (model={smart_data['model']}, serial={smart_data['serial']}, temp={smart_data['temperature']})...") output = result.stdout # Get basic info for line in output.split('\n'): line = line.strip() - if line.startswith('Device Model:') or line.startswith('Model Number:'): + + # Model detection + if (line.startswith('Device Model:') or line.startswith('Model Number:')) and smart_data['model'] == 'Unknown': smart_data['model'] = line.split(':', 1)[1].strip() print(f"[v0] Found model: {smart_data['model']}") - elif line.startswith('Serial Number:'): - smart_data['serial'] = line.split(':', 1)[1].strip() - print(f"[v0] Found serial: {smart_data['serial']}") elif line.startswith('Model Family:') and smart_data['model'] == 'Unknown': smart_data['model'] = line.split(':', 1)[1].strip() print(f"[v0] Found model family: {smart_data['model']}") - - # Parse SMART status - if 'SMART overall-health self-assessment test result: PASSED' in output: - smart_data['smart_status'] = 'passed' - smart_data['health'] = 'healthy' - print(f"[v0] SMART status: PASSED") - elif 'SMART Health Status: OK' in output: # NVMe - smart_data['smart_status'] = 'passed' - smart_data['health'] = 'healthy' - print(f"[v0] NVMe Health: OK") - elif 'SMART overall-health self-assessment test result: FAILED' in output: - smart_data['smart_status'] = 'failed' - smart_data['health'] = 'critical' - print(f"[v0] SMART status: FAILED") + + # Serial detection + elif line.startswith('Serial Number:') and smart_data['serial'] == 'Unknown': + smart_data['serial'] = line.split(':', 1)[1].strip() + print(f"[v0] Found serial: {smart_data['serial']}") + + # SMART status detection + elif 'SMART overall-health self-assessment test result:' in line: + if 'PASSED' in line: + smart_data['smart_status'] = 'passed' + smart_data['health'] = 'healthy' + print(f"[v0] SMART status: PASSED") + elif 'FAILED' in line: + smart_data['smart_status'] = 'failed' + smart_data['health'] = 'critical' + print(f"[v0] SMART status: FAILED") + + # NVMe health + elif 'SMART Health Status:' in line: + if 'OK' in line: + smart_data['smart_status'] = 'passed' + smart_data['health'] = 'healthy' + print(f"[v0] NVMe Health: OK") + + # Temperature detection (various formats) + elif 'Current Temperature:' in line and smart_data['temperature'] == 0: + try: + temp_str = line.split(':')[1].strip().split()[0] + smart_data['temperature'] = int(temp_str) + print(f"[v0] Found temperature: {smart_data['temperature']}°C") + except (ValueError, IndexError): + pass # Parse SMART attributes table in_attributes = False for line in output.split('\n'): line = line.strip() - if 'ID# ATTRIBUTE_NAME' in line: + if 'ID# ATTRIBUTE_NAME' in line or 'ID#' in line and 'ATTRIBUTE_NAME' in line: in_attributes = True print(f"[v0] Found SMART attributes table") continue - if in_attributes and line and not line.startswith('SMART'): + if in_attributes: + # Stop at empty line or next section + if not line or line.startswith('SMART') or line.startswith('==='): + in_attributes = False + continue + parts = line.split() if len(parts) >= 10: try: attr_id = parts[0] - raw_value = parts[9] + # Raw value is typically the last column + raw_value = parts[-1] - # Same ID mapping as JSON parsing + # Parse based on attribute ID if attr_id == '9': # Power On Hours - # Handle different formats: "12345", "12345h", "12345 hours" - raw_clean = raw_value.split()[0].replace('h', '') + raw_clean = raw_value.split()[0].replace('h', '').replace(',', '') smart_data['power_on_hours'] = int(raw_clean) print(f"[v0] Power On Hours: {smart_data['power_on_hours']}") - elif attr_id == '194': # Temperature + elif attr_id == '194' and smart_data['temperature'] == 0: # Temperature temp_str = raw_value.split()[0] smart_data['temperature'] = int(temp_str) - print(f"[v0] Temperature: {smart_data['temperature']}°C") - elif attr_id == '190': # Airflow Temperature - if smart_data['temperature'] == 0: - temp_str = raw_value.split()[0] - smart_data['temperature'] = int(temp_str) - print(f"[v0] Airflow Temperature: {smart_data['temperature']}°C") + print(f"[v0] Temperature (attr 194): {smart_data['temperature']}°C") + elif attr_id == '190' and smart_data['temperature'] == 0: # Airflow Temperature + temp_str = raw_value.split()[0] + smart_data['temperature'] = int(temp_str) + print(f"[v0] Airflow Temperature (attr 190): {smart_data['temperature']}°C") elif attr_id == '5': # Reallocated Sectors smart_data['reallocated_sectors'] = int(raw_value) print(f"[v0] Reallocated Sectors: {smart_data['reallocated_sectors']}") @@ -697,26 +725,17 @@ def get_smart_data(disk_name): print(f"[v0] CRC Errors: {smart_data['crc_errors']}") except (ValueError, IndexError) as e: + print(f"[v0] Error parsing attribute line '{line}': {e}") continue - # Try to find temperature in other formats - if smart_data['temperature'] == 0: - for line in output.split('\n'): - if 'Temperature:' in line or 'Temperature_Celsius' in line: - try: - temp_str = line.split(':')[1].strip().split()[0] - smart_data['temperature'] = int(temp_str) - print(f"[v0] Found temperature: {smart_data['temperature']}°C") - break - except (ValueError, IndexError): - pass - - # If we got some data, break - if smart_data['model'] != 'Unknown' or smart_data['serial'] != 'Unknown': - print(f"[v0] Successfully extracted data from text output (attempt {cmd_index + 1})") + # If we got complete data, break + if smart_data['model'] != 'Unknown' and smart_data['serial'] != 'Unknown': + print(f"[v0] Successfully extracted complete data from text output (attempt {cmd_index + 1})") break + elif smart_data['model'] != 'Unknown' or smart_data['serial'] != 'Unknown': + print(f"[v0] Extracted partial data from text output, continuing to next attempt...") else: - print(f"[v0] Command failed with return code {result.returncode}, trying next...") + print(f"[v0] No usable output (return code {result.returncode}), trying next command...") except subprocess.TimeoutExpired: print(f"[v0] Command timeout for attempt {cmd_index + 1}, trying next...") @@ -726,7 +745,8 @@ def get_smart_data(disk_name): continue if smart_data['reallocated_sectors'] > 0 or smart_data['pending_sectors'] > 0: - smart_data['health'] = 'warning' + if smart_data['health'] == 'healthy': + smart_data['health'] = 'warning' print(f"[v0] Health: WARNING (reallocated/pending sectors)") if smart_data['reallocated_sectors'] > 10 or smart_data['pending_sectors'] > 10: smart_data['health'] = 'critical' @@ -735,12 +755,14 @@ def get_smart_data(disk_name): smart_data['health'] = 'critical' print(f"[v0] Health: CRITICAL (SMART failed)") - # Temperature-based health + # Temperature-based health (only if we have a valid temperature) if smart_data['health'] == 'healthy' and smart_data['temperature'] > 0: if smart_data['temperature'] >= 70: smart_data['health'] = 'critical' + print(f"[v0] Health: CRITICAL (temperature {smart_data['temperature']}°C)") elif smart_data['temperature'] >= 60: smart_data['health'] = 'warning' + print(f"[v0] Health: WARNING (temperature {smart_data['temperature']}°C)") except FileNotFoundError: print(f"[v0] ERROR: smartctl not found - install smartmontools")