mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-10-11 04:16:17 +00:00
Update AppImage
This commit is contained in:
@@ -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 (
|
||||
<div className="space-y-6">
|
||||
@@ -215,7 +220,7 @@ export function StorageOverview() {
|
||||
<Database className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{storageData.used.toFixed(1)} GB</div>
|
||||
<div className="text-2xl font-bold">{totalProxmoxUsed.toFixed(1)} GB</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">{usagePercent}% used</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -414,25 +419,25 @@ export function StorageOverview() {
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
{disk.size && (
|
||||
<div>
|
||||
<p className="text-muted-foreground">Size</p>
|
||||
<p className="text-sm text-muted-foreground">Size</p>
|
||||
<p className="font-medium">{disk.size}</p>
|
||||
</div>
|
||||
)}
|
||||
{disk.smart_status && disk.smart_status !== "unknown" && (
|
||||
<div>
|
||||
<p className="text-muted-foreground">SMART Status</p>
|
||||
<p className="text-sm text-muted-foreground">SMART Status</p>
|
||||
<p className="font-medium capitalize">{disk.smart_status}</p>
|
||||
</div>
|
||||
)}
|
||||
{disk.power_on_hours !== undefined && disk.power_on_hours > 0 && (
|
||||
<div>
|
||||
<p className="text-muted-foreground">Power On Time</p>
|
||||
<p className="text-sm text-muted-foreground">Power On Time</p>
|
||||
<p className="font-medium">{formatHours(disk.power_on_hours)}</p>
|
||||
</div>
|
||||
)}
|
||||
{disk.serial && disk.serial !== "Unknown" && (
|
||||
<div>
|
||||
<p className="text-muted-foreground">Serial</p>
|
||||
<p className="text-sm text-muted-foreground">Serial</p>
|
||||
<p className="font-medium text-xs">{disk.serial}</p>
|
||||
</div>
|
||||
)}
|
||||
|
@@ -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
|
||||
|
||||
# Try JSON parsing first
|
||||
if has_output:
|
||||
print(f"[v0] Got output ({len(result.stdout)} bytes), attempting to parse...")
|
||||
|
||||
# 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:
|
||||
# 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 '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:
|
||||
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:
|
||||
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: {smart_data['temperature']}°C")
|
||||
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,6 +745,7 @@ def get_smart_data(disk_name):
|
||||
continue
|
||||
|
||||
if smart_data['reallocated_sectors'] > 0 or smart_data['pending_sectors'] > 0:
|
||||
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:
|
||||
@@ -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")
|
||||
|
Reference in New Issue
Block a user