mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-05-01 03:46:22 +00:00
update storage-overview.tsx
This commit is contained in:
@@ -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' && (
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
Reference in New Issue
Block a user