SSH (app/ssh_client.py — new):
- asyncssh-based client: start_smart_test, poll_smart_progress, abort_smart_test,
get_smart_attributes, run_badblocks with streaming progress callbacks
- SMART attribute table: monitors attrs 5/10/188/197/198/199 for warn/fail thresholds
- Falls back to REST API / mock simulation when ssh_host is not configured
Burn-in stages updated (burnin.py):
- _stage_smart_test: SSH path polls smartctl -a, stores raw output + parsed attributes
- _stage_surface_validate: SSH path streams badblocks, counts bad blocks vs configurable threshold
- _stage_final_check: SSH path checks smartctl attributes; DB fallback for mock mode
- New DB helpers: _append_stage_log, _update_stage_bad_blocks, _store_smart_attrs,
_store_smart_raw_output
Database (database.py):
- Migrations: burnin_stages.log_text, burnin_stages.bad_blocks,
drives.smart_attrs (JSON), smart_tests.raw_output
Settings (config.py + settings_store.py):
- ssh_host, ssh_port, ssh_user, ssh_password, ssh_key — all runtime-editable
- SSH section in Settings UI with Test SSH Connection button
Webhook (notifier.py):
- Added bad_blocks and timestamp fields to payload per SPEC
Drive reset (routes.py + drives_table.html):
- POST /api/v1/drives/{id}/reset — clears SMART state, smart_attrs; audit logged
- Reset button visible on drives with completed test state (no active burn-in)
Log drawer (app.js):
- Burn-In tab: shows raw stage log_text (SSH output) with bad block highlighting
- SMART tab: shows SMART attribute table with warn/fail colouring + raw smartctl output
Polish:
- Version badge (v1.0.0-6d) in header via Jinja2 global
- Parallel burn-in warning when max_parallel_burnins > 8 in Settings
- Stats page: avg duration by drive size + failure breakdown by stage
- settings.html: SSH section with key textarea, parallel warn div
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
65 lines
2.6 KiB
HTML
65 lines
2.6 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>{% block title %}TrueNAS Burn-In{% endblock %}</title>
|
|
<link rel="stylesheet" href="/static/app.css">
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<a class="header-brand" href="/" aria-label="Dashboard">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
|
|
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
|
|
<line x1="6" y1="6" x2="6.01" y2="6"></line>
|
|
<line x1="6" y1="18" x2="6.01" y2="18"></line>
|
|
</svg>
|
|
<span class="header-title">TrueNAS Burn-In</span>
|
|
</a>
|
|
<div class="header-meta">
|
|
<span class="live-indicator">
|
|
<span class="live-dot{% if poller and not poller.healthy %} degraded{% endif %}"></span>
|
|
{% if poller and poller.healthy %}Live{% else %}Polling error{% endif %}
|
|
</span>
|
|
{% if poller and poller.last_poll_at %}
|
|
<span class="poll-time">Last poll {{ poller.last_poll_at | format_dt }}</span>
|
|
{% endif %}
|
|
<button class="notif-btn" id="notif-btn" title="Enable browser notifications" aria-label="Toggle notifications">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
|
|
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
|
|
</svg>
|
|
</button>
|
|
<a class="header-link" href="/history">History</a>
|
|
<a class="header-link" href="/stats">Stats</a>
|
|
<a class="header-link" href="/audit">Audit</a>
|
|
<a class="header-link" href="/settings">Settings</a>
|
|
<a class="header-link" href="/docs" target="_blank" rel="noopener">API</a>
|
|
<span class="header-version">v{{ app_version if app_version is defined else '—' }}</span>
|
|
</div>
|
|
</header>
|
|
|
|
{% if stale %}
|
|
<div class="banner banner-warn">
|
|
⚠ Data may be stale — no successful poll in over {{ stale_seconds }}s
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if poller and poller.last_error %}
|
|
<div class="banner banner-error">
|
|
✕ Poll error: {{ poller.last_error }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<main>
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
|
|
<div id="toast-container" aria-live="polite"></div>
|
|
<script src="https://unpkg.com/htmx.org@2.0.3/dist/htmx.min.js"></script>
|
|
<script src="https://unpkg.com/htmx-ext-sse@2.2.2/sse.js"></script>
|
|
<script src="/static/app.js"></script>
|
|
</body>
|
|
</html>
|