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.
234 lines
13 KiB
HTML
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 %}">🔒</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 }} · {{ 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 %}">🔒 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 }}">🔓</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>
|