feat: vital-signs strip above per-pattern meters (1.0.0-46)
The drawer's surface_validate area now leads with a row of operator vitals computed from data already in the response: - Temp: drive temperature with cool/warm/hot colour (≥48 red, ≥42 yellow) - Speed: live MB/s, NULL until second progress sample arrives - Elapsed: time since stage started_at - ETA: extrapolated from overall progress; suppressed under 0.5% to avoid the "47 days remaining" artefact early in pattern 1 Live MB/s comes from a new bb_mbps column on burnin_stages, computed in the badblocks parser as (delta_overall_pct / 800) * size_bytes / dt. Skipped on phase transitions (per-phase pct resets) and sub-second samples (noisy). Drawer endpoint now passes drive.temperature_c through; JS stashes the latest drive object in _DRAWER_LAST_DRIVE so the burn-in renderer can pull it for the vitals row without changing call signatures. Tightened table CSS in this same session is unrelated and shipped already in earlier rounds via the bind-mounted app.css.
This commit is contained in:
parent
1393ba0bc8
commit
6b2367b892
7 changed files with 217 additions and 36 deletions
|
|
@ -205,6 +205,22 @@ async def _update_stage_bb_phase(
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def _update_stage_bb_mbps(
|
||||||
|
job_id: int, stage_name: str, mbps: float,
|
||||||
|
) -> None:
|
||||||
|
"""Persist live throughput for the surface_validate meter strip.
|
||||||
|
Computed from delta_overall_pct between successive badblocks
|
||||||
|
progress lines, scaled by drive size_bytes / 800 (8 phases × 100)."""
|
||||||
|
async with _db() as db:
|
||||||
|
await db.execute("PRAGMA journal_mode=WAL")
|
||||||
|
await db.execute(
|
||||||
|
"UPDATE burnin_stages SET bb_mbps=? "
|
||||||
|
"WHERE burnin_job_id=? AND stage_name=?",
|
||||||
|
(mbps, job_id, stage_name),
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
|
||||||
async def _store_smart_attrs(drive_id: int, attrs: dict) -> None:
|
async def _store_smart_attrs(drive_id: int, attrs: dict) -> None:
|
||||||
"""Persist latest SMART attribute dict to drives.smart_attrs (JSON)."""
|
"""Persist latest SMART attribute dict to drives.smart_attrs (JSON)."""
|
||||||
# Convert int keys to str for JSON serialisation
|
# Convert int keys to str for JSON serialisation
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,7 @@ from ._common import (
|
||||||
_store_smart_attrs,
|
_store_smart_attrs,
|
||||||
_store_smart_raw_output,
|
_store_smart_raw_output,
|
||||||
_update_stage_bad_blocks,
|
_update_stage_bad_blocks,
|
||||||
|
_update_stage_bb_mbps,
|
||||||
_update_stage_bb_phase,
|
_update_stage_bb_phase,
|
||||||
_update_stage_percent,
|
_update_stage_percent,
|
||||||
)
|
)
|
||||||
|
|
@ -466,6 +467,17 @@ async def _stage_surface_validate_ssh(job_id: int, devname: str, drive_id: int)
|
||||||
"""Run badblocks over SSH, streaming output to stage log."""
|
"""Run badblocks over SSH, streaming output to stage log."""
|
||||||
from app import ssh_client
|
from app import ssh_client
|
||||||
|
|
||||||
|
# Pull drive size for the throughput calculation. Each badblocks
|
||||||
|
# phase covers the full disk once, so 1% overall progress = size/800
|
||||||
|
# bytes (8 phases × 100). NULL-safe: if size lookup fails we just
|
||||||
|
# skip the MB/s update.
|
||||||
|
drive_size_bytes: int | None = None
|
||||||
|
async with _db() as db:
|
||||||
|
cur = await db.execute("SELECT size_bytes FROM drives WHERE id=?", (drive_id,))
|
||||||
|
row = await cur.fetchone()
|
||||||
|
if row and row[0]:
|
||||||
|
drive_size_bytes = int(row[0])
|
||||||
|
|
||||||
await _append_stage_log(
|
await _append_stage_log(
|
||||||
job_id, "surface_validate",
|
job_id, "surface_validate",
|
||||||
f"[START] badblocks -wsv -b {settings.surface_validate_block_size} "
|
f"[START] badblocks -wsv -b {settings.surface_validate_block_size} "
|
||||||
|
|
@ -495,6 +507,12 @@ async def _stage_surface_validate_ssh(job_id: int, devname: str, drive_id: int)
|
||||||
pid_seen = False
|
pid_seen = False
|
||||||
progress = _BadblocksProgress()
|
progress = _BadblocksProgress()
|
||||||
|
|
||||||
|
# Throughput tracker — store (overall_pct, monotonic_ts)
|
||||||
|
# of the previous progress sample so we can compute MB/s
|
||||||
|
# from the delta on each new sample.
|
||||||
|
last_pct_sample: float = progress.overall_pct
|
||||||
|
last_pct_ts: float = time.monotonic()
|
||||||
|
|
||||||
# Seed bb_phase=1, bb_phase_pct=0 immediately so the
|
# Seed bb_phase=1, bb_phase_pct=0 immediately so the
|
||||||
# drawer's per-pattern meters have something to render
|
# drawer's per-pattern meters have something to render
|
||||||
# before badblocks emits its first "X% done" line. On a
|
# before badblocks emits its first "X% done" line. On a
|
||||||
|
|
@ -546,6 +564,28 @@ async def _stage_surface_validate_ssh(job_id: int, devname: str, drive_id: int)
|
||||||
progress.phase, progress.phase_pct,
|
progress.phase, progress.phase_pct,
|
||||||
)
|
)
|
||||||
await _update_stage_bad_blocks(job_id, "surface_validate", bad_blocks_total)
|
await _update_stage_bad_blocks(job_id, "surface_validate", bad_blocks_total)
|
||||||
|
# Throughput: only on real percent advances
|
||||||
|
# (skip header-only ticks where pct didn't move).
|
||||||
|
# Skip phase transitions too — the per-phase
|
||||||
|
# pct resets and would produce a negative delta.
|
||||||
|
if (
|
||||||
|
drive_size_bytes
|
||||||
|
and pct_changed
|
||||||
|
and not phase_changed
|
||||||
|
and progress.overall_pct > last_pct_sample
|
||||||
|
):
|
||||||
|
now_ts = time.monotonic()
|
||||||
|
dt = now_ts - last_pct_ts
|
||||||
|
if dt >= 1.0: # avoid noisy sub-second samples
|
||||||
|
d_pct = progress.overall_pct - last_pct_sample
|
||||||
|
# Each 1% overall = drive_size / 800 bytes
|
||||||
|
bytes_done = (d_pct / 800.0) * drive_size_bytes
|
||||||
|
mbps = bytes_done / dt / 1_000_000
|
||||||
|
await _update_stage_bb_mbps(
|
||||||
|
job_id, "surface_validate", mbps,
|
||||||
|
)
|
||||||
|
last_pct_sample = progress.overall_pct
|
||||||
|
last_pct_ts = now_ts
|
||||||
await _recalculate_progress(job_id)
|
await _recalculate_progress(job_id)
|
||||||
_push_update()
|
_push_update()
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ class Settings(BaseSettings):
|
||||||
ssh_key: str = "" # PEM private key content (paste full key including headers)
|
ssh_key: str = "" # PEM private key content (paste full key including headers)
|
||||||
|
|
||||||
# Application version — used by the /api/v1/updates/check endpoint
|
# Application version — used by the /api/v1/updates/check endpoint
|
||||||
app_version: str = "1.0.0-45"
|
app_version: str = "1.0.0-46"
|
||||||
|
|
||||||
# ---- Authentication (1.0.0-22) ----
|
# ---- Authentication (1.0.0-22) ----
|
||||||
# session_secret: HMAC key for signing session cookies. Empty = generate
|
# session_secret: HMAC key for signing session cookies. Empty = generate
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,12 @@ _MIGRATIONS = [
|
||||||
# 8=verify 0x00). bb_phase_pct is 0-100 within the current phase.
|
# 8=verify 0x00). bb_phase_pct is 0-100 within the current phase.
|
||||||
"ALTER TABLE burnin_stages ADD COLUMN bb_phase INTEGER",
|
"ALTER TABLE burnin_stages ADD COLUMN bb_phase INTEGER",
|
||||||
"ALTER TABLE burnin_stages ADD COLUMN bb_phase_pct REAL",
|
"ALTER TABLE burnin_stages ADD COLUMN bb_phase_pct REAL",
|
||||||
|
# 1.0.0-46: live write/read throughput for the per-pattern meters.
|
||||||
|
# Computed from successive `XX% done` lines in badblocks output:
|
||||||
|
# delta_bytes = (overall_pct_delta / 800) * drive_size_bytes.
|
||||||
|
# Updated on every progress line; NULL until the second progress
|
||||||
|
# line arrives (need two samples to compute a rate).
|
||||||
|
"ALTER TABLE burnin_stages ADD COLUMN bb_mbps REAL",
|
||||||
# 1.0.0-19: enforce one active burn-in per drive at the storage layer.
|
# 1.0.0-19: enforce one active burn-in per drive at the storage layer.
|
||||||
# Closes the read-then-insert race in burnin.start_job — without this,
|
# Closes the read-then-insert race in burnin.start_job — without this,
|
||||||
# two concurrent /api/v1/burnin/start requests for the same drive could
|
# two concurrent /api/v1/burnin/start requests for the same drive could
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ async def drive_drawer(drive_id: int, db: aiosqlite.Connection = Depends(get_db)
|
||||||
cur = await db.execute(
|
cur = await db.execute(
|
||||||
"SELECT id, stage_name, state, percent, started_at, finished_at, "
|
"SELECT id, stage_name, state, percent, started_at, finished_at, "
|
||||||
"duration_seconds, error_text, log_text, bad_blocks, "
|
"duration_seconds, error_text, log_text, bad_blocks, "
|
||||||
"bb_phase, bb_phase_pct "
|
"bb_phase, bb_phase_pct, bb_mbps "
|
||||||
"FROM burnin_stages WHERE burnin_job_id=? ORDER BY id",
|
"FROM burnin_stages WHERE burnin_job_id=? ORDER BY id",
|
||||||
(job_row["id"],),
|
(job_row["id"],),
|
||||||
)
|
)
|
||||||
|
|
@ -102,11 +102,12 @@ async def drive_drawer(drive_id: int, db: aiosqlite.Connection = Depends(get_db)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"drive": {
|
"drive": {
|
||||||
"id": drive.id,
|
"id": drive.id,
|
||||||
"devname": drive.devname,
|
"devname": drive.devname,
|
||||||
"serial": drive.serial,
|
"serial": drive.serial,
|
||||||
"model": drive.model,
|
"model": drive.model,
|
||||||
"size_bytes": drive.size_bytes,
|
"size_bytes": drive.size_bytes,
|
||||||
|
"temperature_c": drive.temperature_c,
|
||||||
},
|
},
|
||||||
"burnin": burnin_job,
|
"burnin": burnin_job,
|
||||||
"smart": {
|
"smart": {
|
||||||
|
|
|
||||||
|
|
@ -244,7 +244,7 @@ thead {
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
padding: 9px 14px;
|
padding: 6px 8px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|
@ -256,9 +256,10 @@ th {
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
padding: 10px 14px;
|
padding: 7px 8px;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr:last-child td {
|
tr:last-child td {
|
||||||
|
|
@ -276,17 +277,15 @@ tr:hover td {
|
||||||
/* -----------------------------------------------------------------------
|
/* -----------------------------------------------------------------------
|
||||||
Column widths
|
Column widths
|
||||||
----------------------------------------------------------------------- */
|
----------------------------------------------------------------------- */
|
||||||
.col-drive { min-width: 180px; }
|
.col-drive { min-width: 160px; }
|
||||||
.col-serial { min-width: 110px; }
|
.col-serial { min-width: 95px; }
|
||||||
.col-size { min-width: 70px; text-align: right; }
|
.col-size { min-width: 60px; text-align: right; }
|
||||||
.col-temp { min-width: 75px; text-align: right; }
|
.col-temp { min-width: 60px; text-align: right; }
|
||||||
.col-health { min-width: 85px; }
|
.col-health { min-width: 70px; }
|
||||||
.col-smart { min-width: 95px; }
|
.col-smart { min-width: 80px; }
|
||||||
/* Tighter horizontal padding on the SMART columns — they hold short
|
/* Tighter SMART columns — they hold short pills or a progress bar. */
|
||||||
pills ("Passed"/"—") or a progress bar, so the default 14px gutter
|
th.col-smart, td.col-smart { padding-left: 5px; padding-right: 5px; }
|
||||||
wastes space on 13" laptops. */
|
.col-actions { min-width: 150px; }
|
||||||
th.col-smart, td.col-smart { padding-left: 6px; padding-right: 6px; }
|
|
||||||
.col-actions { min-width: 170px; }
|
|
||||||
|
|
||||||
/* -----------------------------------------------------------------------
|
/* -----------------------------------------------------------------------
|
||||||
Drive cell
|
Drive cell
|
||||||
|
|
@ -295,14 +294,23 @@ th.col-smart, td.col-smart { padding-left: 6px; padding-right: 6px; }
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--text-strong);
|
color: var(--text-strong);
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drive-model {
|
.drive-model {
|
||||||
display: block;
|
display: inline;
|
||||||
font-size: 11px;
|
font-size: 10px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-top: 1px;
|
margin-top: 0;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
/* Separator between model and location when both are present on the
|
||||||
|
same line. ::after on .drive-model puts a thin dot between them. */
|
||||||
|
.drive-model + .drive-location::before {
|
||||||
|
content: " · ";
|
||||||
|
color: var(--border);
|
||||||
|
margin: 0 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -----------------------------------------------------------------------
|
/* -----------------------------------------------------------------------
|
||||||
|
|
@ -425,7 +433,7 @@ th.col-smart, td.col-smart { padding-left: 6px; padding-right: 6px; }
|
||||||
/* -----------------------------------------------------------------------
|
/* -----------------------------------------------------------------------
|
||||||
Burn-in column
|
Burn-in column
|
||||||
----------------------------------------------------------------------- */
|
----------------------------------------------------------------------- */
|
||||||
.col-burnin { min-width: 160px; }
|
.col-burnin { min-width: 130px; }
|
||||||
|
|
||||||
.burnin-cell { min-width: 140px; }
|
.burnin-cell { min-width: 140px; }
|
||||||
|
|
||||||
|
|
@ -1180,9 +1188,9 @@ a.stat-card:hover {
|
||||||
Checkbox column
|
Checkbox column
|
||||||
----------------------------------------------------------------------- */
|
----------------------------------------------------------------------- */
|
||||||
.col-check {
|
.col-check {
|
||||||
width: 36px;
|
width: 32px;
|
||||||
min-width: 36px;
|
min-width: 32px;
|
||||||
padding: 10px 8px 10px 14px;
|
padding: 7px 4px 7px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drive-checkbox, #select-all-cb {
|
.drive-checkbox, #select-all-cb {
|
||||||
|
|
@ -1196,18 +1204,15 @@ a.stat-card:hover {
|
||||||
Drive location inline edit
|
Drive location inline edit
|
||||||
----------------------------------------------------------------------- */
|
----------------------------------------------------------------------- */
|
||||||
.drive-location {
|
.drive-location {
|
||||||
display: block;
|
display: inline;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-top: 2px;
|
margin-top: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 1px 3px;
|
padding: 0 3px;
|
||||||
|
line-height: 1.1;
|
||||||
transition: background 0.1s;
|
transition: background 0.1s;
|
||||||
max-width: 160px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
.drive-location:hover { background: var(--border); color: var(--text); }
|
.drive-location:hover { background: var(--border); color: var(--text); }
|
||||||
|
|
||||||
|
|
@ -2771,3 +2776,44 @@ tr.drawer-row-active {
|
||||||
}
|
}
|
||||||
.bb-sub-write { color: color-mix(in srgb, var(--blue) 80%, var(--text-muted)); }
|
.bb-sub-write { color: color-mix(in srgb, var(--blue) 80%, var(--text-muted)); }
|
||||||
.bb-sub-verify { color: color-mix(in srgb, var(--green) 80%, var(--text-muted)); }
|
.bb-sub-verify { color: color-mix(in srgb, var(--green) 80%, var(--text-muted)); }
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------
|
||||||
|
Surface-scan vital-signs row in the drawer (1.0.0-46).
|
||||||
|
Sits directly above the per-pattern meters. Temperature with
|
||||||
|
green/yellow/red colour, live MB/s, elapsed, ETA — all derived
|
||||||
|
from data already in the drawer payload.
|
||||||
|
----------------------------------------------------------------------- */
|
||||||
|
.bb-vitals {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 8px 12px 4px 12px;
|
||||||
|
background: var(--bg-soft, #161b22);
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
|
margin: 6px 0 0 0;
|
||||||
|
border-bottom: 1px solid var(--border, #30363d);
|
||||||
|
}
|
||||||
|
/* When vitals lead, suppress the meter strip's top radius + margin so
|
||||||
|
they read as one stacked unit. */
|
||||||
|
.bb-vitals + .bb-meters {
|
||||||
|
border-radius: 0 0 6px 6px;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.bb-vital {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
font-family: "SF Mono", "Consolas", monospace;
|
||||||
|
}
|
||||||
|
.bb-vital-label {
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .04em;
|
||||||
|
}
|
||||||
|
.bb-vital-value {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-strong, #f0f6fc);
|
||||||
|
font-weight: 500;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1271,8 +1271,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stash the last drive object so the burn-in panel renderer can
|
||||||
|
// pull temperature_c into the vital-signs row without having to
|
||||||
|
// pass it through the Burn-In renderer's signature.
|
||||||
|
var _DRAWER_LAST_DRIVE = null;
|
||||||
|
|
||||||
function _drawerRender(data) {
|
function _drawerRender(data) {
|
||||||
var drive = data.drive || {};
|
var drive = data.drive || {};
|
||||||
|
_DRAWER_LAST_DRIVE = drive;
|
||||||
var devnameEl = document.getElementById('drawer-devname');
|
var devnameEl = document.getElementById('drawer-devname');
|
||||||
var metaEl = document.getElementById('drawer-drive-meta');
|
var metaEl = document.getElementById('drawer-drive-meta');
|
||||||
if (devnameEl) devnameEl.textContent = drive.devname || '\u2014';
|
if (devnameEl) devnameEl.textContent = drive.devname || '\u2014';
|
||||||
|
|
@ -1286,6 +1292,70 @@
|
||||||
_drawerRenderEvents(data.events);
|
_drawerRenderEvents(data.events);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vital-signs row above the meters: drive temp, live throughput,
|
||||||
|
// elapsed time, ETA. Computed from data already in the drawer payload.
|
||||||
|
function _drawerRenderBadblocksVitals(stage, drive) {
|
||||||
|
var phase = parseInt(stage.bb_phase, 10) || 1;
|
||||||
|
var phasePct = parseFloat(stage.bb_phase_pct || 0);
|
||||||
|
var overallPct = ((phase - 1) * 100 + phasePct) / 8; // 0..100
|
||||||
|
var html = '<div class="bb-vitals">';
|
||||||
|
|
||||||
|
// Temperature with hot/warm/cool colour
|
||||||
|
if (drive && typeof drive.temperature_c === 'number') {
|
||||||
|
var tc = drive.temperature_c;
|
||||||
|
var tClass = 'temp-cool';
|
||||||
|
if (tc >= 48) tClass = 'temp-hot';
|
||||||
|
else if (tc >= 42) tClass = 'temp-warm';
|
||||||
|
html += '<div class="bb-vital">';
|
||||||
|
html += '<span class="bb-vital-label">Temp</span>';
|
||||||
|
html += '<span class="bb-vital-value temp ' + tClass + '">' + tc + '°C</span>';
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live throughput in MB/s, NULL until second progress sample arrives
|
||||||
|
if (typeof stage.bb_mbps === 'number' && stage.bb_mbps > 0) {
|
||||||
|
html += '<div class="bb-vital">';
|
||||||
|
html += '<span class="bb-vital-label">Speed</span>';
|
||||||
|
html += '<span class="bb-vital-value">' + stage.bb_mbps.toFixed(0) + ' MB/s</span>';
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Elapsed since stage started
|
||||||
|
if (stage.started_at) {
|
||||||
|
var startMs = Date.parse(stage.started_at);
|
||||||
|
var elapsedSec = Math.max(0, (Date.now() - startMs) / 1000);
|
||||||
|
html += '<div class="bb-vital">';
|
||||||
|
html += '<span class="bb-vital-label">Elapsed</span>';
|
||||||
|
html += '<span class="bb-vital-value">' + _bbFmtDuration(elapsedSec) + '</span>';
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// ETA = (elapsed / overallPct) * 100 - elapsed; only if we have
|
||||||
|
// measurable progress (>= 0.5% so the first few samples don't
|
||||||
|
// produce a "47 days" early estimate that scares the operator).
|
||||||
|
if (overallPct >= 0.5) {
|
||||||
|
var totalSec = elapsedSec * (100 / overallPct);
|
||||||
|
var remainingSec = Math.max(0, totalSec - elapsedSec);
|
||||||
|
html += '<div class="bb-vital">';
|
||||||
|
html += '<span class="bb-vital-label">ETA</span>';
|
||||||
|
html += '<span class="bb-vital-value">' + _bbFmtDuration(remainingSec) + '</span>';
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _bbFmtDuration(sec) {
|
||||||
|
sec = Math.floor(sec);
|
||||||
|
var d = Math.floor(sec / 86400);
|
||||||
|
var h = Math.floor((sec % 86400) / 3600);
|
||||||
|
var m = Math.floor((sec % 3600) / 60);
|
||||||
|
if (d > 0) return d + 'd ' + h + 'h';
|
||||||
|
if (h > 0) return h + 'h ' + m + 'm';
|
||||||
|
return m + 'm';
|
||||||
|
}
|
||||||
|
|
||||||
// Render 4 pattern meters for badblocks -w surface_validate. Each
|
// Render 4 pattern meters for badblocks -w surface_validate. Each
|
||||||
// meter splits write/verify halves so you can see at a glance which
|
// meter splits write/verify halves so you can see at a glance which
|
||||||
// pattern is current AND whether you're writing or verifying within
|
// pattern is current AND whether you're writing or verifying within
|
||||||
|
|
@ -1366,8 +1436,10 @@
|
||||||
if (s.error_text) {
|
if (s.error_text) {
|
||||||
html += '<div class="stage-error-line">' + _esc(s.error_text) + '</div>';
|
html += '<div class="stage-error-line">' + _esc(s.error_text) + '</div>';
|
||||||
}
|
}
|
||||||
// Per-pattern meters for badblocks surface_validate
|
// Per-pattern meters for badblocks surface_validate, plus the
|
||||||
|
// vital-signs row above (temp / speed / elapsed / ETA).
|
||||||
if (s.stage_name === 'surface_validate' && s.bb_phase) {
|
if (s.stage_name === 'surface_validate' && s.bb_phase) {
|
||||||
|
html += _drawerRenderBadblocksVitals(s, _DRAWER_LAST_DRIVE);
|
||||||
html += _drawerRenderBadblocksMeters(s.bb_phase, s.bb_phase_pct);
|
html += _drawerRenderBadblocksMeters(s.bb_phase, s.bb_phase_pct);
|
||||||
}
|
}
|
||||||
// Raw SSH log output (if available)
|
// Raw SSH log output (if available)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue