dashboard: build-log side-panel viewer in agent card (#726 phase 3)
Adds a `↳ build logs · {agent}` drill-in to every agent card that
opens the side panel and fetches the last 10 build-log headers from
GET /api/build-logs/{agent}. Each row shows a status chip (ok/fail/
running), kind, age, and truncated cmdline. Clicking a row lazily
fetches the full stdout+stderr from GET /api/build-logs/id/{id} and
expands it inline as a scrollable pre.
CSS additions: .build-logs-{panel,toolbar,list,item,row-btn,...} plus
.badge-ok/.badge-fail/.badge-running status chips shared with future
uses. docs/web-ui.md updated with the new drill-in and the new badge
classes.
This commit is contained in:
parent
65f385a7a0
commit
cc6227ccb5
4 changed files with 1517 additions and 33 deletions
|
|
@ -709,6 +709,10 @@ window.marked = marked;
|
|||
// narrows to the harness service (or empty = full machine).
|
||||
const journalUnit = c.is_manager ? 'hive-m1nd.service' : 'hive-ag3nt.service';
|
||||
drill.append(buildJournalTrigger(c.container, journalUnit));
|
||||
// Build-log viewer: lists recent nix build / nixos-container
|
||||
// invocations for this agent, with click-to-expand full
|
||||
// stdout + stderr. Backed by GET /api/build-logs/{name}.
|
||||
drill.append(buildBuildLogsTrigger(c.name));
|
||||
// The hardcoded config-repo trigger and the agent-declared
|
||||
// extras block both moved into the unified nav strip in the
|
||||
// head row above (sourced from the agent backend via
|
||||
|
|
@ -1068,6 +1072,110 @@ window.marked = marked;
|
|||
return trigger;
|
||||
}
|
||||
|
||||
// Build-log viewer. Fetches the last 10 build-log headers for
|
||||
// `agentName` from GET /api/build-logs/{agentName}, then renders
|
||||
// a clickable list. Clicking a row fetches its full stdout+stderr
|
||||
// from GET /api/build-logs/id/{id} and expands it inline.
|
||||
function buildBuildLogsTrigger(agentName) {
|
||||
const trigger = el('button', { type: 'button', class: 'panel-trigger' },
|
||||
'↳ build logs · ' + agentName);
|
||||
trigger.addEventListener('click', () => {
|
||||
const panelTitle = 'build logs · ' + agentName;
|
||||
const wrap = el('div', { class: 'build-logs-panel' });
|
||||
|
||||
const hdr = el('div', { class: 'build-logs-toolbar' });
|
||||
const refreshBtn = el('button',
|
||||
{ type: 'button', class: 'btn btn-restart build-logs-refresh' },
|
||||
'↻ refresh');
|
||||
hdr.append(refreshBtn);
|
||||
wrap.append(hdr);
|
||||
|
||||
const list = el('ul', { class: 'build-logs-list' });
|
||||
wrap.append(list);
|
||||
|
||||
let fetching = false;
|
||||
|
||||
async function loadHeaders() {
|
||||
if (fetching) return;
|
||||
fetching = true;
|
||||
list.innerHTML = '';
|
||||
list.append(el('li', { class: 'build-logs-loading' }, 'fetching…'));
|
||||
try {
|
||||
const resp = await fetch('/api/build-logs/' + agentName + '?limit=10');
|
||||
if (!resp.ok) {
|
||||
list.innerHTML = '';
|
||||
list.append(el('li', { class: 'build-logs-error' },
|
||||
'error ' + resp.status + ': ' + await resp.text()));
|
||||
return;
|
||||
}
|
||||
const rows = await resp.json();
|
||||
list.innerHTML = '';
|
||||
if (!rows || !rows.length) {
|
||||
list.append(el('li', { class: 'build-logs-empty' }, '(no build logs yet)'));
|
||||
return;
|
||||
}
|
||||
const nowUnix = Math.floor(Date.now() / 1000);
|
||||
for (const h of rows) {
|
||||
const li = el('li', { class: 'build-logs-item' });
|
||||
const statusCls = h.status === 'ok' ? 'badge-ok'
|
||||
: h.status === 'fail' ? 'badge-fail'
|
||||
: 'badge-running';
|
||||
const statusLabel = h.status === 'ok' ? 'ok'
|
||||
: h.status === 'fail' ? 'fail'
|
||||
: 'running';
|
||||
const age = h.started_at
|
||||
? fmtAgeSecs(nowUnix - h.started_at) + ' ago' : '?';
|
||||
const rowBtn = el('button',
|
||||
{ type: 'button', class: 'build-logs-row-btn' },
|
||||
el('span', { class: `badge ${statusCls}` }, statusLabel),
|
||||
el('span', { class: 'build-logs-kind' }, h.kind),
|
||||
el('span', { class: 'build-logs-age meta' }, age),
|
||||
el('span', { class: 'build-logs-cmdline meta' }, h.cmdline),
|
||||
);
|
||||
const detail = el('div', { class: 'build-logs-detail' });
|
||||
detail.hidden = true;
|
||||
let loaded = false;
|
||||
rowBtn.addEventListener('click', async () => {
|
||||
if (!detail.hidden) { detail.hidden = true; return; }
|
||||
if (!loaded) {
|
||||
detail.textContent = 'fetching…';
|
||||
detail.hidden = false;
|
||||
try {
|
||||
const r2 = await fetch('/api/build-logs/id/' + h.id);
|
||||
if (!r2.ok) {
|
||||
detail.textContent = 'error ' + r2.status + ': ' + await r2.text();
|
||||
} else {
|
||||
const full = await r2.json();
|
||||
const out = (full.stdout || '') + (full.stderr ? '\n--- stderr ---\n' + full.stderr : '');
|
||||
const pre = el('pre', { class: 'build-logs-output' }, out || '(empty)');
|
||||
detail.replaceChildren(pre);
|
||||
loaded = true;
|
||||
}
|
||||
} catch (err) {
|
||||
detail.textContent = 'fetch failed: ' + err;
|
||||
}
|
||||
} else {
|
||||
detail.hidden = false;
|
||||
}
|
||||
});
|
||||
li.append(rowBtn, detail);
|
||||
list.append(li);
|
||||
}
|
||||
} catch (err) {
|
||||
list.innerHTML = '';
|
||||
list.append(el('li', { class: 'build-logs-error' }, 'fetch failed: ' + err));
|
||||
} finally {
|
||||
fetching = false;
|
||||
}
|
||||
}
|
||||
|
||||
refreshBtn.addEventListener('click', loadHeaders);
|
||||
Panel.open(panelTitle, wrap);
|
||||
loadHeaders();
|
||||
});
|
||||
return trigger;
|
||||
}
|
||||
|
||||
function renderTombstones(s) {
|
||||
const root = $('tombstones-section');
|
||||
// #tombstones-section only lives on /index.html (SYST3M tab);
|
||||
|
|
|
|||
Loading…
Reference in a new issue