nas-burnin/claude-sandbox/truenas-burnin/app/templates/history.html
echoparkbaby 3e0000528f TrueNAS Burn-In Dashboard v0.9.0 — Live mode, thermal monitoring, adaptive concurrency
Go live against real TrueNAS SCALE 25.10:
- Remove mock-truenas dependency; mount SSH key as Docker secret
- Filter expired disk records from /api/v2.0/disk (expiretime field)
- Route all SMART operations through SSH (SCALE 25.10 removed REST smart/test endpoint)
- Poll drive temperatures via POST /api/v2.0/disk/temperatures (SCALE-specific)
- Store raw smartctl output in smart_tests.raw_output for proof of test execution
- Fix percent-remaining=0 false jump to 100% on test start
- Fix terminal WebSocket: add mounted key file fallback (/run/secrets/ssh_key)
- Fix WebSocket support: uvicorn → uvicorn[standard] (installs websockets)

HBA/system sensor temps on dashboard:
- SSH to TrueNAS and run sensors -j each poll cycle
- Parse coretemp (CPU package) and pch_* (PCH/chipset — storage I/O proxy)
- Render as compact chips in stats bar, color-coded green/yellow/red
- Live updates via new SSE system-sensors event every 12s

Adaptive concurrency signal:
- Thermal pressure indicator in stats bar: hidden when OK, WARM/HOT when running
  burn-in drives hit temp_warn_c / temp_crit_c thresholds
- Thermal gate in burn-in queue: jobs wait up to 3 min before acquiring semaphore
  slot if running drives are already at warning temp; times out and proceeds

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 06:33:36 -05:00

95 lines
3.1 KiB
HTML

{% extends "layout.html" %}
{% block title %}TrueNAS Burn-In — History{% endblock %}
{% block content %}
<div class="page-toolbar">
<h1 class="page-title">Burn-In History</h1>
<div class="toolbar-right">
<a class="btn-export" href="/api/v1/burnin/export.csv">Export CSV</a>
</div>
</div>
<!-- State filter tabs -->
<div class="filter-bar" style="margin-bottom: 14px;">
{% set states = [('all','All'), ('passed','Passed'), ('failed','Failed'), ('cancelled','Cancelled'), ('running','Running'), ('unknown','Unknown')] %}
{% for val, label in states %}
<a class="filter-btn{% if active_state == val %} active{% endif %}"
href="/history?state={{ val }}&page=1">
{{ label }}
{% if val in counts %}<span class="badge">{{ counts[val] }}</span>{% endif %}
</a>
{% endfor %}
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th class="col-job">#</th>
<th class="col-drive">Drive</th>
<th>Profile</th>
<th>State</th>
<th>Operator</th>
<th>Started</th>
<th>Completed</th>
<th>Duration</th>
<th>Error</th>
<th class="col-actions"></th>
</tr>
</thead>
<tbody>
{% if jobs %}
{% for j in jobs %}
<tr>
<td class="mono text-muted">{{ j.id }}</td>
<td class="col-drive">
<span class="drive-name">{{ j.devname }}</span>
<span class="drive-model">{{ j.serial }}</span>
</td>
<td>
<span class="chip chip-{{ 'red' if j.profile == 'full' else 'gray' }}">{{ j.profile }}</span>
</td>
<td>
<span class="chip chip-{{ j.state }}">{{ j.state }}</span>
</td>
<td class="text-muted">{{ j.operator or '—' }}</td>
<td class="mono text-muted">{{ j.started_at | format_dt_full }}</td>
<td class="mono text-muted">{{ j.finished_at | format_dt_full }}</td>
<td class="mono text-muted">{{ j.duration_seconds | format_duration }}</td>
<td class="error-cell">
{% if j.error_text %}
<span class="error-snippet" title="{{ j.error_text }}">{{ j.error_text[:60] }}{% if j.error_text | length > 60 %}…{% endif %}</span>
{% else %}—{% endif %}
</td>
<td>
<a class="btn-detail" href="/history/{{ j.id }}">Detail</a>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="10" class="empty-state">No burn-in jobs found.</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if total_pages > 1 %}
<div class="pagination">
{% if page > 1 %}
<a class="page-btn" href="/history?state={{ active_state }}&page={{ page - 1 }}">← Prev</a>
{% endif %}
<span class="page-info">Page {{ page }} of {{ total_pages }} &nbsp;·&nbsp; {{ total_count }} jobs</span>
{% if page < total_pages %}
<a class="page-btn" href="/history?state={{ active_state }}&page={{ page + 1 }}">Next →</a>
{% endif %}
</div>
{% else %}
<div class="pagination">
<span class="page-info">{{ total_count }} job{% if total_count != 1 %}s{% endif %}</span>
</div>
{% endif %}
{% endblock %}