Full-stack burn-in orchestration dashboard (Stages 1–6d complete): FastAPI backend, SQLite/WAL, SSE live dashboard, mock TrueNAS server, SMTP/webhook notifications, batch burn-in, settings UI, audit log, stats page, cancel SMART/burn-in, drag-to-reorder stages. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
174 lines
7.3 KiB
HTML
174 lines
7.3 KiB
HTML
{%- macro smart_cell(smart) -%}
|
|
<div class="smart-cell">
|
|
{%- if smart.state == 'running' -%}
|
|
<div class="progress-wrap">
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" style="width:{{ smart.percent or 0 }}%"></div>
|
|
</div>
|
|
<span class="progress-pct">{{ smart.percent or 0 }}%</span>
|
|
</div>
|
|
{%- if smart.eta_seconds %}
|
|
<div class="eta-text">{{ smart.eta_seconds | format_eta }}</div>
|
|
{%- endif %}
|
|
{%- elif smart.state == 'passed' -%}
|
|
<span class="chip chip-passed">Passed</span>
|
|
{%- elif smart.state == 'failed' -%}
|
|
<span class="chip chip-failed" title="{{ smart.error_text or '' }}">Failed</span>
|
|
{%- elif smart.state == 'aborted' -%}
|
|
<span class="chip chip-aborted">Aborted</span>
|
|
{%- else -%}
|
|
<span class="cell-empty">—</span>
|
|
{%- endif -%}
|
|
</div>
|
|
{%- endmacro -%}
|
|
|
|
{%- macro burnin_cell(bi) -%}
|
|
<div class="burnin-cell">
|
|
{%- if bi is none -%}
|
|
<span class="cell-empty">—</span>
|
|
{%- elif bi.state == 'queued' -%}
|
|
<span class="chip chip-queued">Queued</span>
|
|
{%- elif bi.state == 'running' -%}
|
|
<div class="progress-wrap">
|
|
<div class="progress-bar">
|
|
<div class="progress-fill progress-fill-green" style="width:{{ bi.percent or 0 }}%"></div>
|
|
</div>
|
|
<span class="progress-pct">{{ bi.percent or 0 }}%</span>
|
|
</div>
|
|
<div class="burnin-meta">
|
|
{%- if bi.stage_name %}
|
|
<span class="stage-name">{{ bi.stage_name | replace('_', ' ') | title }}</span>
|
|
{%- endif %}
|
|
{%- if bi.started_at %}
|
|
<span class="elapsed-timer" data-started="{{ bi.started_at }}">{{ bi.started_at | format_elapsed }}</span>
|
|
{%- endif %}
|
|
</div>
|
|
{%- elif bi.state == 'passed' -%}
|
|
<span class="chip chip-passed">Passed</span>
|
|
{%- elif bi.state == 'failed' -%}
|
|
<span class="chip chip-failed">Failed{% if bi.stage_name %} ({{ bi.stage_name | replace('_',' ') }}){% endif %}</span>
|
|
{%- elif bi.state == 'cancelled' -%}
|
|
<span class="chip chip-aborted">Cancelled</span>
|
|
{%- elif bi.state == 'unknown' -%}
|
|
<span class="chip chip-unknown">Unknown</span>
|
|
{%- else -%}
|
|
<span class="cell-empty">—</span>
|
|
{%- endif -%}
|
|
</div>
|
|
{%- endmacro -%}
|
|
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th class="col-check">
|
|
<input type="checkbox" id="select-all-cb" class="drive-cb" title="Select all idle drives">
|
|
</th>
|
|
<th class="col-drive">Drive</th>
|
|
<th class="col-serial">Serial</th>
|
|
<th class="col-size">Size</th>
|
|
<th class="col-temp">Temp</th>
|
|
<th class="col-health">Health</th>
|
|
<th class="col-smart">Short SMART</th>
|
|
<th class="col-smart">Long SMART</th>
|
|
<th class="col-burnin">Burn-In</th>
|
|
<th class="col-actions">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="drives-tbody">
|
|
{%- if drives %}
|
|
{%- for drive in drives %}
|
|
{%- set bi_active = drive.burnin and drive.burnin.state in ('queued', 'running') %}
|
|
{%- set short_busy = drive.smart_short and drive.smart_short.state == 'running' %}
|
|
{%- set long_busy = drive.smart_long and drive.smart_long.state == 'running' %}
|
|
{%- set selectable = not bi_active and not short_busy and not long_busy %}
|
|
<tr data-status="{{ drive.status }}" id="drive-{{ drive.id }}">
|
|
<td class="col-check">
|
|
{%- if selectable %}
|
|
<input type="checkbox" class="drive-checkbox" data-drive-id="{{ drive.id }}">
|
|
{%- endif %}
|
|
</td>
|
|
<td class="col-drive">
|
|
<span class="drive-name">{{ drive.devname }}</span>
|
|
<span class="drive-model">{{ drive.model or "Unknown" }}</span>
|
|
{%- if drive.location %}
|
|
<span class="drive-location"
|
|
data-drive-id="{{ drive.id }}"
|
|
data-field="location"
|
|
title="Click to edit location">{{ drive.location }}</span>
|
|
{%- else %}
|
|
<span class="drive-location drive-location-empty"
|
|
data-drive-id="{{ drive.id }}"
|
|
data-field="location"
|
|
title="Click to set location">+ location</span>
|
|
{%- endif %}
|
|
</td>
|
|
<td class="col-serial mono">{{ drive.serial or "—" }}</td>
|
|
<td class="col-size">{{ drive.size_bytes | format_bytes }}</td>
|
|
<td class="col-temp">
|
|
{%- if drive.temperature_c is not none %}
|
|
<span class="temp {{ drive.temperature_c | temp_class }}">{{ drive.temperature_c }}°C</span>
|
|
{%- else %}
|
|
<span class="cell-empty">—</span>
|
|
{%- endif %}
|
|
</td>
|
|
<td class="col-health">
|
|
<span class="chip chip-{{ drive.smart_health | lower }}">{{ drive.smart_health }}</span>
|
|
</td>
|
|
<td class="col-smart">{{ smart_cell(drive.smart_short) }}</td>
|
|
<td class="col-smart">{{ smart_cell(drive.smart_long) }}</td>
|
|
<td class="col-burnin">{{ burnin_cell(drive.burnin) }}</td>
|
|
<td class="col-actions">
|
|
<div class="action-group">
|
|
{%- if bi_active %}
|
|
<!-- Burn-in running/queued: only show cancel -->
|
|
<button class="btn-action btn-cancel"
|
|
data-job-id="{{ drive.burnin.id }}">✕ Burn-In</button>
|
|
{%- else %}
|
|
<!-- Short SMART: show cancel if running, else start button -->
|
|
{%- if short_busy %}
|
|
<button class="btn-action btn-cancel-smart"
|
|
data-drive-id="{{ drive.id }}"
|
|
data-test-type="short"
|
|
title="Cancel Short SMART test">✕ Short</button>
|
|
{%- else %}
|
|
<button class="btn-action btn-smart-short{% if long_busy %} btn-disabled{% endif %}"
|
|
data-drive-id="{{ drive.id }}"
|
|
data-test-type="SHORT"
|
|
{% if long_busy %}disabled{% endif %}
|
|
title="Start Short SMART test">Short</button>
|
|
{%- endif %}
|
|
<!-- Long SMART: show cancel if running, else start button -->
|
|
{%- if long_busy %}
|
|
<button class="btn-action btn-cancel-smart"
|
|
data-drive-id="{{ drive.id }}"
|
|
data-test-type="long"
|
|
title="Cancel Long SMART test">✕ Long</button>
|
|
{%- else %}
|
|
<button class="btn-action btn-smart-long{% if short_busy %} btn-disabled{% endif %}"
|
|
data-drive-id="{{ drive.id }}"
|
|
data-test-type="LONG"
|
|
{% if short_busy %}disabled{% endif %}
|
|
title="Start Long SMART test (~several hours)">Long</button>
|
|
{%- endif %}
|
|
<!-- Burn-In -->
|
|
<button class="btn-action btn-start{% if short_busy or long_busy %} btn-disabled{% endif %}"
|
|
data-drive-id="{{ drive.id }}"
|
|
data-devname="{{ drive.devname }}"
|
|
data-serial="{{ drive.serial or '' }}"
|
|
data-model="{{ drive.model or 'Unknown' }}"
|
|
data-size="{{ drive.size_bytes | format_bytes }}"
|
|
data-health="{{ drive.smart_health }}"
|
|
{% if short_busy or long_busy %}disabled{% endif %}
|
|
title="Start Burn-In">Burn-In</button>
|
|
{%- endif %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{%- endfor %}
|
|
{%- else %}
|
|
<tr>
|
|
<td colspan="10" class="empty-state">No drives found. Waiting for first poll…</td>
|
|
</tr>
|
|
{%- endif %}
|
|
</tbody>
|
|
</table>
|