From ee61eac66384233e10a0e60886eb202d9684ed7a Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 1 Jun 2026 19:18:30 +0200 Subject: [PATCH] fix(989): correct four live build-log viewer bugs in logs.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - live detection: use !h.status (null while running) not === 'running' - SSE: replace named addEventListener('chunk'/'done') with onmessage + frame.done check — backend sends generic message events, no named type - SSE payload: use frame.stdout_append/stderr_append not frame.text - timestamps: started_at/finished_at are unix seconds, multiply by 1000 was already done implicitly — actually use unix arithmetic directly to avoid new Date() confusion entirely Also move the misplaced .flow-main-slim rule from the logs section to the flow-shell section of dashboard.css. --- frontend/packages/dashboard/src/dashboard.css | 8 ++- frontend/packages/dashboard/src/logs.js | 62 ++++++++++++------- 2 files changed, 43 insertions(+), 27 deletions(-) diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css index 4ee4ba8d..f2019b52 100644 --- a/frontend/packages/dashboard/src/dashboard.css +++ b/frontend/packages/dashboard/src/dashboard.css @@ -1805,6 +1805,11 @@ body.flow-shell .tabbar .tab.active.tab-link { bottom: 0; overflow: hidden; } +/* flow.html now has a slim header (back link only) instead of the full + tabbar — reduce the top padding to match the smaller header height. */ +body.flow-shell .flow-main-slim { + padding-top: calc(var(--flow-header-h) + 0.4em); +} .flow-main .terminal-wrap { position: absolute; inset: 0; @@ -1864,9 +1869,6 @@ body.logs-shell { margin: 0; padding: 0; } -body.flow-shell .flow-main-slim { - padding-top: calc(var(--flow-header-h) + 0.4em); -} .logs-header { position: sticky; diff --git a/frontend/packages/dashboard/src/logs.js b/frontend/packages/dashboard/src/logs.js index 5d70b4ad..6a768661 100644 --- a/frontend/packages/dashboard/src/logs.js +++ b/frontend/packages/dashboard/src/logs.js @@ -40,13 +40,11 @@ import { // ─── helpers ────────────────────────────────────────────────────────── - function fmtTs(isoStr) { - if (!isoStr) return ''; - try { - const d = new Date(isoStr); - const age = Math.floor((Date.now() - d.getTime()) / 1000); - return fmtAgeSecs(age) + ' ago'; - } catch { return isoStr; } + // started_at / finished_at are unix seconds (i64), not milliseconds. + function fmtTs(unixSecs) { + if (!unixSecs) return ''; + const age = Math.floor(Date.now() / 1000) - unixSecs; + return fmtAgeSecs(Math.max(0, age)) + ' ago'; } function fmtDuration(secs) { @@ -76,10 +74,11 @@ import { const ul = el('ul', { class: 'build-logs-list' }); for (const h of rows) { const li = el('li', { class: 'build-logs-item' }); + // status is null while running, 'ok'/'fail' when finished. + const live = !h.status; const ok = h.status === 'ok'; - const live = h.status === 'running'; - const statusClass = ok ? 'build-logs-ok' : live ? 'build-logs-live-badge badge badge-running' : 'build-logs-fail'; - const statusLabel = ok ? '✓' : live ? 'live' : '✗'; + const statusClass = live ? 'badge badge-running' : ok ? 'badge badge-ok' : 'badge badge-fail'; + const statusLabel = live ? 'live' : ok ? 'ok' : 'fail'; const age = h.finished_at ? fmtTs(h.finished_at) : (live ? '' : fmtTs(h.started_at)); const runtime = h.runtime_secs != null ? el('span', { class: 'build-logs-runtime meta' }, fmtDuration(Math.max(0, h.runtime_secs))) @@ -125,20 +124,35 @@ import { const badge = el('span', { class: 'build-logs-live-badge badge badge-running' }, 'live'); detail.append(badge, pre); streamEs = new EventSource('/api/build-logs/id/' + h.id + '/stream'); - streamEs.addEventListener('chunk', (e) => { - try { pre.textContent += JSON.parse(e.data).text; } catch { /**/ } - }); - streamEs.addEventListener('done', () => { - badge.textContent = 'done'; - badge.className = 'build-logs-live-badge badge'; - streamEs.close(); streamEs = null; - detail.dataset.loaded = '1'; - }); - streamEs.addEventListener('error', () => { - badge.textContent = 'stream error'; - badge.className = 'build-logs-live-badge badge'; - streamEs.close(); streamEs = null; - }); + let stdoutLen = 0, stderrLen = 0; + streamEs.onmessage = (e) => { + let frame; + try { frame = JSON.parse(e.data); } catch { return; } + if (frame.stdout_append) { + pre.textContent += frame.stdout_append; + stdoutLen += frame.stdout_append.length; + } + if (frame.stderr_append) { + if (stderrLen === 0) pre.textContent += '\n--- stderr ---\n'; + pre.textContent += frame.stderr_append; + stderrLen += frame.stderr_append.length; + } + if (frame.done) { + badge.className = frame.status === 'ok' ? 'badge badge-ok' : 'badge badge-fail'; + badge.textContent = frame.status || 'done'; + streamEs.close(); streamEs = null; + detail.dataset.loaded = '1'; + } + }; + streamEs.onerror = () => { + if (streamEs && streamEs.readyState === EventSource.CONNECTING) { + pre.textContent = ''; stdoutLen = 0; stderrLen = 0; + } else { + badge.textContent = 'stream error'; + badge.className = 'badge badge-fail'; + if (streamEs) { streamEs.close(); streamEs = null; } + } + }; } else { const pre = el('pre', { class: 'build-logs-output' }, 'fetching…'); detail.append(pre);