mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-11-18 11:36:17 +00:00
Update AppImage
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
@@ -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 = []
|
||||||
|
|||||||
Reference in New Issue
Block a user