Update AppImage

This commit is contained in:
MacRimi
2025-10-04 17:34:07 +02:00
parent 22aa8cdd6c
commit 54ff50ce68
2 changed files with 269 additions and 152 deletions

View File

@@ -202,9 +202,11 @@ export function StorageOverview() {
</Card> </Card>
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader>
<CardTitle className="text-sm font-medium">Avg Temperature</CardTitle> <CardTitle className="flex items-center gap-2">
<Thermometer className="h-4 w-4 text-muted-foreground" /> <Thermometer className="h-5 w-5" />
Avg Temperature
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className={`text-2xl font-bold ${getTempColor(avgTemp)}`}>{avgTemp > 0 ? `${avgTemp}°C` : "N/A"}</div> <div className={`text-2xl font-bold ${getTempColor(avgTemp)}`}>{avgTemp > 0 ? `${avgTemp}°C` : "N/A"}</div>
@@ -213,6 +215,68 @@ export function StorageOverview() {
</Card> </Card>
</div> </div>
{storageData.disks.some((disk) => disk.mountpoint) && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database className="h-5 w-5" />
Mounted Partitions
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{storageData.disks
.filter((disk) => disk.mountpoint)
.map((disk) => (
<div key={disk.name} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<div>
<h3 className="font-semibold">{disk.mountpoint}</h3>
<p className="text-sm text-muted-foreground">
/dev/{disk.name} ({disk.fstype})
</p>
</div>
{disk.usage_percent !== undefined && (
<span className="text-sm font-medium">{disk.usage_percent}%</span>
)}
</div>
{disk.usage_percent !== undefined && (
<div className="space-y-1">
<Progress
value={disk.usage_percent}
className={`h-2 ${
disk.usage_percent > 90
? "[&>div]:bg-red-500"
: disk.usage_percent > 75
? "[&>div]:bg-yellow-500"
: "[&>div]:bg-blue-500"
}`}
/>
<div className="flex justify-between text-xs text-muted-foreground">
<span
className={
disk.usage_percent > 90
? "text-red-400"
: disk.usage_percent > 75
? "text-yellow-400"
: "text-blue-400"
}
>
{disk.used} GB used
</span>
<span className="text-green-400">
{disk.available} GB free of {disk.total} GB
</span>
</div>
</div>
)}
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* ZFS Pools */} {/* ZFS Pools */}
{storageData.zfs_pools && storageData.zfs_pools.length > 0 && ( {storageData.zfs_pools && storageData.zfs_pools.length > 0 && (
<Card> <Card>

View File

@@ -496,7 +496,7 @@ def get_storage_info():
} }
def get_smart_data(disk_name): def get_smart_data(disk_name):
"""Get SMART data for a specific disk - Enhanced with better parsing logic from Home Assistant""" """Get SMART data for a specific disk - Enhanced with multiple device type attempts"""
smart_data = { smart_data = {
'temperature': 0, 'temperature': 0,
'health': 'unknown', 'health': 'unknown',
@@ -512,19 +512,39 @@ def get_smart_data(disk_name):
print(f"[v0] ===== Starting SMART data collection for /dev/{disk_name} =====") print(f"[v0] ===== Starting SMART data collection for /dev/{disk_name} =====")
try: try:
print(f"[v0] Step 1: Attempting JSON output for {disk_name}...") commands_to_try = [
result = subprocess.run(['smartctl', '-a', '-j', f'/dev/{disk_name}'], ['smartctl', '-a', '-j', f'/dev/{disk_name}'], # JSON output (preferred)
capture_output=True, text=True, timeout=10) ['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', '-j', '-d', 'scsi', f'/dev/{disk_name}'], # JSON with SCSI device type
['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', '-i', '-H', f'/dev/{disk_name}'], # Basic info + health only
]
print(f"[v0] smartctl return code: {result.returncode}") for cmd_index, cmd in enumerate(commands_to_try):
print(f"[v0] Attempt {cmd_index + 1}/{len(commands_to_try)}: Running command: {' '.join(cmd)}")
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
print(f"[v0] Command return code: {result.returncode}")
if result.stderr:
stderr_preview = result.stderr[:300].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 # smartctl returns: 0=OK, 2=SMART disabled, 4=threshold exceeded (still valid), 8=error log has errors
if result.returncode in [0, 4, 8] and result.stdout: if result.returncode in [0, 2, 4, 8] and result.stdout:
print(f"[v0] Got output ({len(result.stdout)} bytes)")
# Try JSON parsing first
if '-j' in cmd:
try: try:
print(f"[v0] Attempting JSON parse...") print(f"[v0] Attempting JSON parse...")
data = json.loads(result.stdout) data = json.loads(result.stdout)
print(f"[v0] JSON parse successful!") print(f"[v0] JSON parse successful!")
# Extract model
if 'model_name' in data: if 'model_name' in data:
smart_data['model'] = data['model_name'] smart_data['model'] = data['model_name']
print(f"[v0] Model: {smart_data['model']}") print(f"[v0] Model: {smart_data['model']}")
@@ -532,19 +552,23 @@ def get_smart_data(disk_name):
smart_data['model'] = data['model_family'] smart_data['model'] = data['model_family']
print(f"[v0] Model family: {smart_data['model']}") print(f"[v0] Model family: {smart_data['model']}")
# Extract serial
if 'serial_number' in data: if 'serial_number' in data:
smart_data['serial'] = data['serial_number'] smart_data['serial'] = data['serial_number']
print(f"[v0] Serial: {smart_data['serial']}") print(f"[v0] Serial: {smart_data['serial']}")
# Extract SMART status
if 'smart_status' in data and 'passed' in data['smart_status']: if 'smart_status' in data and 'passed' in data['smart_status']:
smart_data['smart_status'] = 'passed' if data['smart_status']['passed'] else 'failed' smart_data['smart_status'] = 'passed' if data['smart_status']['passed'] else 'failed'
smart_data['health'] = 'healthy' if data['smart_status']['passed'] else 'critical' smart_data['health'] = 'healthy' if data['smart_status']['passed'] else 'critical'
print(f"[v0] SMART status: {smart_data['smart_status']}") print(f"[v0] SMART status: {smart_data['smart_status']}, health: {smart_data['health']}")
# Extract temperature
if 'temperature' in data and 'current' in data['temperature']: if 'temperature' in data and 'current' in data['temperature']:
smart_data['temperature'] = data['temperature']['current'] smart_data['temperature'] = data['temperature']['current']
print(f"[v0] Temperature: {smart_data['temperature']}°C") print(f"[v0] Temperature: {smart_data['temperature']}°C")
# Parse ATA SMART attributes
if 'ata_smart_attributes' in data and 'table' in data['ata_smart_attributes']: if 'ata_smart_attributes' in data and 'table' in data['ata_smart_attributes']:
print(f"[v0] Parsing ATA SMART attributes...") print(f"[v0] Parsing ATA SMART attributes...")
for attr in data['ata_smart_attributes']['table']: for attr in data['ata_smart_attributes']['table']:
@@ -573,6 +597,7 @@ def get_smart_data(disk_name):
smart_data['crc_errors'] = raw_value smart_data['crc_errors'] = raw_value
print(f"[v0] CRC Errors (ID 199): {raw_value}") print(f"[v0] CRC Errors (ID 199): {raw_value}")
# Parse NVMe SMART data
if 'nvme_smart_health_information_log' in data: if 'nvme_smart_health_information_log' in data:
print(f"[v0] Parsing NVMe SMART data...") print(f"[v0] Parsing NVMe SMART data...")
nvme_data = data['nvme_smart_health_information_log'] nvme_data = data['nvme_smart_health_information_log']
@@ -583,8 +608,17 @@ def get_smart_data(disk_name):
smart_data['power_on_hours'] = nvme_data['power_on_hours'] smart_data['power_on_hours'] = nvme_data['power_on_hours']
print(f"[v0] NVMe Power On Hours: {smart_data['power_on_hours']}") 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})")
break
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
print(f"[v0] JSON parse failed: {e}, falling back to text parsing...") print(f"[v0] JSON parse failed: {e}, will try next command...")
# Text parsing fallback
if smart_data['model'] == 'Unknown' or smart_data['serial'] == 'Unknown':
print(f"[v0] Parsing text output...")
output = result.stdout output = result.stdout
# Get basic info # Get basic info
@@ -592,28 +626,36 @@ def get_smart_data(disk_name):
line = line.strip() line = line.strip()
if line.startswith('Device Model:') or line.startswith('Model Number:'): if line.startswith('Device Model:') or line.startswith('Model Number:'):
smart_data['model'] = line.split(':', 1)[1].strip() smart_data['model'] = line.split(':', 1)[1].strip()
print(f"[v0] Found model: {smart_data['model']}")
elif line.startswith('Serial Number:'): elif line.startswith('Serial Number:'):
smart_data['serial'] = line.split(':', 1)[1].strip() 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': elif line.startswith('Model Family:') and smart_data['model'] == 'Unknown':
smart_data['model'] = line.split(':', 1)[1].strip() smart_data['model'] = line.split(':', 1)[1].strip()
print(f"[v0] Found model family: {smart_data['model']}")
# Parse SMART status # Parse SMART status
if 'SMART overall-health self-assessment test result: PASSED' in output: if 'SMART overall-health self-assessment test result: PASSED' in output:
smart_data['smart_status'] = 'passed' smart_data['smart_status'] = 'passed'
smart_data['health'] = 'healthy' smart_data['health'] = 'healthy'
print(f"[v0] SMART status: PASSED")
elif 'SMART Health Status: OK' in output: # NVMe elif 'SMART Health Status: OK' in output: # NVMe
smart_data['smart_status'] = 'passed' smart_data['smart_status'] = 'passed'
smart_data['health'] = 'healthy' smart_data['health'] = 'healthy'
print(f"[v0] NVMe Health: OK")
elif 'SMART overall-health self-assessment test result: FAILED' in output: elif 'SMART overall-health self-assessment test result: FAILED' in output:
smart_data['smart_status'] = 'failed' smart_data['smart_status'] = 'failed'
smart_data['health'] = 'critical' smart_data['health'] = 'critical'
print(f"[v0] SMART status: FAILED")
# Parse SMART attributes table
in_attributes = False in_attributes = False
for line in output.split('\n'): for line in output.split('\n'):
line = line.strip() line = line.strip()
if 'ID# ATTRIBUTE_NAME' in line: if 'ID# ATTRIBUTE_NAME' in line:
in_attributes = True in_attributes = True
print(f"[v0] Found SMART attributes table")
continue continue
if in_attributes and line and not line.startswith('SMART'): if in_attributes and line and not line.startswith('SMART'):
@@ -628,43 +670,54 @@ def get_smart_data(disk_name):
# Handle different formats: "12345", "12345h", "12345 hours" # Handle different formats: "12345", "12345h", "12345 hours"
raw_clean = raw_value.split()[0].replace('h', '') raw_clean = raw_value.split()[0].replace('h', '')
smart_data['power_on_hours'] = int(raw_clean) 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': # Temperature
temp_str = raw_value.split()[0] temp_str = raw_value.split()[0]
smart_data['temperature'] = int(temp_str) smart_data['temperature'] = int(temp_str)
print(f"[v0] Temperature: {smart_data['temperature']}°C")
elif attr_id == '190': # Airflow Temperature elif attr_id == '190': # Airflow Temperature
if smart_data['temperature'] == 0: if smart_data['temperature'] == 0:
temp_str = raw_value.split()[0] temp_str = raw_value.split()[0]
smart_data['temperature'] = int(temp_str) smart_data['temperature'] = int(temp_str)
print(f"[v0] Airflow Temperature: {smart_data['temperature']}°C")
elif attr_id == '5': # Reallocated Sectors elif attr_id == '5': # Reallocated Sectors
smart_data['reallocated_sectors'] = int(raw_value) smart_data['reallocated_sectors'] = int(raw_value)
print(f"[v0] Reallocated Sectors: {smart_data['reallocated_sectors']}")
elif attr_id == '197': # Pending Sectors elif attr_id == '197': # Pending Sectors
smart_data['pending_sectors'] = int(raw_value) smart_data['pending_sectors'] = int(raw_value)
print(f"[v0] Pending Sectors: {smart_data['pending_sectors']}")
elif attr_id == '199': # CRC Errors elif attr_id == '199': # CRC Errors
smart_data['crc_errors'] = int(raw_value) smart_data['crc_errors'] = int(raw_value)
print(f"[v0] CRC Errors: {smart_data['crc_errors']}")
except (ValueError, IndexError) as e: except (ValueError, IndexError) as e:
continue continue
# Try to find temperature in other formats
if smart_data['temperature'] == 0: if smart_data['temperature'] == 0:
for line in output.split('\n'): for line in output.split('\n'):
if 'Temperature:' in line: if 'Temperature:' in line or 'Temperature_Celsius' in line:
try: try:
temp_str = line.split(':')[1].strip().split()[0] temp_str = line.split(':')[1].strip().split()[0]
smart_data['temperature'] = int(temp_str) smart_data['temperature'] = int(temp_str)
print(f"[v0] Found temperature: {smart_data['temperature']}°C")
break
except (ValueError, IndexError): except (ValueError, IndexError):
pass 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})")
break
else: else:
print(f"[v0] JSON command failed, trying basic info...") print(f"[v0] Command failed with return code {result.returncode}, trying next...")
result_info = subprocess.run(['smartctl', '-i', f'/dev/{disk_name}'],
capture_output=True, text=True, timeout=10) except subprocess.TimeoutExpired:
if result_info.stdout: print(f"[v0] Command timeout for attempt {cmd_index + 1}, trying next...")
for line in result_info.stdout.split('\n'): continue
line = line.strip() except Exception as e:
if line.startswith('Device Model:') or line.startswith('Model Number:'): print(f"[v0] Error in attempt {cmd_index + 1}: {type(e).__name__}: {e}")
smart_data['model'] = line.split(':', 1)[1].strip() continue
elif line.startswith('Serial Number:'):
smart_data['serial'] = line.split(':', 1)[1].strip()
if smart_data['reallocated_sectors'] > 0 or smart_data['pending_sectors'] > 0: if smart_data['reallocated_sectors'] > 0 or smart_data['pending_sectors'] > 0:
smart_data['health'] = 'warning' smart_data['health'] = 'warning'
@@ -685,10 +738,10 @@ def get_smart_data(disk_name):
except FileNotFoundError: except FileNotFoundError:
print(f"[v0] ERROR: smartctl not found - install smartmontools") print(f"[v0] ERROR: smartctl not found - install smartmontools")
except subprocess.TimeoutExpired:
print(f"[v0] ERROR: Timeout getting SMART data for {disk_name}")
except Exception as e: except Exception as e:
print(f"[v0] ERROR: Exception for {disk_name}: {type(e).__name__}: {e}") print(f"[v0] ERROR: Unexpected exception for {disk_name}: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
print(f"[v0] ===== Final SMART data for /dev/{disk_name}: {smart_data} =====") print(f"[v0] ===== Final SMART data for /dev/{disk_name}: {smart_data} =====")
return smart_data return smart_data