diff --git a/AppImage/components/storage-overview.tsx b/AppImage/components/storage-overview.tsx index 9b79f54..2b08320 100644 --- a/AppImage/components/storage-overview.tsx +++ b/AppImage/components/storage-overview.tsx @@ -28,6 +28,11 @@ interface DiskInfo { crc_errors?: number rotation_rate?: 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 { @@ -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) { return (
@@ -589,6 +652,51 @@ export function StorageOverview() {
+ {/* Wear & Lifetime Section */} + {getWearIndicator(selectedDisk) && ( +
+

Wear & Lifetime

+
+
+
+

{getWearIndicator(selectedDisk)!.label}

+

+ {getWearIndicator(selectedDisk)!.value}% +

+
+ 80 + ? "[&>div]:bg-red-500" + : getWearIndicator(selectedDisk)!.value > 50 + ? "[&>div]:bg-yellow-500" + : "[&>div]:bg-green-500" + }`} + /> +
+ {getEstimatedLifeRemaining(selectedDisk) && ( +
+
+

Estimated Life Remaining

+

{getEstimatedLifeRemaining(selectedDisk)}

+
+ {selectedDisk.total_lbas_written && selectedDisk.total_lbas_written > 0 && ( +
+

Total Data Written

+

+ {selectedDisk.total_lbas_written >= 1024 + ? `${(selectedDisk.total_lbas_written / 1024).toFixed(2)} TB` + : `${selectedDisk.total_lbas_written.toFixed(2)} GB`} +

+
+ )} +
+ )} +
+
+ )} +

SMART Attributes

diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py index 94748d0..87337b9 100644 --- a/AppImage/scripts/flask_server.py +++ b/AppImage/scripts/flask_server.py @@ -593,8 +593,13 @@ def get_storage_info(): 'reallocated_sectors': smart_data.get('reallocated_sectors', 0), 'pending_sectors': smart_data.get('pending_sectors', 0), 'crc_errors': smart_data.get('crc_errors', 0), - 'rotation_rate': smart_data.get('rotation_rate', 0), # Added - 'power_cycles': smart_data.get('power_cycles', 0) # Added + 'rotation_rate': smart_data.get('rotation_rate', 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 @@ -713,6 +718,11 @@ def get_smart_data(disk_name): 'crc_errors': 0, 'rotation_rate': 0, # Added rotation rate (RPM) '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} =====") @@ -790,12 +800,37 @@ def get_smart_data(disk_name): smart_data['temperature'] = data['temperature']['current'] 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 if 'ata_smart_attributes' in data and 'table' in data['ata_smart_attributes']: print(f"[v0] Parsing ATA SMART attributes...") for attr in data['ata_smart_attributes']['table']: attr_id = attr.get('id') 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 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 smart_data['crc_errors'] = raw_value print(f"[v0] CRC Errors (ID 199): {raw_value}") - - # 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']}") + 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 good data, break out of the loop 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: print(f"[v0] Error parsing attribute line '{line}': {e}") 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 smart_data['model'] != 'Unknown' and smart_data['serial'] != 'Unknown': 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 } + def get_ups_info(): """Get UPS information from NUT (upsc) - supports both local and remote UPS""" ups_list = []