update storage-overview.tsx

This commit is contained in:
MacRimi
2026-04-13 10:07:09 +02:00
parent 07f1098418
commit a6149e3cd8
2 changed files with 104 additions and 71 deletions

View File

@@ -2155,22 +2155,52 @@ function SmartTestTab({ disk, observations = [] }: SmartTestTabProps) {
// Extract SMART attributes from testStatus for the report // Extract SMART attributes from testStatus for the report
const smartAttributes = testStatus.smart_data?.attributes || [] const smartAttributes = testStatus.smart_data?.attributes || []
// Fetch current SMART status on mount
useEffect(() => {
fetchSmartStatus()
}, [disk.name])
const fetchSmartStatus = async () => { const fetchSmartStatus = async () => {
try { try {
setLoading(true) setLoading(true)
const data = await fetchApi<SmartTestStatus>(`/api/storage/smart/${disk.name}`) const data = await fetchApi<SmartTestStatus>(`/api/storage/smart/${disk.name}`)
setTestStatus(data) setTestStatus(data)
} catch { return data
setTestStatus({ status: 'idle' }) } catch {
} finally { setTestStatus({ status: 'idle' })
setLoading(false) return { status: 'idle' }
} finally {
setLoading(false)
}
}
// Fetch current SMART status on mount and start polling if test is running
useEffect(() => {
let pollInterval: NodeJS.Timeout | null = null
const checkAndPoll = async () => {
const data = await fetchSmartStatus()
// If a test is already running, start polling
if (data.status === 'running') {
pollInterval = setInterval(async () => {
try {
const status = await fetchApi<SmartTestStatus>(`/api/storage/smart/${disk.name}`)
setTestStatus(status)
if (status.status !== 'running' && pollInterval) {
clearInterval(pollInterval)
pollInterval = null
}
} catch {
if (pollInterval) {
clearInterval(pollInterval)
pollInterval = null
} }
} }
}, 5000)
}
}
checkAndPoll()
return () => {
if (pollInterval) clearInterval(pollInterval)
}
}, [disk.name])
const [testError, setTestError] = useState<string | null>(null) const [testError, setTestError] = useState<string | null>(null)
const [installing, setInstalling] = useState(false) const [installing, setInstalling] = useState(false)
@@ -2206,11 +2236,15 @@ function SmartTestTab({ disk, observations = [] }: SmartTestTabProps) {
try { try {
setRunningTest(testType) setRunningTest(testType)
setTestError(null) setTestError(null)
await fetchApi(`/api/storage/smart/${disk.name}/test`, { await fetchApi(`/api/storage/smart/${disk.name}/test`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ test_type: testType }) body: JSON.stringify({ test_type: testType })
}) })
// Immediately fetch status to show progress bar
fetchSmartStatus()
// Poll for status updates // Poll for status updates
const pollInterval = setInterval(async () => { const pollInterval = setInterval(async () => {
try { try {
@@ -2294,61 +2328,61 @@ function SmartTestTab({ disk, observations = [] }: SmartTestTabProps) {
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"
size="sm" size="sm"
onClick={() => runSmartTest('short')} onClick={() => runSmartTest('short')}
disabled={runningTest !== null} disabled={runningTest !== null || testStatus.status === 'running'}
className="gap-2 bg-blue-500/10 border-blue-500/30 text-blue-500 hover:bg-blue-500/20 hover:text-blue-400" className="gap-2 bg-blue-500/10 border-blue-500/30 text-blue-500 hover:bg-blue-500/20 hover:text-blue-400"
> >
{runningTest === 'short' ? ( {runningTest === 'short' || (testStatus.status === 'running' && testStatus.test_type === 'short') ? (
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
) : ( ) : (
<Activity className="h-4 w-4" /> <Activity className="h-4 w-4" />
)} )}
Short Test (~2 min) Short Test (~2 min)
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => runSmartTest('long')} onClick={() => runSmartTest('long')}
disabled={runningTest !== null} disabled={runningTest !== null || testStatus.status === 'running'}
className="gap-2 bg-blue-500/10 border-blue-500/30 text-blue-500 hover:bg-blue-500/20 hover:text-blue-400" className="gap-2 bg-blue-500/10 border-blue-500/30 text-blue-500 hover:bg-blue-500/20 hover:text-blue-400"
> >
{runningTest === 'long' ? ( {runningTest === 'long' || (testStatus.status === 'running' && testStatus.test_type === 'long') ? (
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
) : ( ) : (
<Activity className="h-4 w-4" /> <Activity className="h-4 w-4" />
)} )}
Extended Test (background) Extended Test (background)
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={fetchSmartStatus} onClick={fetchSmartStatus}
disabled={runningTest !== null} className="gap-2 bg-blue-500/10 border-blue-500/30 text-blue-500 hover:bg-blue-500/20 hover:text-blue-400"
className="gap-2 bg-blue-500/10 border-blue-500/30 text-blue-500 hover:bg-blue-500/20 hover:text-blue-400" >
> <Activity className="h-4 w-4" />
<Activity className="h-4 w-4" /> Refresh Status
Refresh Status </Button>
</Button>
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Short test takes ~2 minutes. Extended test runs in the background and can take several hours for large disks. 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. You will receive a notification when the test completes.
</p> </p>
{/* Error Message */} {/* Error Message */}
{testError && ( {testError && (
<div className="flex items-start gap-2 p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400"> <div className="flex items-start gap-2 p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400">
<AlertTriangle className="h-4 w-4 mt-0.5 flex-shrink-0" /> <AlertTriangle className="h-4 w-4 mt-0.5 flex-shrink-0" />
<div className="flex-1"> <div className="flex-1">
<p className="text-sm font-medium">Failed to start test</p> <p className="text-sm font-medium">Failed to start test</p>
<p className="text-xs opacity-80">{testError}</p> <p className="text-xs opacity-80">{testError}</p>
</div> </div>
</div> </div>
)} )}
</div>
</div>
{/* Test Progress */} {/* Test Progress */}
{testStatus.status === 'running' && ( {testStatus.status === 'running' && (

View File

@@ -6698,27 +6698,21 @@ def api_smart_status(disk_name):
def api_smart_run_test(disk_name): def api_smart_run_test(disk_name):
"""Start a SMART self-test on a disk.""" """Start a SMART self-test on a disk."""
try: try:
logging.info(f"[SMART Test] Starting test for disk: {disk_name}")
# Validate disk name (security) # Validate disk name (security)
if not re.match(r'^[a-zA-Z0-9]+$', disk_name): if not re.match(r'^[a-zA-Z0-9]+$', disk_name):
logging.warning(f"[SMART Test] Invalid disk name rejected: {disk_name}")
return jsonify({'error': 'Invalid disk name'}), 400 return jsonify({'error': 'Invalid disk name'}), 400
device = f'/dev/{disk_name}' device = f'/dev/{disk_name}'
if not os.path.exists(device): if not os.path.exists(device):
logging.warning(f"[SMART Test] Device not found: {device}")
return jsonify({'error': 'Device not found'}), 404 return jsonify({'error': 'Device not found'}), 404
data = request.get_json() or {} data = request.get_json() or {}
test_type = data.get('test_type', 'short') test_type = data.get('test_type', 'short')
logging.info(f"[SMART Test] Test type: {test_type}, Device: {device}")
if test_type not in ('short', 'long'): if test_type not in ('short', 'long'):
return jsonify({'error': 'Invalid test type. Use "short" or "long"'}), 400 return jsonify({'error': 'Invalid test type. Use "short" or "long"'}), 400
is_nvme = _is_nvme(disk_name) is_nvme = _is_nvme(disk_name)
logging.info(f"[SMART Test] Is NVMe: {is_nvme}")
# Check tools and auto-install if missing # Check tools and auto-install if missing
tools = _ensure_smart_tools(install_if_missing=True) tools = _ensure_smart_tools(install_if_missing=True)
@@ -6760,17 +6754,22 @@ def api_smart_run_test(disk_name):
if not supports_selftest: if not supports_selftest:
return jsonify({'error': 'This NVMe device does not support self-test (OACS bit 4 not set)'}), 400 return jsonify({'error': 'This NVMe device does not support self-test (OACS bit 4 not set)'}), 400
logging.info(f"[SMART Test] Running: nvme device-self-test {device} --self-test-code={code}")
proc = subprocess.run( proc = subprocess.run(
['nvme', 'device-self-test', device, f'--self-test-code={code}'], ['nvme', 'device-self-test', device, f'--self-test-code={code}'],
capture_output=True, text=True, timeout=30 capture_output=True, text=True, timeout=30
) )
logging.info(f"[SMART Test] Result: returncode={proc.returncode}, stdout={proc.stdout[:200] if proc.stdout else ''}, stderr={proc.stderr[:200] if proc.stderr else ''}")
if proc.returncode != 0: if proc.returncode != 0:
error_msg = proc.stderr.strip() or proc.stdout.strip() or 'Unknown error' error_msg = proc.stderr.strip() or proc.stdout.strip() or 'Unknown error'
# Test already in progress - return success so frontend shows progress
if 'in progress' in error_msg.lower() or '0x211d' in error_msg.lower():
return jsonify({
'success': True,
'message': 'Test started successfully',
'test_type': test_type
}), 200
# Some NVMe devices don't support self-test # Some NVMe devices don't support self-test
if 'not support' in error_msg.lower() or 'invalid' in error_msg.lower() or 'operation' in error_msg.lower(): if 'not support' in error_msg.lower() or 'invalid' in error_msg.lower():
return jsonify({'error': f'This NVMe device does not support self-test: {error_msg}'}), 400 return jsonify({'error': f'This NVMe device does not support self-test: {error_msg}'}), 400
# Check for permission errors # Check for permission errors
if 'permission' in error_msg.lower() or 'operation not permitted' in error_msg.lower(): if 'permission' in error_msg.lower() or 'operation not permitted' in error_msg.lower():