- Run a SMART test in the SMART Test tab for more detailed analysis.
-
+ {/* Last Test Info */}
+
+
+
Last Test Date
+
+ {smartJsonData.timestamp
+ ? new Date(smartJsonData.timestamp).toLocaleString()
+ : 'Unknown'}
+
+
+
+
Test Type
+
{smartJsonData.test_type || 'Unknown'}
+
+
) : (
@@ -1700,30 +1731,92 @@ function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttri
// Build attributes table - format differs for NVMe vs SATA
const isNvmeForTable = diskType === 'NVMe'
+
+ // Explanations for NVMe metrics
+ const nvmeExplanations: Record = {
+ 'Critical Warning': 'Active alert flags from the NVMe controller. Any non-zero value requires immediate investigation.',
+ 'Temperature': 'Composite temperature. Sustained high temps cause throttling and reduce lifespan.',
+ 'Available Spare': 'Spare NAND blocks remaining. Alert triggers below 5%.',
+ 'Available Spare Threshold': 'Threshold below which spare blocks are considered critical.',
+ 'Percentage Used': "Drive's own estimate of endurance consumed. 100% means rated lifespan has been reached.",
+ 'Percent Used': "Drive's own estimate of endurance consumed. 100% means rated lifespan has been reached.",
+ 'Media Errors': 'Unrecoverable errors involving the NAND flash. Any non-zero value indicates flash cell damage.',
+ 'Unsafe Shutdowns': 'Power losses without proper shutdown. Very high counts can cause firmware corruption.',
+ 'Power Cycles': 'Total on/off cycles. Frequent cycling increases connector and capacitor wear.',
+ 'Power On Hours': 'Total hours the drive has been powered on.',
+ 'Data Units Read': 'Total data read from the drive in 512KB units.',
+ 'Data Units Written': 'Total data written to the drive in 512KB units.',
+ 'Host Read Commands': 'Total read commands processed by the controller.',
+ 'Host Write Commands': 'Total write commands processed by the controller.',
+ 'Controller Busy Time': 'Minutes the controller was busy processing commands.',
+ 'Error Log Entries': 'Number of entries in the error log. Often includes benign self-test artifacts.',
+ 'Warning Temp Time': 'Minutes spent in the warning temperature range. Zero is ideal.',
+ 'Critical Temp Time': 'Minutes spent in the critical temperature range. Should always be zero.',
+ }
+
+ // Explanations for SATA/SSD attributes
+ const sataExplanations: Record = {
+ 'Raw Read Error Rate': 'Raw read errors detected. High values on Seagate drives are often normal (uses proprietary formula).',
+ 'Reallocated Sector Count': 'Bad sectors replaced by spare sectors. Growing count indicates drive degradation.',
+ 'Reallocated Sectors': 'Bad sectors replaced by spare sectors. Growing count indicates drive degradation.',
+ 'Spin Up Time': 'Time needed for platters to reach operating speed (HDD only).',
+ 'Start Stop Count': 'Number of spindle start/stop cycles (HDD only).',
+ 'Power On Hours': 'Total hours the drive has been powered on.',
+ 'Power Cycle Count': 'Total number of complete power on/off cycles.',
+ 'Temperature': 'Current drive temperature. High temps reduce lifespan.',
+ 'Temperature Celsius': 'Current drive temperature in Celsius. High temps reduce lifespan.',
+ 'Current Pending Sector': 'Sectors waiting to be remapped. May resolve or become reallocated.',
+ 'Pending Sectors': 'Sectors waiting to be remapped. May resolve or become reallocated.',
+ 'Offline Uncorrectable': 'Uncorrectable errors found during offline scan. Indicates potential data loss.',
+ 'UDMA CRC Error Count': 'Interface communication errors. Usually caused by cable or connection issues.',
+ 'CRC Errors': 'Interface communication errors. Usually caused by cable or connection issues.',
+ 'Wear Leveling Count': 'SSD wear indicator. Lower values mean more wear.',
+ 'Media Wearout Indicator': 'SSD life remaining estimate. Lower values mean less life remaining.',
+ 'Total LBAs Written': 'Total logical blocks written to the drive.',
+ 'Total LBAs Read': 'Total logical blocks read from the drive.',
+ 'SSD Life Left': 'Estimated remaining lifespan percentage.',
+ 'Percent Lifetime Remain': 'Estimated remaining lifespan percentage.',
+ }
+
+ const getAttrExplanation = (name: string, isNvme: boolean): string => {
+ const cleanName = name.replace(/_/g, ' ')
+ if (isNvme) {
+ return nvmeExplanations[cleanName] || nvmeExplanations[name] || ''
+ }
+ return sataExplanations[cleanName] || sataExplanations[name] || ''
+ }
+
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'
+ const explanation = getAttrExplanation(attr.name, isNvmeForTable)
if (isNvmeForTable) {
- // NVMe format: Metric | Value | Status
+ // NVMe format: Metric | Value | Status (with explanation)
return `
`
} else {
- // SATA format: ID | Attribute | Val | Worst | Thr | Raw | Status
+ // SATA format: ID | Attribute | Val | Worst | Thr | Raw | Status (with explanation)
return `
`
}
@@ -2585,19 +2678,36 @@ function SmartTestTab({ disk, observations = [] }: SmartTestTabProps) {
fetchSmartStatus()
// Poll for status updates
+ // For disks that don't report progress, we keep polling but show an indeterminate progress bar
+ let pollCount = 0
+ const maxPolls = testType === 'short' ? 36 : 720 // 3 min for short, 1 hour for long (at 5s intervals)
+
const pollInterval = setInterval(async () => {
+ pollCount++
try {
- const data = await fetchApi(`/api/storage/smart/${disk.name}`)
- setTestStatus(data)
- if (data.status !== 'running') {
+ const statusData = await fetchApi(`/api/storage/smart/${disk.name}`)
+ setTestStatus(statusData)
+
+ // Only clear runningTest when we get a definitive "not running" status
+ if (statusData.status !== 'running') {
clearInterval(pollInterval)
setRunningTest(null)
+ // Refresh SMART JSON data to get new test results
+ fetchSmartStatus()
}
} catch {
+ // Don't clear on error - keep showing progress
+ }
+
+ // Safety timeout: stop polling after max duration
+ if (pollCount >= maxPolls) {
clearInterval(pollInterval)
setRunningTest(null)
+ // Refresh status one more time to get final result
+ fetchSmartStatus()
}
}, 5000)
+
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to start test'
setTestError(message)
@@ -2666,79 +2776,80 @@ function SmartTestTab({ disk, observations = [] }: SmartTestTabProps) {
Run SMART Test
+
-
-
-
+
+
+
Short test takes ~2 minutes. Extended test runs in the background and can take several hours for large disks.
You will receive a notification when the test completes.
- {/* Error Message */}
- {testError && (
-
-
-
-
Failed to start test
-
{testError}
-
-
- )}
-
-
+ {/* Error Message */}
+ {testError && (
+
+
+
+
Failed to start test
+
{testError}
+
+
+ )}
+
- {/* Test Progress */}
- {testStatus.status === 'running' && (
+ {/* Test Progress - Show when API reports running OR when we just started a test */}
+ {(testStatus.status === 'running' || runningTest !== null) && (
-
+
-
+
- {testStatus.test_type === 'short' ? 'Short' : 'Extended'} test in progress
+ {(runningTest || testStatus.test_type) === 'short' ? 'Short' : 'Extended'} test in progress
Please wait while the test completes...
+ {/* Only show progress bar if the disk reports progress percentage */}
{testStatus.progress !== undefined && (
-
+
)}