fix(989): correct four live build-log viewer bugs in logs.js

- 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.
This commit is contained in:
iris 2026-06-01 19:18:30 +02:00
commit ee61eac663
2 changed files with 43 additions and 27 deletions

View file

@ -1805,6 +1805,11 @@ body.flow-shell .tabbar .tab.active.tab-link {
bottom: 0; bottom: 0;
overflow: hidden; 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 { .flow-main .terminal-wrap {
position: absolute; position: absolute;
inset: 0; inset: 0;
@ -1864,9 +1869,6 @@ body.logs-shell {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
body.flow-shell .flow-main-slim {
padding-top: calc(var(--flow-header-h) + 0.4em);
}
.logs-header { .logs-header {
position: sticky; position: sticky;

View file

@ -40,13 +40,11 @@ import {
// ─── helpers ────────────────────────────────────────────────────────── // ─── helpers ──────────────────────────────────────────────────────────
function fmtTs(isoStr) { // started_at / finished_at are unix seconds (i64), not milliseconds.
if (!isoStr) return ''; function fmtTs(unixSecs) {
try { if (!unixSecs) return '';
const d = new Date(isoStr); const age = Math.floor(Date.now() / 1000) - unixSecs;
const age = Math.floor((Date.now() - d.getTime()) / 1000); return fmtAgeSecs(Math.max(0, age)) + ' ago';
return fmtAgeSecs(age) + ' ago';
} catch { return isoStr; }
} }
function fmtDuration(secs) { function fmtDuration(secs) {
@ -76,10 +74,11 @@ import {
const ul = el('ul', { class: 'build-logs-list' }); const ul = el('ul', { class: 'build-logs-list' });
for (const h of rows) { for (const h of rows) {
const li = el('li', { class: 'build-logs-item' }); 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 ok = h.status === 'ok';
const live = h.status === 'running'; const statusClass = live ? 'badge badge-running' : ok ? 'badge badge-ok' : 'badge badge-fail';
const statusClass = ok ? 'build-logs-ok' : live ? 'build-logs-live-badge badge badge-running' : 'build-logs-fail'; const statusLabel = live ? 'live' : ok ? 'ok' : 'fail';
const statusLabel = ok ? '✓' : live ? 'live' : '✗';
const age = h.finished_at ? fmtTs(h.finished_at) : (live ? '' : fmtTs(h.started_at)); const age = h.finished_at ? fmtTs(h.finished_at) : (live ? '' : fmtTs(h.started_at));
const runtime = h.runtime_secs != null const runtime = h.runtime_secs != null
? el('span', { class: 'build-logs-runtime meta' }, fmtDuration(Math.max(0, h.runtime_secs))) ? 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'); const badge = el('span', { class: 'build-logs-live-badge badge badge-running' }, 'live');
detail.append(badge, pre); detail.append(badge, pre);
streamEs = new EventSource('/api/build-logs/id/' + h.id + '/stream'); streamEs = new EventSource('/api/build-logs/id/' + h.id + '/stream');
streamEs.addEventListener('chunk', (e) => { let stdoutLen = 0, stderrLen = 0;
try { pre.textContent += JSON.parse(e.data).text; } catch { /**/ } streamEs.onmessage = (e) => {
}); let frame;
streamEs.addEventListener('done', () => { try { frame = JSON.parse(e.data); } catch { return; }
badge.textContent = 'done'; if (frame.stdout_append) {
badge.className = 'build-logs-live-badge badge'; pre.textContent += frame.stdout_append;
streamEs.close(); streamEs = null; stdoutLen += frame.stdout_append.length;
detail.dataset.loaded = '1'; }
}); if (frame.stderr_append) {
streamEs.addEventListener('error', () => { if (stderrLen === 0) pre.textContent += '\n--- stderr ---\n';
badge.textContent = 'stream error'; pre.textContent += frame.stderr_append;
badge.className = 'build-logs-live-badge badge'; stderrLen += frame.stderr_append.length;
streamEs.close(); streamEs = null; }
}); 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 { } else {
const pre = el('pre', { class: 'build-logs-output' }, 'fetching…'); const pre = el('pre', { class: 'build-logs-output' }, 'fetching…');
detail.append(pre); detail.append(pre);