nas-burnin/app/templates/components/drives_table.html
Brandon Walter 659f540270
Some checks are pending
Security scan / pip-audit (push) Waiting to run
Security scan / bandit (push) Waiting to run
Security scan / gitleaks (push) Waiting to run
Security scan / mypy (push) Waiting to run
fix: drop redundant stage suffix from Burn-In failed chip
Per user: '(LONG SMART)' was redundant since the LONG SMART column
already shows FAILED. Same for short SMART and surface_validate
(the dominant case — the drawer shows per-stage Reason for digging).

Suffix kept for precheck / final_check since those are rare enough
that the hint is genuinely helpful.
2026-05-09 12:33:26 -07:00

234 lines
13 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' -%}
{# Suppress the stage suffix for SMART + surface_validate stages.
SMART has its own columns, and surface_validate is the dominant
case so a redundant suffix just adds visual noise. The drawer
shows the per-stage Reason for any digging. Keep the suffix for
precheck / final_check since those are rare enough that the hint
is helpful. #}
<span class="chip chip-failed">Failed{% if bi.stage_name and bi.stage_name not in ('short_smart', 'long_smart', 'surface_validate') %} ({{ 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 sortable" data-sort-key="drive">Drive</th>
<th class="col-serial sortable" data-sort-key="serial">Serial</th>
<th class="col-size sortable" data-sort-key="size">Size</th>
<th class="col-temp sortable" data-sort-key="temp">Temp</th>
<th class="col-health sortable" data-sort-key="health">Health</th>
<th class="col-smart sortable" data-sort-key="short">Short SMART</th>
<th class="col-smart sortable" data-sort-key="long">Long SMART</th>
<th class="col-burnin sortable" data-sort-key="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 pool_locked = drive.pool_name and not drive.pool_unlocked_until %}
{%- set is_boot_pool = drive.pool_name == 'boot-pool' %}
{%- set is_exported = drive.pool_role == 'exported' %}
{%- set is_mounted = drive.pool_role == 'mounted' %}
{%- set selectable = not bi_active and not short_busy and not long_busy and not pool_locked %}
{%- set bi_done = drive.burnin and drive.burnin.state in ('passed', 'failed', 'cancelled', 'unknown') %}
{%- set smart_done = (drive.smart_short and drive.smart_short.state in ('passed','failed','aborted'))
or (drive.smart_long and drive.smart_long.state in ('passed','failed','aborted')) %}
{%- set can_reset = (bi_done or smart_done) and not bi_active and not short_busy and not long_busy and not pool_locked %}
{%- set short_state = drive.smart_short.state if drive.smart_short else 'idle' %}
{%- set long_state = drive.smart_long.state if drive.smart_long else 'idle' %}
{%- set burnin_state = drive.burnin.state if drive.burnin else '' %}
<tr data-status="{{ drive.status }}" id="drive-{{ drive.id }}"
data-sort-drive="{{ drive.devname }}"
data-sort-serial="{{ (drive.serial or '') | lower }}"
data-sort-size="{{ drive.size_bytes or 0 }}"
data-sort-temp="{{ drive.temperature_c if drive.temperature_c is not none else '' }}"
data-sort-health="{{ {'PASSED': 1, 'WARNING': 2, 'FAILED': 3, 'UNKNOWN': 4}.get(drive.smart_health, 9) }}"
data-sort-short="{{ {'running': 1, 'failed': 2, 'aborted': 3, 'passed': 4, 'idle': 5}.get(short_state, 9) }}"
data-sort-long="{{ {'running': 1, 'failed': 2, 'aborted': 3, 'passed': 4, 'idle': 5}.get(long_state, 9) }}"
data-sort-burnin="{{ {'running': 1, 'queued': 2, 'failed': 3, 'unknown': 4, 'cancelled': 5, 'passed': 6}.get(burnin_state, 9) }}"
>
<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">
{%- if drive.pool_name -%}
<span class="pool-lock-icon{% if is_boot_pool %} pool-lock-boot{% elif is_exported %} pool-lock-exported{% elif is_mounted %} pool-lock-mounted{% endif %}"
title="{% if is_boot_pool %}In BOOT POOL '{{ drive.pool_name }}'{% elif is_exported %}Carries ZFS data from a previously-imported pool{% elif is_mounted %}Has a mounted (non-ZFS) filesystem{% else %}In pool '{{ drive.pool_name }}' ({{ drive.pool_role or 'data' }}){% endif %}">&#x1F512;</span>
{%- endif -%}
{{ drive.devname }}
</span>
<span class="drive-model">{{ drive.model or "Unknown" }}</span>
{%- if drive.pool_name %}
<span class="pool-pill{% if is_boot_pool %} pool-pill-boot{% elif is_exported %} pool-pill-exported{% elif is_mounted %} pool-pill-mounted{% endif %}"
title="Drive lock reason">{% if is_exported %}exported ZFS{% elif is_mounted %}mounted FS{% else %}{{ drive.pool_name }} &middot; {{ drive.pool_role or 'data' }}{% endif %}</span>
{%- endif %}
{%- 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 %}
{%- if pool_locked %}
<!-- Drive is in a zpool — replace Burn-In with Unlock affordance -->
<button class="btn-action btn-unlock{% if is_boot_pool %} btn-unlock-boot{% elif is_exported %} btn-unlock-exported{% elif is_mounted %} btn-unlock-mounted{% 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-pool-name="{{ drive.pool_name }}"
data-pool-role="{{ drive.pool_role or 'data' }}"
data-is-boot-pool="{{ '1' if is_boot_pool else '0' }}"
data-is-exported="{{ '1' if is_exported else '0' }}"
data-is-mounted="{{ '1' if is_mounted else '0' }}"
title="{% if is_boot_pool %}Drive is in BOOT POOL '{{ drive.pool_name }}' — click to unlock{% elif is_exported %}Drive carries ZFS data from a previously-imported pool — click to unlock{% elif is_mounted %}Drive has a mounted filesystem — click to unlock{% else %}Drive is in pool '{{ drive.pool_name }}' — click to unlock{% endif %}">&#x1F512; Unlock</button>
{%- else %}
<!-- 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 }}"
data-pool-name="{{ drive.pool_name or '' }}"
data-pool-unlocked-until="{{ drive.pool_unlocked_until or '' }}"
{% if short_busy or long_busy %}disabled{% endif %}
title="Start Burn-In{% if drive.pool_name %} (UNLOCKED — pool drive){% endif %}">Burn-In{% if drive.pool_name %} <span class="unlock-countdown" data-expires="{{ drive.pool_unlocked_until }}">&#x1F513;</span>{% endif %}</button>
<!-- Reset — clears SMART state so drive can be re-tested from scratch -->
{%- if can_reset %}
<button class="btn-action btn-reset"
data-drive-id="{{ drive.id }}"
title="Reset SMART state — clears test results so drive shows as fresh">Reset</button>
{%- endif %}
{%- endif %}
{%- 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>