mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-25 08:56:21 +00:00
update storage-overview.tsx
This commit is contained in:
@@ -1818,7 +1818,7 @@ export function StorageOverview() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate SMART Report HTML and open in new window (same pattern as Lynis/Latency reports)
|
// Generate SMART Report HTML and open in new window (same pattern as Lynis/Latency reports)
|
||||||
function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttributes: SmartAttribute[], observations: DiskObservation[] = [], lastTestDate?: string, targetWindow?: Window) {
|
function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttributes: SmartAttribute[], observations: DiskObservation[] = [], lastTestDate?: string, targetWindow?: Window, isHistorical = false) {
|
||||||
const now = new Date().toLocaleString()
|
const now = new Date().toLocaleString()
|
||||||
const logoUrl = `${window.location.origin}/images/proxmenux-logo.png`
|
const logoUrl = `${window.location.origin}/images/proxmenux-logo.png`
|
||||||
const reportId = `SMART-${Date.now().toString(36).toUpperCase()}`
|
const reportId = `SMART-${Date.now().toString(36).toUpperCase()}`
|
||||||
@@ -2417,7 +2417,7 @@ function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttri
|
|||||||
<div style="font-size:12px;font-weight:600;color:#1e293b;">${now}</div>
|
<div style="font-size:12px;font-weight:600;color:#1e293b;">${now}</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;padding:10px 12px;">
|
<div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;padding:10px 12px;">
|
||||||
<div style="font-size:10px;color:#475569;font-weight:600;text-transform:uppercase;">Last Test Type</div>
|
<div style="font-size:10px;color:#475569;font-weight:600;text-transform:uppercase;">${isHistorical ? 'Test Type' : 'Last Test Type'}</div>
|
||||||
<div style="font-size:12px;font-weight:600;color:#1e293b;">${testStatus.last_test?.type || 'N/A'}</div>
|
<div style="font-size:12px;font-weight:600;color:#1e293b;">${testStatus.last_test?.type || 'N/A'}</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;padding:10px 12px;">
|
<div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;padding:10px 12px;">
|
||||||
@@ -2740,7 +2740,7 @@ ${!isNvmeDisk && diskType === 'SSD' ? (() => {
|
|||||||
|
|
||||||
<!-- 5. Last Test Result -->
|
<!-- 5. Last Test Result -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title">${isNvmeDisk ? '5' : '4'}. Last Self-Test Result</div>
|
<div class="section-title">${isNvmeDisk ? '5' : '4'}. ${isHistorical ? 'Self-Test Result' : 'Last Self-Test Result'}</div>
|
||||||
${testStatus.last_test ? `
|
${testStatus.last_test ? `
|
||||||
<div class="grid-4">
|
<div class="grid-4">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -2789,6 +2789,28 @@ ${!isNvmeDisk && diskType === 'SSD' ? (() => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
|
` : lastTestDate ? `
|
||||||
|
<div class="grid-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-label">${isHistorical ? 'Test Type' : 'Last Test Type'}</div>
|
||||||
|
<div class="card-value" style="text-transform:capitalize;">${testStatus.test_type || 'Extended'}</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-label">Result</div>
|
||||||
|
<div class="card-value" style="color:#16a34a;">Passed</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-label">Date</div>
|
||||||
|
<div class="card-value" style="font-size:11px;">${new Date(lastTestDate).toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-label">At Power-On Hours</div>
|
||||||
|
<div class="card-value">${fmtNum(powerOnHours)}h</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:8px;padding:8px 12px;background:#f1f5f9;border:1px solid #e2e8f0;border-radius:6px;font-size:11px;color:#475569;">
|
||||||
|
<strong>Note:</strong> This disk's firmware does not maintain an internal self-test log. Test results are tracked by ProxMenux Monitor.
|
||||||
|
</div>
|
||||||
` : `
|
` : `
|
||||||
<div style="text-align:center;padding:20px;color:#64748b;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;">
|
<div style="text-align:center;padding:20px;color:#64748b;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;">
|
||||||
No self-test history available. Run a SMART self-test to see results here.
|
No self-test history available. Run a SMART self-test to see results here.
|
||||||
@@ -3353,51 +3375,11 @@ function HistoryTab({ disk }: { disk: DiskInfo }) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setViewingReport(entry.filename)
|
setViewingReport(entry.filename)
|
||||||
const jsonData = await fetchApi<Record<string, unknown>>(`/api/storage/smart/${disk.name}/history/${entry.filename}`)
|
// Fetch full SMART status from backend (same data as SMART tab uses)
|
||||||
|
const fullStatus = await fetchApi<SmartTestStatus>(`/api/storage/smart/${disk.name}`)
|
||||||
|
const attrs = fullStatus.smart_data?.attributes || []
|
||||||
|
|
||||||
const isNvme = disk.name.includes('nvme')
|
openSmartReport(disk, fullStatus, attrs, [], entry.timestamp, reportWindow || undefined, true)
|
||||||
let attrs: SmartAttribute[] = []
|
|
||||||
|
|
||||||
if (isNvme) {
|
|
||||||
const fieldMap: [string, string][] = [
|
|
||||||
['critical_warning', 'Critical Warning'], ['temperature', 'Temperature'],
|
|
||||||
['avail_spare', 'Available Spare'], ['percent_used', 'Percentage Used'],
|
|
||||||
['data_units_written', 'Data Units Written'], ['data_units_read', 'Data Units Read'],
|
|
||||||
['power_cycles', 'Power Cycles'], ['power_on_hours', 'Power On Hours'],
|
|
||||||
['unsafe_shutdowns', 'Unsafe Shutdowns'], ['media_errors', 'Media Errors'],
|
|
||||||
['num_err_log_entries', 'Error Log Entries'],
|
|
||||||
]
|
|
||||||
fieldMap.forEach(([key, name], i) => {
|
|
||||||
if (jsonData[key] !== undefined) {
|
|
||||||
const v = jsonData[key] as number
|
|
||||||
const status = (key === 'critical_warning' || key === 'media_errors') && v > 0 ? 'critical' as const : 'ok' as const
|
|
||||||
attrs.push({ id: i + 1, name, value: String(v), worst: '-', threshold: '-', raw_value: String(v), status })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
const ataTable = (jsonData as Record<string, unknown>)?.ata_smart_attributes as { table?: Array<Record<string, unknown>> }
|
|
||||||
if (ataTable?.table) {
|
|
||||||
attrs = ataTable.table.map(a => ({
|
|
||||||
id: (a.id as number) || 0,
|
|
||||||
name: (a.name as string) || '',
|
|
||||||
value: (a.value as number) || 0,
|
|
||||||
worst: (a.worst as number) || 0,
|
|
||||||
threshold: (a.thresh as number) || 0,
|
|
||||||
raw_value: (a.raw as Record<string, unknown>)?.string as string || String((a.raw as Record<string, unknown>)?.value || 0),
|
|
||||||
status: 'ok' as const
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const testStatus: SmartTestStatus = {
|
|
||||||
status: 'idle',
|
|
||||||
smart_data: { device: disk.name, model: disk.model || '', serial: disk.serial || '', firmware: '', smart_status: 'passed', temperature: disk.temperature, power_on_hours: disk.power_on_hours || 0, attributes: attrs }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate report — write directly into the already-open window (avoids popup blocker)
|
|
||||||
// openSmartReport creates a blob and calls window.open, but we already have a window.
|
|
||||||
// So we call the same logic but write to our existing window.
|
|
||||||
openSmartReport(disk, testStatus, attrs, [], entry.timestamp, reportWindow || undefined)
|
|
||||||
} catch {
|
} catch {
|
||||||
if (reportWindow && !reportWindow.closed) {
|
if (reportWindow && !reportWindow.closed) {
|
||||||
reportWindow.document.body.innerHTML = '<p style="color:#ef4444;text-align:center;margin-top:40vh">Failed to load report data.</p>'
|
reportWindow.document.body.innerHTML = '<p style="color:#ef4444;text-align:center;margin-top:40vh">Failed to load report data.</p>'
|
||||||
@@ -3845,7 +3827,7 @@ function ScheduleTab({ disk }: { disk: DiskInfo }) {
|
|||||||
<Button
|
<Button
|
||||||
onClick={handleSaveSchedule}
|
onClick={handleSaveSchedule}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
className="bg-purple-600 hover:bg-purple-700"
|
className="bg-purple-600 hover:bg-purple-700 text-white"
|
||||||
>
|
>
|
||||||
{saving ? 'Saving...' : 'Save Schedule'}
|
{saving ? 'Saving...' : 'Save Schedule'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -7156,7 +7156,10 @@ def api_smart_run_test(disk_name):
|
|||||||
[ -z "$op" ] || [ "$op" -eq 0 ] && break
|
[ -z "$op" ] || [ "$op" -eq 0 ] && break
|
||||||
sleep {sleep_interval}
|
sleep {sleep_interval}
|
||||||
done
|
done
|
||||||
nvme smart-log -o json {device} > {json_path} 2>/dev/null
|
# Save complete data: smartctl gives device info + health + self-test log in one JSON
|
||||||
|
smartctl -a --json=c {device} > {json_path} 2>/dev/null
|
||||||
|
# Fallback to nvme smart-log if smartctl fails
|
||||||
|
[ ! -s {json_path} ] && nvme smart-log -o json {device} > {json_path} 2>/dev/null
|
||||||
''',
|
''',
|
||||||
shell=True, start_new_session=True,
|
shell=True, start_new_session=True,
|
||||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
||||||
|
|||||||
Reference in New Issue
Block a user