Skip to content

Commit 09774a4

Browse files
fix(live): /live reacts to stop/done — freeze ETA + status, drop stuck "running"
Root cause (client-side): the server already forwards status:'stopped'/'done' to /live (PUBLIC_EVENT_WHITELIST has 'state', PUBLIC_STATE_KEYS has 'status'), but live.html's `case 'state'` merged the status without reacting — so the log-header label stayed "running" and the per-second ETA ticker (gated only on _cb.startedAt, never cleared) climbed forever, making /live look perpetually live after Stop. Fix (reviewed — proper root-cause, gated on the real server status, no resume regression since Resume re-emits running + a fresh batch:start that re-arms the ETA): - isRunLive() ticks the ETA ONLY while status==='running'. Paused states (paused_balance/paused_internet) and terminal states freeze it. - cbFreeze(status) on terminal status: clears startedAt + gap timer, drops the pulse, sets the real label (stopped/complete/error/idle), resolves ETA to 00:00:00 / —. - `case 'state'`: terminal → cbFreeze; running → setStatus; paused* → show a friendly 'paused' label without freezing startedAt. Also fixes the status-vocabulary drift the reviewer flagged (same class fixed in the server guards/types earlier): match the real paused_balance/paused_internet strings instead of the dead bare 'paused'. /live stays spectator-only; broadcast- off behavior unchanged. Test suite green (188/31/45/35).
1 parent 0e04a3e commit 09774a4

1 file changed

Lines changed: 55 additions & 0 deletions

File tree

live.html

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1562,6 +1562,29 @@ <h1 class="live-page-title">
15621562
cbRender();
15631563
}
15641564

1565+
// Halt the live-progress UI when the run ends (stop / done / error / idle).
1566+
// Called from the `state` event handler for direct /api/start runs, which
1567+
// never emit `loop:stopped`. Freezing `_cb.startedAt` stops the per-second
1568+
// ETA ticker from climbing; clearing the gap timer stops any countdown;
1569+
// the dot drops its "running" pulse and the log-header label updates.
1570+
function cbFreeze(status) {
1571+
// Stop the ETA ticker — without clearing startedAt the 1s timer keeps
1572+
// recomputing elapsed/tested and the ETA marches upward forever.
1573+
_cb.startedAt = null;
1574+
if (_cb.gapTimer) { clearInterval(_cb.gapTimer); _cb.gapTimer = null; }
1575+
_cb.gapEndsAt = null;
1576+
const dot = _cbEl('cbDot');
1577+
if (dot) dot.className = 'pulse-dot';
1578+
const hdrDot = document.getElementById('headerDot');
1579+
if (hdrDot) hdrDot.className = 'pulse-dot';
1580+
// Log-panel header label: 'stopped' / 'complete' / 'error' / 'idle'.
1581+
setStatus(status === 'done' ? 'complete' : status);
1582+
// cbRender recomputes the ETA: with startedAt now null it resolves to
1583+
// "ETA 00:00:00" (all tested) or "ETA —" (stopped mid-run), never a
1584+
// climbing number.
1585+
cbRender();
1586+
}
1587+
15651588
function cbApplyGap(data) {
15661589
data = data || {};
15671590
const gapMs = data.next_in_ms || data.gapMs || 30000;
@@ -1752,10 +1775,26 @@ <h1 class="live-page-title">
17521775
applyHeaderStatsFromState();
17531776
}, 5000);
17541777

1778+
// True only while a run is genuinely in flight. A stopped/done/error/idle
1779+
// status from the server means the run ended — the ETA must freeze and the
1780+
// status label must drop out of "running". Without this gate the ETA timer
1781+
// keeps ticking off `_cb.startedAt` (which is never cleared on stop) so the
1782+
// countdown climbs forever and /live looks like the test is still running.
1783+
function isRunLive() {
1784+
// The ETA ticks ONLY while the run is actively testing. Paused states
1785+
// (the pipeline emits paused_balance / paused_internet — never bare
1786+
// 'paused') freeze the ETA so a long balance/internet pause doesn't inflate
1787+
// the projection; terminal states (stopped/done/error/idle) freeze it too.
1788+
// Resume re-emits status='running' + a fresh batch:start, re-arming the ETA.
1789+
return ((_liveState && _liveState.status) || '') === 'running';
1790+
}
1791+
17551792
// Tick ETA every second so the countdown reads smoothly between
17561793
// node:result events (which can be 10–60s apart on slow nodes).
17571794
setInterval(() => {
17581795
if (!_cb.startedAt) return;
1796+
// Freeze the ETA the moment the run is no longer live (stopped/done/etc).
1797+
if (!isRunLive()) return;
17591798
const etaEl = document.getElementById('cbEta');
17601799
if (!etaEl) return;
17611800
const snap = _cb.snapshotSize;
@@ -2257,6 +2296,22 @@ <h1 class="live-page-title">
22572296
_prevActiveRunNumber = newRunNum;
22582297
_prevStatus = newStatus;
22592298
mergeLiveState(d.state);
2299+
// Direct /api/start runs (p2p / sub-plan / test-run) signal stop and
2300+
// natural finish via this `state` event — they do NOT emit the
2301+
// `loop:stopped` event the continuous runner uses. So the terminal
2302+
// transition must be handled here too, or /live stays stuck on
2303+
// "running" with a climbing ETA after the admin presses Stop.
2304+
if (newStatus === 'stopped' || newStatus === 'done' ||
2305+
newStatus === 'error' || newStatus === 'idle') {
2306+
cbFreeze(newStatus);
2307+
} else if (newStatus === 'running') {
2308+
setStatus(newStatus);
2309+
} else if (newStatus && newStatus.indexOf('paused') === 0) {
2310+
// paused_balance / paused_internet — show a friendly 'paused' label.
2311+
// Not terminal: don't cbFreeze (keep startedAt so the resumed run's
2312+
// ETA continues); the ETA is frozen meanwhile via isRunLive().
2313+
setStatus('paused');
2314+
}
22602315
renderLiveStats();
22612316
if (_cb.batchId) cbRender();
22622317
}

0 commit comments

Comments
 (0)