From e94e065eca0edc80829c403adc6179970aa05a91 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Sun, 12 Apr 2026 23:45:23 +0200 Subject: [PATCH] update storage-overview.tsx --- AppImage/components/storage-overview.tsx | 398 ++++++++++++++++++++--- AppImage/scripts/flask_server.py | 111 ++++++- 2 files changed, 449 insertions(+), 60 deletions(-) diff --git a/AppImage/components/storage-overview.tsx b/AppImage/components/storage-overview.tsx index e08adf45..842a2feb 100644 --- a/AppImage/components/storage-overview.tsx +++ b/AppImage/components/storage-overview.tsx @@ -1461,7 +1461,7 @@ export function StorageOverview() { {/* SMART Test Tab */} {selectedDisk && activeModalTab === "smart" && ( - + )} @@ -1471,7 +1471,7 @@ export function StorageOverview() { } // Generate SMART Report HTML and open in new window (same pattern as Lynis/Latency reports) -function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttributes: Array<{id: number; name: string; value: number; worst: number; threshold: number; raw_value: string; status: 'ok' | 'warning' | 'critical'}>) { +function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttributes: Array<{id: number; name: string; value: number; worst: number; threshold: number; raw_value: string; status: 'ok' | 'warning' | 'critical'}>, observations: DiskObservation[] = []) { const now = new Date().toLocaleString() const logoUrl = `${window.location.origin}/images/proxmenux-logo.png` const reportId = `SMART-${Date.now().toString(36).toUpperCase()}` @@ -1499,27 +1499,112 @@ function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttri ? `${powerOnYears}y ${powerOnRemainingDays}d (${powerOnHours.toLocaleString()}h)` : `${powerOnDays}d (${powerOnHours.toLocaleString()}h)` - // Build attributes table + // Build attributes table - format differs for NVMe vs SATA + const isNvmeForTable = diskType === 'NVMe' const attributeRows = smartAttributes.map((attr, i) => { const statusColor = attr.status === 'ok' ? '#16a34a' : attr.status === 'warning' ? '#ca8a04' : '#dc2626' const statusBg = attr.status === 'ok' ? '#16a34a15' : attr.status === 'warning' ? '#ca8a0415' : '#dc262615' - return ` - - ${attr.id} - ${attr.name.replace(/_/g, ' ')} - ${attr.value} - ${attr.worst} - ${attr.threshold} - ${attr.raw_value} - ${attr.status === 'ok' ? 'OK' : attr.status.toUpperCase()} - - ` + + if (isNvmeForTable) { + // NVMe format: Metric | Value | Status + return ` + + ${attr.name} + ${attr.value} + ${attr.status === 'ok' ? 'OK' : attr.status.toUpperCase()} + + ` + } else { + // SATA format: ID | Attribute | Val | Worst | Thr | Raw | Status + return ` + + ${attr.id} + ${attr.name.replace(/_/g, ' ')} + ${attr.value} + ${attr.worst} + ${attr.threshold} + ${attr.raw_value} + ${attr.status === 'ok' ? 'OK' : attr.status.toUpperCase()} + + ` + } }).join('') // Critical attributes to highlight const criticalAttrs = smartAttributes.filter(a => a.status !== 'ok') const hasCritical = criticalAttrs.length > 0 + // Temperature color based on disk type + const getTempColorForReport = (temp: number): string => { + if (temp <= 0) return '#94a3b8' // gray for N/A + switch (diskType) { + case 'NVMe': + // NVMe: <=70 green, 71-80 yellow, >80 red + if (temp <= 70) return '#16a34a' + if (temp <= 80) return '#ca8a04' + return '#dc2626' + case 'SSD': + // SSD: <=59 green, 60-70 yellow, >70 red + if (temp <= 59) return '#16a34a' + if (temp <= 70) return '#ca8a04' + return '#dc2626' + case 'HDD': + default: + // HDD: <=45 green, 46-55 yellow, >55 red + if (temp <= 45) return '#16a34a' + if (temp <= 55) return '#ca8a04' + return '#dc2626' + } + } + + // Temperature thresholds for display + const tempThresholds = diskType === 'NVMe' + ? { optimal: '<=70°C', warning: '71-80°C', critical: '>80°C' } + : diskType === 'SSD' + ? { optimal: '<=59°C', warning: '60-70°C', critical: '>70°C' } + : { optimal: '<=45°C', warning: '46-55°C', critical: '>55°C' } + + const isNvmeDisk = diskType === 'NVMe' + + // NVMe Wear & Lifetime data + const nvmePercentUsed = testStatus.smart_data?.nvme_raw?.percent_used ?? disk.percentage_used ?? 0 + const nvmeAvailSpare = testStatus.smart_data?.nvme_raw?.avail_spare ?? 100 + const nvmeDataWritten = testStatus.smart_data?.nvme_raw?.data_units_written ?? 0 + // Data units are in 512KB blocks, convert to TB + const nvmeDataWrittenTB = (nvmeDataWritten * 512 * 1024) / (1024 * 1024 * 1024 * 1024) + + // Calculate estimated life remaining for NVMe + let nvmeEstimatedLife = 'N/A' + if (nvmePercentUsed > 0 && disk.power_on_hours && disk.power_on_hours > 0) { + const totalEstimatedHours = disk.power_on_hours / (nvmePercentUsed / 100) + const remainingHours = totalEstimatedHours - disk.power_on_hours + const remainingYears = remainingHours / (24 * 365) + if (remainingYears >= 1) { + nvmeEstimatedLife = `~${remainingYears.toFixed(1)} years` + } else if (remainingHours >= 24) { + nvmeEstimatedLife = `~${Math.floor(remainingHours / 24)} days` + } else { + nvmeEstimatedLife = `~${Math.floor(remainingHours)} hours` + } + } else if (nvmePercentUsed === 0) { + nvmeEstimatedLife = 'Excellent' + } + + // Wear color based on percentage + const getWearColorHex = (pct: number): string => { + if (pct <= 50) return '#16a34a' // green + if (pct <= 80) return '#ca8a04' // yellow + return '#dc2626' // red + } + + // Life remaining color (inverse) + const getLifeColorHex = (pct: number): string => { + const remaining = 100 - pct + if (remaining >= 50) return '#16a34a' // green + if (remaining >= 20) return '#ca8a04' // yellow + return '#dc2626' // red + } + // Build recommendations const recommendations: string[] = [] if (isHealthy) { @@ -1545,8 +1630,8 @@ function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttri } if (recommendations.length === 1 && isHealthy) { - recommendations.push('
Regular Maintenance

Schedule periodic extended SMART tests (monthly) to catch issues early.

') - recommendations.push('
Backup Strategy

Ensure critical data is backed up regularly regardless of disk health status.

') + recommendations.push('
Regular Maintenance

Schedule periodic extended SMART tests (monthly) to catch issues early.

') + recommendations.push('
Backup Strategy

Ensure critical data is backed up regularly regardless of disk health status.

') } const html = ` @@ -1746,8 +1831,9 @@ function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttri
-
${disk.temperature > 0 ? disk.temperature + '°C' : 'N/A'}
+
${disk.temperature > 0 ? disk.temperature + '°C' : 'N/A'}
Temperature
+
Optimal: ${tempThresholds.optimal}
${powerOnHours.toLocaleString()}h
@@ -1758,36 +1844,168 @@ function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttri
Power Cycles
+ ${isNvmeDisk ? ` +
${testStatus.smart_data?.nvme_raw?.media_errors ?? 0}
+
Media Errors
+ ` : `
${disk.reallocated_sectors ?? 0}
Reallocated Sectors
+ `}
- +${!isNvmeDisk ? ` +
-
3. SMART Attributes (${smartAttributes.length} total${hasCritical ? `, ${criticalAttrs.length} warning(s)` : ''})
+
3. Disk Overview
+
+ +
+
+
+
Temperature
+
${disk.temperature > 0 ? disk.temperature + '°C' : 'N/A'}
+
Optimal: ${tempThresholds.optimal}
+
+
+
Power On Hours
+
${powerOnHours.toLocaleString()}h
+
${powerOnYears}y ${powerOnDays}d
+
+
+
Rotation Rate
+
${diskType === 'HDD' ? (disk.rotation_rate ? disk.rotation_rate + ' RPM' : 'N/A') : 'SSD'}
+
+
+
Power Cycles
+
${(disk.power_cycles ?? 0).toLocaleString()}
+
+
+
+ + +
+
HEALTH INDICATORS
+
+
+
+
+
SMART Status
+
${disk.smart_status || 'N/A'}
+
+
+
+
+
+
Reallocated Sectors
+
${disk.reallocated_sectors ?? 0}
+
+
+
+
+
+
Pending Sectors
+
${disk.pending_sectors ?? 0}
+
+
+
+
+
+
CRC Errors
+
${disk.crc_errors ?? 0}
+
+
+
+
+
+
+` : ''} + +${isNvmeDisk ? ` + +
+
3. NVMe Wear & Lifetime
+
+ +
+
LIFE REMAINING
+
+ + + + +
+
${100 - nvmePercentUsed}%
+
+
+
Estimated: ${nvmeEstimatedLife}
+
+ + +
+
USAGE STATISTICS
+ +
+
+ Percentage Used + ${nvmePercentUsed}% +
+
+
+
+
+ +
+
+ Available Spare + ${nvmeAvailSpare}% +
+
+
+
+
+ +
+
+
Data Written
+
${nvmeDataWrittenTB >= 1 ? nvmeDataWrittenTB.toFixed(2) + ' TB' : (nvmeDataWrittenTB * 1024).toFixed(1) + ' GB'}
+
+
+
Power Cycles
+
${testStatus.smart_data?.nvme_raw?.power_cycles?.toLocaleString() ?? disk.power_cycles ?? 'N/A'}
+
+
+
+
+
+` : ''} + + +
+
4. ${isNvmeDisk ? 'NVMe Health Metrics' : 'SMART Attributes'} (${smartAttributes.length} total${hasCritical ? `, ${criticalAttrs.length} warning(s)` : ''})
- - - - - - + ${isNvmeDisk ? '' : ''} + + + ${isNvmeDisk ? '' : ''} + ${isNvmeDisk ? '' : ''} + ${isNvmeDisk ? '' : ''} - ${attributeRows || ''} + ${attributeRows || ''}
IDAttributeValWorstThrRawID${isNvmeDisk ? 'Metric' : 'Attribute'}ValueWorstThrRaw
No SMART attributes available
No ${isNvmeDisk ? 'NVMe metrics' : 'SMART attributes'} available
- +
-
4. Last Self-Test Result
+
5. Last Self-Test Result
${testStatus.last_test ? `
@@ -1814,13 +2032,102 @@ function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttri `}
- +${observations.length > 0 ? ` +
-
5. Recommendations
+
6. Observations & Events (${observations.length} recorded, ${observations.reduce((sum, o) => sum + o.occurrence_count, 0)} total occurrences)
+

The following events have been detected and logged for this disk. These observations may indicate potential issues that require attention.

+ + + ${(() => { + const groupedObs: Record = {} + observations.forEach(obs => { + const type = obs.error_type || 'unknown' + if (!groupedObs[type]) groupedObs[type] = [] + groupedObs[type].push(obs) + }) + + return Object.entries(groupedObs).map(([type, obsList]) => { + const typeLabel = type === 'io_error' ? 'I/O Errors' : type === 'smart_error' ? 'SMART Errors' : type === 'filesystem_error' ? 'Filesystem Errors' : type.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()) + const totalOccurrences = obsList.reduce((sum, o) => sum + o.occurrence_count, 0) + + return \` +
+
+ \${typeLabel} + \${obsList.length} unique, \${totalOccurrences} total +
+
+ \${obsList.map(obs => { + const severityColor = obs.severity === 'critical' ? '#dc2626' : obs.severity === 'warning' ? '#ca8a04' : '#3b82f6' + const severityBg = obs.severity === 'critical' ? '#dc262615' : obs.severity === 'warning' ? '#ca8a0415' : '#3b82f615' + const severityLabel = obs.severity ? obs.severity.charAt(0).toUpperCase() + obs.severity.slice(1) : 'Info' + const firstDate = obs.first_occurrence ? new Date(obs.first_occurrence).toLocaleString() : 'N/A' + const lastDate = obs.last_occurrence ? new Date(obs.last_occurrence).toLocaleString() : 'N/A' + const dismissed = obs.dismissed ? 'Dismissed' : '' + + return \\\` +
+
+ \${severityLabel} + ID: #\${obs.id} + Occurrences: \${obs.occurrence_count} + \${dismissed} +
+ + +
+
Error Signature:
+
\${obs.error_signature}
+
+ + +
+
Raw Message:
+
\${obs.raw_message || 'N/A'}
+
+ + +
+
+ Device: + \${obs.device_name || disk.name} +
+
+ Serial: + \${obs.serial || disk.serial || 'N/A'} +
+
+ Model: + \${obs.model || disk.model || 'N/A'} +
+
+ First Seen: + \${firstDate} +
+
+ Last Seen: + \${lastDate} +
+
+
+ \\\` + }).join('')} +
+
+ \` + }).join('') + })()} +
+` : ''} + + +
+
${observations.length > 0 ? '7' : '6'}. Recommendations
${recommendations.join('')}
- - + +