mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-25 00:46:21 +00:00
update storage-overview.tsx
This commit is contained in:
@@ -163,3 +163,15 @@
|
|||||||
.xterm-rows {
|
.xterm-rows {
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===================== */
|
||||||
|
/* Progress Animations */
|
||||||
|
/* ===================== */
|
||||||
|
@keyframes indeterminate {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(400%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1436,12 +1436,20 @@ export function StorageOverview() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* SMART Test Data Section (from real test JSON) */}
|
{/* SMART Test Data Section (from real test JSON) - Uniform style with Wear & Lifetime */}
|
||||||
{(loadingSmartJson || smartJsonData?.has_data) && (
|
{(loadingSmartJson || smartJsonData?.has_data) && (
|
||||||
<div className="border-t pt-4">
|
<div className="border-t pt-4">
|
||||||
<h4 className="font-semibold mb-3 flex items-center gap-2">
|
<h4 className="font-semibold mb-3 flex items-center gap-2">
|
||||||
<Activity className="h-4 w-4 text-green-400" />
|
<Activity className="h-4 w-4 text-green-400" />
|
||||||
SMART Test Data
|
{(() => {
|
||||||
|
// Check if this is SSD without Proxmox wear data - show as "Wear & Lifetime"
|
||||||
|
const isNvme = selectedDisk.name?.includes('nvme')
|
||||||
|
const hasProxmoxWear = getWearIndicator(selectedDisk) !== null
|
||||||
|
if (!isNvme && !hasProxmoxWear && smartJsonData?.has_data) {
|
||||||
|
return 'Wear & Lifetime'
|
||||||
|
}
|
||||||
|
return 'SMART Test Data'
|
||||||
|
})()}
|
||||||
{smartJsonData?.has_data && (
|
{smartJsonData?.has_data && (
|
||||||
<Badge className="bg-green-500/10 text-green-400 border-green-500/20 text-[10px] px-1.5">
|
<Badge className="bg-green-500/10 text-green-400 border-green-500/20 text-[10px] px-1.5">
|
||||||
Real Test
|
Real Test
|
||||||
@@ -1455,33 +1463,18 @@ export function StorageOverview() {
|
|||||||
</div>
|
</div>
|
||||||
) : smartJsonData?.has_data && smartJsonData.data ? (
|
) : smartJsonData?.has_data && smartJsonData.data ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{/* Last Test Info */}
|
{/* SSD/NVMe Life Estimation from JSON - Uniform style */}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-muted-foreground">Last Test Date</p>
|
|
||||||
<p className="font-medium">
|
|
||||||
{smartJsonData.timestamp
|
|
||||||
? new Date(smartJsonData.timestamp).toLocaleString()
|
|
||||||
: 'Unknown'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-muted-foreground">Test Type</p>
|
|
||||||
<p className="font-medium capitalize">{smartJsonData.test_type || 'Unknown'}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* SSD Life Estimation from JSON (if available) */}
|
|
||||||
{(() => {
|
{(() => {
|
||||||
const data = smartJsonData.data as Record<string, unknown>
|
const data = smartJsonData.data as Record<string, unknown>
|
||||||
const ataAttrs = data?.ata_smart_attributes as { table?: Array<{ id: number; name: string; value: number; raw?: { value: number } }> }
|
const ataAttrs = data?.ata_smart_attributes as { table?: Array<{ id: number; name: string; value: number; raw?: { value: number } }> }
|
||||||
const table = ataAttrs?.table || []
|
const table = ataAttrs?.table || []
|
||||||
|
|
||||||
// Look for wear-related attributes
|
// Look for wear-related attributes for SSD
|
||||||
const wearAttr = table.find(a =>
|
const wearAttr = table.find(a =>
|
||||||
a.name?.toLowerCase().includes('wear_leveling') ||
|
a.name?.toLowerCase().includes('wear_leveling') ||
|
||||||
a.name?.toLowerCase().includes('media_wearout') ||
|
a.name?.toLowerCase().includes('media_wearout') ||
|
||||||
a.name?.toLowerCase().includes('percent_lifetime') ||
|
a.name?.toLowerCase().includes('percent_lifetime') ||
|
||||||
|
a.name?.toLowerCase().includes('ssd_life_left') ||
|
||||||
a.id === 177 || a.id === 231 || a.id === 233
|
a.id === 177 || a.id === 231 || a.id === 233
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1491,55 +1484,95 @@ export function StorageOverview() {
|
|||||||
a.id === 241
|
a.id === 241
|
||||||
)
|
)
|
||||||
|
|
||||||
if (wearAttr || lbasAttr) {
|
// Look for power on hours from SMART data
|
||||||
return (
|
const pohAttr = table.find(a =>
|
||||||
<div className="bg-green-500/5 border border-green-500/20 rounded-lg p-3">
|
a.name?.toLowerCase().includes('power_on_hours') ||
|
||||||
<p className="text-xs text-green-400 mb-2 font-medium">From Real SMART Test</p>
|
a.id === 9
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
{wearAttr && (
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-muted-foreground">{wearAttr.name?.replace(/_/g, ' ')}</p>
|
|
||||||
<p className={`font-medium ${wearAttr.value < 50 ? 'text-red-400' : wearAttr.value < 80 ? 'text-yellow-400' : 'text-green-400'}`}>
|
|
||||||
{wearAttr.value}%
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{lbasAttr && lbasAttr.raw?.value && (
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-muted-foreground">Total Data Written</p>
|
|
||||||
<p className="font-medium">
|
|
||||||
{(() => {
|
|
||||||
const tbWritten = (lbasAttr.raw.value * 512) / (1024 ** 4)
|
|
||||||
return tbWritten >= 1
|
|
||||||
? `${tbWritten.toFixed(2)} TB`
|
|
||||||
: `${(tbWritten * 1024).toFixed(2)} GB`
|
|
||||||
})()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// For NVMe, check nvme_smart_health_information_log
|
// For NVMe, check nvme_smart_health_information_log
|
||||||
const nvmeHealth = data?.nvme_smart_health_information_log as Record<string, unknown>
|
const nvmeHealth = data?.nvme_smart_health_information_log as Record<string, unknown>
|
||||||
if (nvmeHealth) {
|
|
||||||
const percentUsed = nvmeHealth.percentage_used as number
|
|
||||||
const dataUnitsWritten = nvmeHealth.data_units_written as number
|
|
||||||
const availableSpare = nvmeHealth.available_spare as number
|
|
||||||
|
|
||||||
|
// Calculate data written
|
||||||
|
let dataWrittenTB = 0
|
||||||
|
let dataWrittenLabel = ''
|
||||||
|
if (lbasAttr && lbasAttr.raw?.value) {
|
||||||
|
dataWrittenTB = (lbasAttr.raw.value * 512) / (1024 ** 4)
|
||||||
|
dataWrittenLabel = dataWrittenTB >= 1
|
||||||
|
? `${dataWrittenTB.toFixed(2)} TB`
|
||||||
|
: `${(dataWrittenTB * 1024).toFixed(2)} GB`
|
||||||
|
} else if (nvmeHealth?.data_units_written) {
|
||||||
|
const units = nvmeHealth.data_units_written as number
|
||||||
|
dataWrittenTB = (units * 512000) / (1024 ** 4)
|
||||||
|
dataWrittenLabel = dataWrittenTB >= 1
|
||||||
|
? `${dataWrittenTB.toFixed(2)} TB`
|
||||||
|
: `${(dataWrittenTB * 1024).toFixed(2)} GB`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get wear percentage
|
||||||
|
let wearPercent: number | null = null
|
||||||
|
let wearLabel = 'Life Remaining'
|
||||||
|
if (wearAttr) {
|
||||||
|
wearPercent = wearAttr.value
|
||||||
|
wearLabel = wearAttr.name?.replace(/_/g, ' ') || 'Life Remaining'
|
||||||
|
} else if (nvmeHealth?.percentage_used !== undefined) {
|
||||||
|
wearPercent = 100 - (nvmeHealth.percentage_used as number)
|
||||||
|
wearLabel = 'Life Remaining'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate estimated life remaining
|
||||||
|
let estimatedLife = ''
|
||||||
|
const powerOnHours = pohAttr?.raw?.value || selectedDisk.power_on_hours || 0
|
||||||
|
if (wearPercent !== null && wearPercent > 0 && wearPercent < 100 && powerOnHours > 0) {
|
||||||
|
const usedPercent = 100 - wearPercent
|
||||||
|
if (usedPercent > 0) {
|
||||||
|
const totalEstimatedHours = powerOnHours / (usedPercent / 100)
|
||||||
|
const remainingHours = totalEstimatedHours - powerOnHours
|
||||||
|
const remainingYears = remainingHours / (24 * 365)
|
||||||
|
if (remainingYears >= 1) {
|
||||||
|
estimatedLife = `~${remainingYears.toFixed(1)} years`
|
||||||
|
} else {
|
||||||
|
const remainingMonths = remainingYears * 12
|
||||||
|
estimatedLife = `~${remainingMonths.toFixed(0)} months`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Available spare for NVMe
|
||||||
|
const availableSpare = nvmeHealth?.available_spare as number | undefined
|
||||||
|
|
||||||
|
if (wearPercent !== null || dataWrittenLabel) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-green-500/5 border border-green-500/20 rounded-lg p-3">
|
<>
|
||||||
<p className="text-xs text-green-400 mb-2 font-medium">From Real SMART Test (NVMe)</p>
|
{/* Wear Progress Bar - Same style as NVMe */}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
{wearPercent !== null && (
|
||||||
{percentUsed !== undefined && (
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground">Percent Used</p>
|
<div className="flex items-center justify-between mb-2">
|
||||||
<p className={`font-medium ${percentUsed > 80 ? 'text-red-400' : percentUsed > 50 ? 'text-yellow-400' : 'text-green-400'}`}>
|
<p className="text-sm text-muted-foreground">{wearLabel}</p>
|
||||||
{percentUsed}%
|
<p className={`font-medium ${wearPercent < 20 ? 'text-red-400' : wearPercent < 50 ? 'text-yellow-400' : 'text-green-400'}`}>
|
||||||
|
{wearPercent}%
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<Progress
|
||||||
|
value={wearPercent}
|
||||||
|
className={`h-2 ${wearPercent < 20 ? '[&>div]:bg-red-500' : wearPercent < 50 ? '[&>div]:bg-yellow-500' : '[&>div]:bg-green-500'}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Stats Grid - Same layout as NVMe Wear & Lifetime */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{estimatedLife && (
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Estimated Life Remaining</p>
|
||||||
|
<p className="font-medium">{estimatedLife}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{dataWrittenLabel && (
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Total Data Written</p>
|
||||||
|
<p className="font-medium">{dataWrittenLabel}</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{availableSpare !== undefined && (
|
{availableSpare !== undefined && (
|
||||||
<div>
|
<div>
|
||||||
@@ -1549,31 +1582,29 @@ export function StorageOverview() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{dataUnitsWritten !== undefined && (
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-muted-foreground">Total Data Written</p>
|
|
||||||
<p className="font-medium">
|
|
||||||
{(() => {
|
|
||||||
const tbWritten = (dataUnitsWritten * 512000) / (1024 ** 4)
|
|
||||||
return tbWritten >= 1
|
|
||||||
? `${tbWritten.toFixed(2)} TB`
|
|
||||||
: `${(tbWritten * 1024).toFixed(2)} GB`
|
|
||||||
})()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
<p className="text-xs text-muted-foreground">
|
{/* Last Test Info */}
|
||||||
Run a SMART test in the SMART Test tab for more detailed analysis.
|
<div className="grid grid-cols-2 gap-4 pt-2 border-t border-border/50">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Last Test Date</p>
|
||||||
|
<p className="font-medium text-sm">
|
||||||
|
{smartJsonData.timestamp
|
||||||
|
? new Date(smartJsonData.timestamp).toLocaleString()
|
||||||
|
: 'Unknown'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Test Type</p>
|
||||||
|
<p className="font-medium text-sm capitalize">{smartJsonData.test_type || 'Unknown'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
<p>No SMART test data available for this disk.</p>
|
<p>No SMART test data available for this disk.</p>
|
||||||
@@ -1700,30 +1731,92 @@ function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttri
|
|||||||
|
|
||||||
// Build attributes table - format differs for NVMe vs SATA
|
// Build attributes table - format differs for NVMe vs SATA
|
||||||
const isNvmeForTable = diskType === 'NVMe'
|
const isNvmeForTable = diskType === 'NVMe'
|
||||||
|
|
||||||
|
// Explanations for NVMe metrics
|
||||||
|
const nvmeExplanations: Record<string, string> = {
|
||||||
|
'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<string, string> = {
|
||||||
|
'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 attributeRows = smartAttributes.map((attr, i) => {
|
||||||
const statusColor = attr.status === 'ok' ? '#16a34a' : attr.status === 'warning' ? '#ca8a04' : '#dc2626'
|
const statusColor = attr.status === 'ok' ? '#16a34a' : attr.status === 'warning' ? '#ca8a04' : '#dc2626'
|
||||||
const statusBg = attr.status === 'ok' ? '#16a34a15' : attr.status === 'warning' ? '#ca8a0415' : '#dc262615'
|
const statusBg = attr.status === 'ok' ? '#16a34a15' : attr.status === 'warning' ? '#ca8a0415' : '#dc262615'
|
||||||
|
const explanation = getAttrExplanation(attr.name, isNvmeForTable)
|
||||||
|
|
||||||
if (isNvmeForTable) {
|
if (isNvmeForTable) {
|
||||||
// NVMe format: Metric | Value | Status
|
// NVMe format: Metric | Value | Status (with explanation)
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td class="col-name">${attr.name}</td>
|
<td class="col-name">
|
||||||
<td style="text-align:center;font-family:monospace;">${attr.value}</td>
|
<div style="font-weight:500;">${attr.name}</div>
|
||||||
<td><span class="f-tag" style="background:${statusBg};color:${statusColor}">${attr.status === 'ok' ? 'OK' : attr.status.toUpperCase()}</span></td>
|
${explanation ? `<div style="font-size:10px;color:#64748b;margin-top:2px;">${explanation}</div>` : ''}
|
||||||
|
</td>
|
||||||
|
<td style="text-align:center;font-family:monospace;vertical-align:top;padding-top:12px;">${attr.value}</td>
|
||||||
|
<td style="vertical-align:top;padding-top:8px;"><span class="f-tag" style="background:${statusBg};color:${statusColor}">${attr.status === 'ok' ? 'OK' : attr.status.toUpperCase()}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
`
|
`
|
||||||
} else {
|
} else {
|
||||||
// SATA format: ID | Attribute | Val | Worst | Thr | Raw | Status
|
// SATA format: ID | Attribute | Val | Worst | Thr | Raw | Status (with explanation)
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td style="font-weight:600;">${attr.id}</td>
|
<td style="font-weight:600;vertical-align:top;padding-top:12px;">${attr.id}</td>
|
||||||
<td class="col-name">${attr.name.replace(/_/g, ' ')}</td>
|
<td class="col-name">
|
||||||
<td style="text-align:center;">${attr.value}</td>
|
<div style="font-weight:500;">${attr.name.replace(/_/g, ' ')}</div>
|
||||||
<td style="text-align:center;">${attr.worst}</td>
|
${explanation ? `<div style="font-size:10px;color:#64748b;margin-top:2px;">${explanation}</div>` : ''}
|
||||||
<td class="hide-mobile" style="text-align:center;">${attr.threshold}</td>
|
</td>
|
||||||
<td class="col-raw">${attr.raw_value}</td>
|
<td style="text-align:center;vertical-align:top;padding-top:12px;">${attr.value}</td>
|
||||||
<td><span class="f-tag" style="background:${statusBg};color:${statusColor}">${attr.status === 'ok' ? 'OK' : attr.status.toUpperCase()}</span></td>
|
<td style="text-align:center;vertical-align:top;padding-top:12px;">${attr.worst}</td>
|
||||||
|
<td class="hide-mobile" style="text-align:center;vertical-align:top;padding-top:12px;">${attr.threshold}</td>
|
||||||
|
<td class="col-raw" style="vertical-align:top;padding-top:12px;">${attr.raw_value}</td>
|
||||||
|
<td style="vertical-align:top;padding-top:8px;"><span class="f-tag" style="background:${statusBg};color:${statusColor}">${attr.status === 'ok' ? 'OK' : attr.status.toUpperCase()}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
@@ -2585,19 +2678,36 @@ function SmartTestTab({ disk, observations = [] }: SmartTestTabProps) {
|
|||||||
fetchSmartStatus()
|
fetchSmartStatus()
|
||||||
|
|
||||||
// Poll for status updates
|
// 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 () => {
|
const pollInterval = setInterval(async () => {
|
||||||
|
pollCount++
|
||||||
try {
|
try {
|
||||||
const data = await fetchApi<SmartTestStatus>(`/api/storage/smart/${disk.name}`)
|
const statusData = await fetchApi<SmartTestStatus>(`/api/storage/smart/${disk.name}`)
|
||||||
setTestStatus(data)
|
setTestStatus(statusData)
|
||||||
if (data.status !== 'running') {
|
|
||||||
|
// Only clear runningTest when we get a definitive "not running" status
|
||||||
|
if (statusData.status !== 'running') {
|
||||||
clearInterval(pollInterval)
|
clearInterval(pollInterval)
|
||||||
setRunningTest(null)
|
setRunningTest(null)
|
||||||
|
// Refresh SMART JSON data to get new test results
|
||||||
|
fetchSmartStatus()
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
// Don't clear on error - keep showing progress
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety timeout: stop polling after max duration
|
||||||
|
if (pollCount >= maxPolls) {
|
||||||
clearInterval(pollInterval)
|
clearInterval(pollInterval)
|
||||||
setRunningTest(null)
|
setRunningTest(null)
|
||||||
|
// Refresh status one more time to get final result
|
||||||
|
fetchSmartStatus()
|
||||||
}
|
}
|
||||||
}, 5000)
|
}, 5000)
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : 'Failed to start test'
|
const message = err instanceof Error ? err.message : 'Failed to start test'
|
||||||
setTestError(message)
|
setTestError(message)
|
||||||
@@ -2666,6 +2776,7 @@ function SmartTestTab({ disk, observations = [] }: SmartTestTabProps) {
|
|||||||
<Play className="h-4 w-4" />
|
<Play className="h-4 w-4" />
|
||||||
Run SMART Test
|
Run SMART Test
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -2720,25 +2831,25 @@ function SmartTestTab({ disk, observations = [] }: SmartTestTabProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Test Progress */}
|
{/* Test Progress - Show when API reports running OR when we just started a test */}
|
||||||
{testStatus.status === 'running' && (
|
{(testStatus.status === 'running' || runningTest !== null) && (
|
||||||
<div className="border rounded-lg p-4 bg-blue-500/5 border-blue-500/20">
|
<div className="border rounded-lg p-4 bg-blue-500/5 border-blue-500/20">
|
||||||
<div className="flex items-center gap-3 mb-3">
|
<div className="flex items-center gap-3">
|
||||||
<Loader2 className="h-5 w-5 animate-spin text-blue-500" />
|
<Loader2 className="h-5 w-5 animate-spin text-blue-500" />
|
||||||
<div>
|
<div className="flex-1">
|
||||||
<p className="font-medium text-blue-500">
|
<p className="font-medium text-blue-500">
|
||||||
{testStatus.test_type === 'short' ? 'Short' : 'Extended'} test in progress
|
{(runningTest || testStatus.test_type) === 'short' ? 'Short' : 'Extended'} test in progress
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Please wait while the test completes...
|
Please wait while the test completes...
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Only show progress bar if the disk reports progress percentage */}
|
||||||
{testStatus.progress !== undefined && (
|
{testStatus.progress !== undefined && (
|
||||||
<Progress value={testStatus.progress} className="h-2 [&>div]:bg-blue-500" />
|
<Progress value={testStatus.progress} className="h-2 mt-3 [&>div]:bg-blue-500" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user