Update AppImage

This commit is contained in:
MacRimi
2025-10-14 22:14:48 +02:00
parent 04304f8283
commit 996dcc4b23
2 changed files with 189 additions and 16 deletions

View File

@@ -28,6 +28,11 @@ interface DiskInfo {
crc_errors?: number crc_errors?: number
rotation_rate?: number rotation_rate?: number
power_cycles?: number power_cycles?: number
percentage_used?: number // NVMe: Percentage Used (0-100)
media_wearout_indicator?: number // SSD: Media Wearout Indicator
wear_leveling_count?: number // SSD: Wear Leveling Count
total_lbas_written?: number // SSD/NVMe: Total LBAs Written (GB)
ssd_life_left?: number // SSD: SSD Life Left percentage
} }
interface ZFSPool { interface ZFSPool {
@@ -264,6 +269,64 @@ export function StorageOverview() {
} }
} }
const getWearIndicator = (disk: DiskInfo): { value: number; label: string } | null => {
const diskType = getDiskType(disk.name, disk.rotation_rate)
if (diskType === "NVMe" && disk.percentage_used !== undefined && disk.percentage_used !== null) {
return { value: disk.percentage_used, label: "Percentage Used" }
}
if (diskType === "SSD") {
// Prioridad: Media Wearout Indicator > Wear Leveling Count > SSD Life Left
if (disk.media_wearout_indicator !== undefined && disk.media_wearout_indicator !== null) {
return { value: disk.media_wearout_indicator, label: "Media Wearout" }
}
if (disk.wear_leveling_count !== undefined && disk.wear_leveling_count !== null) {
return { value: disk.wear_leveling_count, label: "Wear Level" }
}
if (disk.ssd_life_left !== undefined && disk.ssd_life_left !== null) {
return { value: 100 - disk.ssd_life_left, label: "Life Used" }
}
}
return null
}
const getWearColor = (wearPercent: number): string => {
if (wearPercent <= 50) return "text-green-500"
if (wearPercent <= 80) return "text-yellow-500"
return "text-red-500"
}
const getEstimatedLifeRemaining = (disk: DiskInfo): string | null => {
const wearIndicator = getWearIndicator(disk)
if (!wearIndicator || !disk.power_on_hours || disk.power_on_hours === 0) {
return null
}
const wearPercent = wearIndicator.value
const hoursUsed = disk.power_on_hours
// Si el desgaste es 0, no podemos calcular
if (wearPercent === 0) {
return "N/A"
}
// Calcular horas totales estimadas: hoursUsed / (wearPercent / 100)
const totalEstimatedHours = hoursUsed / (wearPercent / 100)
const remainingHours = totalEstimatedHours - hoursUsed
// Convertir a años
const remainingYears = remainingHours / 8760 // 8760 horas en un año
if (remainingYears < 1) {
const remainingMonths = Math.round(remainingYears * 12)
return `~${remainingMonths} months`
}
return `~${remainingYears.toFixed(1)} years`
}
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
@@ -589,6 +652,51 @@ export function StorageOverview() {
</div> </div>
</div> </div>
{/* Wear & Lifetime Section */}
{getWearIndicator(selectedDisk) && (
<div className="border-t pt-4">
<h4 className="font-semibold mb-3">Wear & Lifetime</h4>
<div className="space-y-3">
<div>
<div className="flex items-center justify-between mb-2">
<p className="text-sm text-muted-foreground">{getWearIndicator(selectedDisk)!.label}</p>
<p className={`font-medium ${getWearColor(getWearIndicator(selectedDisk)!.value)}`}>
{getWearIndicator(selectedDisk)!.value}%
</p>
</div>
<Progress
value={getWearIndicator(selectedDisk)!.value}
className={`h-2 ${
getWearIndicator(selectedDisk)!.value > 80
? "[&>div]:bg-red-500"
: getWearIndicator(selectedDisk)!.value > 50
? "[&>div]:bg-yellow-500"
: "[&>div]:bg-green-500"
}`}
/>
</div>
{getEstimatedLifeRemaining(selectedDisk) && (
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground">Estimated Life Remaining</p>
<p className="font-medium">{getEstimatedLifeRemaining(selectedDisk)}</p>
</div>
{selectedDisk.total_lbas_written && selectedDisk.total_lbas_written > 0 && (
<div>
<p className="text-sm text-muted-foreground">Total Data Written</p>
<p className="font-medium">
{selectedDisk.total_lbas_written >= 1024
? `${(selectedDisk.total_lbas_written / 1024).toFixed(2)} TB`
: `${selectedDisk.total_lbas_written.toFixed(2)} GB`}
</p>
</div>
)}
</div>
)}
</div>
</div>
)}
<div className="border-t pt-4"> <div className="border-t pt-4">
<h4 className="font-semibold mb-3">SMART Attributes</h4> <h4 className="font-semibold mb-3">SMART Attributes</h4>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">

View File

@@ -593,8 +593,13 @@ def get_storage_info():
'reallocated_sectors': smart_data.get('reallocated_sectors', 0), 'reallocated_sectors': smart_data.get('reallocated_sectors', 0),
'pending_sectors': smart_data.get('pending_sectors', 0), 'pending_sectors': smart_data.get('pending_sectors', 0),
'crc_errors': smart_data.get('crc_errors', 0), 'crc_errors': smart_data.get('crc_errors', 0),
'rotation_rate': smart_data.get('rotation_rate', 0), # Added 'rotation_rate': smart_data.get('rotation_rate', 0), # Added
'power_cycles': smart_data.get('power_cycles', 0) # Added 'power_cycles': smart_data.get('power_cycles', 0), # Added
'percentage_used': smart_data.get('percentage_used'), # Added
'media_wearout_indicator': smart_data.get('media_wearout_indicator'), # Added
'wear_leveling_count': smart_data.get('wear_leveling_count'), # Added
'total_lbas_written': smart_data.get('total_lbas_written'), # Added
'ssd_life_left': smart_data.get('ssd_life_left') # Added
} }
storage_data['disk_count'] += 1 storage_data['disk_count'] += 1
@@ -713,6 +718,11 @@ def get_smart_data(disk_name):
'crc_errors': 0, 'crc_errors': 0,
'rotation_rate': 0, # Added rotation rate (RPM) 'rotation_rate': 0, # Added rotation rate (RPM)
'power_cycles': 0, # Added power cycle count 'power_cycles': 0, # Added power cycle count
'percentage_used': None, # NVMe: Percentage Used (0-100)
'media_wearout_indicator': None, # SSD: Media Wearout Indicator (Intel/Samsung)
'wear_leveling_count': None, # SSD: Wear Leveling Count
'total_lbas_written': None, # SSD/NVMe: Total LBAs Written
'ssd_life_left': None, # SSD: SSD Life Left percentage
} }
print(f"[v0] ===== Starting SMART data collection for /dev/{disk_name} =====") print(f"[v0] ===== Starting SMART data collection for /dev/{disk_name} =====")
@@ -790,12 +800,37 @@ def get_smart_data(disk_name):
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 NVMe SMART data
if 'nvme_smart_health_information_log' in data:
print(f"[v0] Parsing NVMe SMART data...")
nvme_data = data['nvme_smart_health_information_log']
if 'temperature' in nvme_data:
smart_data['temperature'] = nvme_data['temperature']
print(f"[v0] NVMe Temperature: {smart_data['temperature']}°C")
if 'power_on_hours' in nvme_data:
smart_data['power_on_hours'] = nvme_data['power_on_hours']
print(f"[v0] NVMe Power On Hours: {smart_data['power_on_hours']}")
if 'power_cycles' in nvme_data:
smart_data['power_cycles'] = nvme_data['power_cycles']
print(f"[v0] NVMe Power Cycles: {smart_data['power_cycles']}")
if 'percentage_used' in nvme_data:
smart_data['percentage_used'] = nvme_data['percentage_used']
print(f"[v0] NVMe Percentage Used: {smart_data['percentage_used']}%")
if 'data_units_written' in nvme_data:
# data_units_written está en unidades de 512KB
data_units = nvme_data['data_units_written']
# Convertir a GB (data_units * 512KB / 1024 / 1024)
total_gb = (data_units * 512) / (1024 * 1024)
smart_data['total_lbas_written'] = round(total_gb, 2)
print(f"[v0] NVMe Total Data Written: {smart_data['total_lbas_written']} GB")
# Parse ATA SMART attributes # 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']:
attr_id = attr.get('id') attr_id = attr.get('id')
raw_value = attr.get('raw', {}).get('value', 0) raw_value = attr.get('raw', {}).get('value', 0)
normalized_value = attr.get('value', 0) # Normalized value (0-100)
if attr_id == 9: # Power_On_Hours if attr_id == 9: # Power_On_Hours
smart_data['power_on_hours'] = raw_value smart_data['power_on_hours'] = raw_value
@@ -820,20 +855,27 @@ def get_smart_data(disk_name):
elif attr_id == 199: # UDMA_CRC_Error_Count elif attr_id == 199: # UDMA_CRC_Error_Count
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}")
elif attr_id == 233: # Media_Wearout_Indicator (Intel/Samsung SSD)
# Parse NVMe SMART data # Valor normalizado: 100 = nuevo, 0 = gastado
if 'nvme_smart_health_information_log' in data: # Invertimos para mostrar desgaste: 0% = nuevo, 100% = gastado
print(f"[v0] Parsing NVMe SMART data...") smart_data['media_wearout_indicator'] = 100 - normalized_value
nvme_data = data['nvme_smart_health_information_log'] print(f"[v0] Media Wearout Indicator (ID 233): {smart_data['media_wearout_indicator']}% used")
if 'temperature' in nvme_data: elif attr_id == 177: # Wear_Leveling_Count
smart_data['temperature'] = nvme_data['temperature'] # Valor normalizado: 100 = nuevo, 0 = gastado
print(f"[v0] NVMe Temperature: {smart_data['temperature']}°C") smart_data['wear_leveling_count'] = 100 - normalized_value
if 'power_on_hours' in nvme_data: print(f"[v0] Wear Leveling Count (ID 177): {smart_data['wear_leveling_count']}% used")
smart_data['power_on_hours'] = nvme_data['power_on_hours'] elif attr_id == 202: # Percentage_Lifetime_Remain (algunos fabricantes)
print(f"[v0] NVMe Power On Hours: {smart_data['power_on_hours']}") # Valor normalizado: 100 = nuevo, 0 = gastado
if 'power_cycles' in nvme_data: smart_data['ssd_life_left'] = normalized_value
smart_data['power_cycles'] = nvme_data['power_cycles'] print(f"[v0] SSD Life Left (ID 202): {smart_data['ssd_life_left']}%")
print(f"[v0] NVMe Power Cycles: {smart_data['power_cycles']}") elif attr_id == 231: # SSD_Life_Left (algunos fabricantes)
smart_data['ssd_life_left'] = normalized_value
print(f"[v0] SSD Life Left (ID 231): {smart_data['ssd_life_left']}%")
elif attr_id == 241: # Total_LBAs_Written
# Convertir a GB (raw_value es en sectores de 512 bytes)
total_gb = (raw_value * 512) / (1024 * 1024 * 1024)
smart_data['total_lbas_written'] = round(total_gb, 2)
print(f"[v0] Total LBAs Written (ID 241): {smart_data['total_lbas_written']} GB")
# If we got good data, break out of the loop # If we got good data, break out of the loop
if smart_data['model'] != 'Unknown' and smart_data['serial'] != 'Unknown': if smart_data['model'] != 'Unknown' and smart_data['serial'] != 'Unknown':
@@ -956,6 +998,28 @@ def get_smart_data(disk_name):
except (ValueError, IndexError) as e: except (ValueError, IndexError) as e:
print(f"[v0] Error parsing attribute line '{line}': {e}") print(f"[v0] Error parsing attribute line '{line}': {e}")
continue continue
elif attr_id == 233: # Media_Wearout_Indicator (Intel/Samsung SSD)
# Valor normalizado: 100 = nuevo, 0 = gastado
# Invertimos para mostrar desgaste: 0% = nuevo, 100% = gastado
smart_data['media_wearout_indicator'] = 100 - normalized_value
print(f"[v0] Media Wearout Indicator (ID 233): {smart_data['media_wearout_indicator']}% used")
elif attr_id == 177: # Wear_Leveling_Count
# Valor normalizado: 100 = nuevo, 0 = gastado
smart_data['wear_leveling_count'] = 100 - normalized_value
print(f"[v0] Wear Leveling Count (ID 177): {smart_data['wear_leveling_count']}% used")
elif attr_id == 202: # Percentage_Lifetime_Remain (algunos fabricantes)
# Valor normalizado: 100 = nuevo, 0 = gastado
smart_data['ssd_life_left'] = normalized_value
print(f"[v0] SSD Life Left (ID 202): {smart_data['ssd_life_left']}%")
elif attr_id == 231: # SSD_Life_Left (algunos fabricantes)
smart_data['ssd_life_left'] = normalized_value
print(f"[v0] SSD Life Left (ID 231): {smart_data['ssd_life_left']}%")
elif attr_id == 241: # Total_LBAs_Written
# Convertir a GB (raw_value es en sectores de 512 bytes)
total_gb = (raw_value * 512) / (1024 * 1024 * 1024)
smart_data['total_lbas_written'] = round(total_gb, 2)
print(f"[v0] Total LBAs Written (ID 241): {smart_data['total_lbas_written']} GB")
# If we got complete data, break # If we got complete data, break
if smart_data['model'] != 'Unknown' and smart_data['serial'] != 'Unknown': if smart_data['model'] != 'Unknown' and smart_data['serial'] != 'Unknown':
print(f"[v0] Successfully extracted complete data from text output (attempt {cmd_index + 1})") print(f"[v0] Successfully extracted complete data from text output (attempt {cmd_index + 1})")
@@ -1548,6 +1612,7 @@ def get_ipmi_power():
'power_meter': power_meter 'power_meter': power_meter
} }
def get_ups_info(): def get_ups_info():
"""Get UPS information from NUT (upsc) - supports both local and remote UPS""" """Get UPS information from NUT (upsc) - supports both local and remote UPS"""
ups_list = [] ups_list = []