From 2f700d9a4ca8be1aea55769f47367a5b44f5d4c1 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Tue, 14 Oct 2025 09:15:44 +0200 Subject: [PATCH] Update AppImage --- AppImage/components/storage-metrics.tsx | 300 +++++++++++++++++++++++- AppImage/scripts/flask_server.py | 40 +++- 2 files changed, 332 insertions(+), 8 deletions(-) diff --git a/AppImage/components/storage-metrics.tsx b/AppImage/components/storage-metrics.tsx index bb00716..70c6a1f 100644 --- a/AppImage/components/storage-metrics.tsx +++ b/AppImage/components/storage-metrics.tsx @@ -4,6 +4,8 @@ import { useState, useEffect } from "react" import { Card, CardContent, CardHeader, CardTitle } from "./ui/card" import { Progress } from "./ui/progress" import { Badge } from "./ui/badge" +import { Button } from "./ui/button" +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog" import { HardDrive, Database, @@ -13,6 +15,7 @@ import { Activity, AlertCircle, Thermometer, + Info, } from "lucide-react" interface StorageData { @@ -33,6 +36,19 @@ interface DiskInfo { health: string temperature: number disk_type?: string + model?: string + serial?: string + smart_status?: string + power_on_hours?: number + power_cycles?: number + reallocated_sectors?: number + pending_sectors?: number + crc_errors?: number + percentage_used?: number // NVMe + ssd_life_left?: number // SSD + wear_leveling_count?: number // SSD + media_wearout_indicator?: number // SSD + total_lbas_written?: number // Both } const TEMP_THRESHOLDS = { @@ -74,6 +90,13 @@ const getDiskTypeBadgeColor = (diskType: string): string => { } } +const getWearStatus = (lifeLeft: number): { status: string; color: string } => { + if (lifeLeft >= 80) return { status: "Excellent", color: "text-green-500" } + if (lifeLeft >= 50) return { status: "Good", color: "text-yellow-500" } + if (lifeLeft >= 20) return { status: "Fair", color: "text-orange-500" } + return { status: "Poor", color: "text-red-500" } +} + const fetchStorageData = async (): Promise => { try { const response = await fetch("/api/storage", { @@ -100,6 +123,9 @@ export function StorageMetrics() { const [storageData, setStorageData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + const [selectedDisk, setSelectedDisk] = useState(null) + const [showDiskDetails, setShowDiskDetails] = useState(false) + const [showTempInfo, setShowTempInfo] = useState(false) useEffect(() => { const fetchData = async () => { @@ -171,7 +197,7 @@ export function StorageMetrics() { const status = getTempStatus(avgTemp, type) return { type, avgTemp: Math.round(avgTemp), status, count: disks.length } }) - .filter((item) => item.type !== "Unknown") // Filter out unknown types + .filter((item) => item.type !== "Unknown") return (
@@ -251,9 +277,14 @@ export function StorageMetrics() { Avg Temperature
- - {type} - +
+ + {type} + + +
@@ -295,10 +326,33 @@ export function StorageMetrics() { const diskType = disk.disk_type || "HDD" const tempStatus = getTempStatus(disk.temperature, diskType) + let lifeLeft: number | null = null + let wearLabel = "" + + if (diskType === "NVMe" && disk.percentage_used !== undefined && disk.percentage_used !== null) { + lifeLeft = 100 - disk.percentage_used + wearLabel = "Life Left" + } else if (diskType === "SSD") { + if (disk.ssd_life_left !== undefined && disk.ssd_life_left !== null) { + lifeLeft = disk.ssd_life_left + wearLabel = "Life Left" + } else if (disk.media_wearout_indicator !== undefined && disk.media_wearout_indicator !== null) { + lifeLeft = disk.media_wearout_indicator + wearLabel = "Health" + } else if (disk.wear_leveling_count !== undefined && disk.wear_leveling_count !== null) { + lifeLeft = disk.wear_leveling_count + wearLabel = "Wear Level" + } + } + return (
{ + setSelectedDisk(disk) + setShowDiskDetails(true) + }} >
@@ -330,6 +384,15 @@ export function StorageMetrics() {
{disk.temperature}°C
+ {lifeLeft !== null && (diskType === "SSD" || diskType === "NVMe") && ( +
+
{wearLabel}
+
+ {lifeLeft.toFixed(0)}% +
+
+ )} + + + + + + Temperature Thresholds by Disk Type + + Recommended operating temperature ranges for different storage devices + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Disk TypeSafe ZoneWarning ZoneCritical Zone
+ + HDD + + ≤ 45°C46 – 55°C> 55°C
+ + SSD + + ≤ 55°C56 – 65°C> 65°C
+ + NVMe + + ≤ 60°C61 – 70°C> 70°C
+
+

+ These thresholds are based on industry standards and manufacturer recommendations. Operating within the + safe zone ensures optimal performance and longevity. +

+
+
+
+ + + + {selectedDisk && ( + <> + + + + Disk Details: {selectedDisk.name} + + Complete SMART information and health status + +
+ {/* Basic Info */} +
+
+
Model
+
{selectedDisk.model || "Unknown"}
+
+
+
Serial Number
+
{selectedDisk.serial || "Unknown"}
+
+
+
Capacity
+
{selectedDisk.total.toFixed(1)}G
+
+
+
Health Status
+ + {selectedDisk.health === "healthy" ? "Healthy" : "Warning"} + +
+
+ + {(selectedDisk.disk_type === "SSD" || selectedDisk.disk_type === "NVMe") && ( +
+

Wear & Life Indicators

+
+ {selectedDisk.disk_type === "NVMe" && + selectedDisk.percentage_used !== undefined && + selectedDisk.percentage_used !== null && ( + <> +
+
Percentage Used
+
+ {selectedDisk.percentage_used}% +
+
+
+
Life Remaining
+
+ {(100 - selectedDisk.percentage_used).toFixed(0)}% +
+ +
+ + )} + {selectedDisk.disk_type === "SSD" && ( + <> + {selectedDisk.ssd_life_left !== undefined && selectedDisk.ssd_life_left !== null && ( +
+
SSD Life Left
+
+ {selectedDisk.ssd_life_left}% +
+ +
+ )} + {selectedDisk.wear_leveling_count !== undefined && + selectedDisk.wear_leveling_count !== null && ( +
+
Wear Leveling Count
+
+ {selectedDisk.wear_leveling_count} +
+
+ )} + {selectedDisk.media_wearout_indicator !== undefined && + selectedDisk.media_wearout_indicator !== null && ( +
+
Media Wearout Indicator
+
+ {selectedDisk.media_wearout_indicator} +
+ +
+ )} + + )} + {selectedDisk.total_lbas_written !== undefined && selectedDisk.total_lbas_written !== null && ( +
+
Total Data Written
+
{(selectedDisk.total_lbas_written / 1000000).toFixed(2)} TB
+
+ )} +
+
+ )} + + {/* SMART Attributes */} +
+

SMART Attributes

+
+
+
Temperature
+
+ {selectedDisk.temperature}°C +
+
+
+
Power On Hours
+
+ {selectedDisk.power_on_hours + ? `${selectedDisk.power_on_hours}h (${Math.floor(selectedDisk.power_on_hours / 24)}d)` + : "N/A"} +
+
+
+
Rotation Rate
+
{selectedDisk.disk_type || "Unknown"}
+
+
+
Power Cycles
+
{selectedDisk.power_cycles || 0}
+
+
+
SMART Status
+
{selectedDisk.smart_status === "passed" ? "Passed" : "Unknown"}
+
+
+
Reallocated Sectors
+
{selectedDisk.reallocated_sectors || 0}
+
+
+
Pending Sectors
+
{selectedDisk.pending_sectors || 0}
+
+
+
CRC Errors
+
{selectedDisk.crc_errors || 0}
+
+
+
+
+ + )} +
+
) } diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py index b1f4f67..4096e0b 100644 --- a/AppImage/scripts/flask_server.py +++ b/AppImage/scripts/flask_server.py @@ -537,7 +537,13 @@ def get_storage_info(): '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 - 'disk_type': smart_data.get('disk_type', 'Unknown') # Added from get_smart_data + 'disk_type': smart_data.get('disk_type', 'Unknown'), # Added from get_smart_data + # Added wear indicators + 'percentage_used': smart_data.get('percentage_used'), + 'ssd_life_left': smart_data.get('ssd_life_left'), + 'wear_leveling_count': smart_data.get('wear_leveling_count'), + 'media_wearout_indicator': smart_data.get('media_wearout_indicator'), + 'total_lbas_written': smart_data.get('total_lbas_written'), } storage_data['disk_count'] += 1 @@ -657,6 +663,11 @@ def get_smart_data(disk_name): 'rotation_rate': 0, # Added rotation rate (RPM) 'power_cycles': 0, # Added power cycle count 'disk_type': 'Unknown', # Will be 'HDD', 'SSD', or 'NVMe' + 'percentage_used': None, # NVMe specific + 'ssd_life_left': None, # SSD specific (percentage remaining) + 'wear_leveling_count': None, # SSD specific + 'media_wearout_indicator': None, # SSD specific + 'total_lbas_written': None, # Both SSD and NVMe } print(f"[v0] ===== Starting SMART data collection for /dev/{disk_name} =====") @@ -753,6 +764,7 @@ def get_smart_data(disk_name): 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) if attr_id == 9: # Power_On_Hours smart_data['power_on_hours'] = raw_value @@ -777,6 +789,22 @@ 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}") + elif attr_id == 177: # Wear_Leveling_Count + smart_data['wear_leveling_count'] = normalized_value + print(f"[v0] Wear Leveling Count (ID 177): {normalized_value}") + elif attr_id == 231: # SSD_Life_Left or Temperature + if normalized_value <= 100: # Likely life left percentage + smart_data['ssd_life_left'] = normalized_value + print(f"[v0] SSD Life Left (ID 231): {normalized_value}%") + elif attr_id == 233: # Media_Wearout_Indicator + smart_data['media_wearout_indicator'] = normalized_value + print(f"[v0] Media Wearout Indicator (ID 233): {normalized_value}") + elif attr_id == 202: # Percent_Lifetime_Remain + smart_data['ssd_life_left'] = normalized_value + print(f"[v0] Percent Lifetime Remain (ID 202): {normalized_value}%") + elif attr_id == 241: # Total_LBAs_Written + smart_data['total_lbas_written'] = raw_value + print(f"[v0] Total LBAs Written (ID 241): {raw_value}") # Parse NVMe SMART data if 'nvme_smart_health_information_log' in data: @@ -791,7 +819,13 @@ def get_smart_data(disk_name): 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: + smart_data['total_lbas_written'] = nvme_data['data_units_written'] + print(f"[v0] NVMe Data Units Written: {smart_data['total_lbas_written']}") + # If we got good data, break out of the loop if smart_data['model'] != 'Unknown' and smart_data['serial'] != 'Unknown': print(f"[v0] Successfully extracted complete data from JSON (attempt {cmd_index + 1})") @@ -1370,7 +1404,7 @@ def get_network_info(): } def get_proxmox_vms(): - """Get Proxmox VM and LXC information (requires pvesh command) - only from local node""" + """Get Proxmox VM and LXC information using pvesh command - only from local node""" try: all_vms = []