diff --git a/frontend/packages/dashboard/build.mjs b/frontend/packages/dashboard/build.mjs index 6dc72e37..551360d6 100644 --- a/frontend/packages/dashboard/build.mjs +++ b/frontend/packages/dashboard/build.mjs @@ -5,29 +5,25 @@ // dist/dashboard.html the operator dashboard SPA — served // at GET /dashboard.html // dist/flow.html served at GET /flow.html -// dist/logs.html served at GET /logs.html (AGENT/SYSTEM/AUDIT) -// dist/core.html served at GET /core.html (C0R3: kept -// state / container load) -// dist/builds.html served at GET /builds.html (BU1LDS: -// rebuild queue / meta inputs / build logs) +// dist/logs.html served at GET /logs.html +// dist/core.html served at GET /core.html (C0R3: rebuild +// queue / meta inputs / kept state / +// container load — the old SYST3M tab) // dist/static/home.js index.html (H0M3) entry — menu tiles + // matrix-tile gating + identity line // dist/static/tabs.js dashboard.html entry — tab renderers + // tab routing + refreshState // dist/static/flow.js /flow.html entry — broker terminal + // @-mention composer -// dist/static/logs.js /logs.html entry — agent/system/audit +// dist/static/logs.js /logs.html entry — build/agent/system // log viewer sub-tabs -// dist/static/builds.js /builds.html entry — rebuild queue, -// meta inputs, build log history -// dist/static/{home,tabs,flow,logs,builds}.js.map source map siblings +// dist/static/{home,tabs,flow,logs}.js.map source map siblings // dist/static/common.css loaded by every page (@hive/shared // imports + shared typography/badges/ // buttons/inbox/side-panel) // dist/static/dashboard.css dashboard.html only (dashboard-specific) // dist/static/flow.css /flow.html only (flow chrome + composer) // dist/static/logs.css /logs.html only (log viewer sub-tabs) -// dist/static/builds.css /builds.html only (build lifecycle hub) // dist/static/home.css index.html (H0M3) only (tile grid) // // Each JS entry bundles `./common.js` (DOM helpers, Panel singleton, @@ -59,7 +55,7 @@ mkdirSync(staticDir(''), { recursive: true }); // follow-up once asset sizes warrant it). esbuild writes each entry // to `static/.js` based on the entryPoint basename. await build({ - entryPoints: [src('tabs.js'), src('flow.js'), src('logs.js'), src('home.js'), src('settings.js'), src('stats.js'), src('core.js'), src('builds.js'), src('matrix-accounts.js')], + entryPoints: [src('tabs.js'), src('flow.js'), src('logs.js'), src('home.js'), src('settings.js'), src('stats.js'), src('core.js'), src('matrix-accounts.js')], outdir: staticDir(''), bundle: true, format: 'esm', @@ -98,7 +94,7 @@ await build({ // so a swap replaces only it) + theme.css (the semantic derivation // layer) + common.css (shared typography, badges, buttons, inbox, side // panel) plus its own page-specific bundle. -for (const entry of ['colors.css', 'theme.css', 'common.css', 'dashboard.css', 'flow.css', 'logs.css', 'home.css', 'settings.css', 'stats.css', 'core.css', 'builds.css', 'matrix-accounts.css']) { +for (const entry of ['colors.css', 'theme.css', 'common.css', 'dashboard.css', 'flow.css', 'logs.css', 'home.css', 'settings.css', 'stats.css', 'core.css', 'matrix-accounts.css']) { await build({ entryPoints: [src(entry)], outfile: staticDir(entry), @@ -108,7 +104,7 @@ for (const entry of ['colors.css', 'theme.css', 'common.css', 'dashboard.css', ' }); } -for (const html of ['index.html', 'dashboard.html', 'flow.html', 'logs.html', 'settings.html', 'stats.html', 'core.html', 'builds.html', 'matrix-accounts.html']) { +for (const html of ['index.html', 'dashboard.html', 'flow.html', 'logs.html', 'settings.html', 'stats.html', 'core.html', 'matrix-accounts.html']) { copyFileSync(src(html), dist(html)); } diff --git a/frontend/packages/dashboard/src/builds.css b/frontend/packages/dashboard/src/builds.css deleted file mode 100644 index e06f16e1..00000000 --- a/frontend/packages/dashboard/src/builds.css +++ /dev/null @@ -1,26 +0,0 @@ -/* ─── /builds.html — BU1LDS page ──────────────────────────────────── - The build lifecycle hub: rebuild queue + live log, meta inputs, and - build log history on one page. Carved out of /core.html (rebuild - queue + meta inputs) and /logs.html (BUILD tab). - - Imports system-sections.css for rebuild queue + meta inputs + live-log - component styles, and logs.css for the build-logs-* component styles. - Body-scoped `.logs-shell` rules in logs.css don't apply here - (`.builds-shell` body); only the build-logs-* component rules are used. */ -@import "./system-sections.css"; -@import "./logs.css"; - -body.builds-shell { - margin: 0; - padding: 0; -} - -.builds-tabbar { - flex: 1; -} - -.builds-main { - padding: 1.2em 1.5em 2em; -} - -.builds-pane[hidden] { display: none; } diff --git a/frontend/packages/dashboard/src/builds.html b/frontend/packages/dashboard/src/builds.html deleted file mode 100644 index c0ad63ed..00000000 --- a/frontend/packages/dashboard/src/builds.html +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - hyperhive // BU1LDS - - - - - - - - - - - -
- - -
-

pending + running rebuilds, meta-updates, and first-spawns. one runs at a time; meta-update cascades nest under their parent. dedup: re-enqueueing a still-queued op collapses into the existing entry.

-
-

loading…

-
- - -
- - -
-

select inputs to nix flake update in /meta/. selected agents rebuild in sequence after the lock bump; manager learns each outcome via the usual rebuilt system event.

-
-

loading…

-
-
- - -
-

all-agents build log history. click a row to expand stdout + stderr. live builds stream in real time.

-
- -
-

loading…

-
- -
- - - - diff --git a/frontend/packages/dashboard/src/builds.js b/frontend/packages/dashboard/src/builds.js deleted file mode 100644 index afed59d2..00000000 --- a/frontend/packages/dashboard/src/builds.js +++ /dev/null @@ -1,632 +0,0 @@ -// BU1LDS page entry (/builds.html). The build lifecycle hub — rebuild -// queue, live build log, meta inputs, and build log history on one page. -// Three sub-tabs via the shared createTabStrip (default = Rebuild Queue): -// - R3BU1LD QU3U3: pending + running rebuilds + live build log inline -// - M3T4 1NPUTS: nix flake input selector + meta-update trigger -// - BUILD L0GS: all-agents build log history (lazy-loaded on tab show) -// -// Carved out of /core.html (rebuild queue + meta inputs) and /logs.html -// (BUILD tab) so the full build lifecycle is in one place. The section -// renderers here are direct copies from core.js / logs.js with only the -// deep-link URL and count-pill id adjusted. - -import { $, el, fmtAgeSecs, openStream, openBuildLogStream, initServerWarnings, bindAsyncForms } from './common.js'; -import { fmtAgo, fmtElapsed, fmtDuration, truncate } from './util.js'; -import { createTabStrip } from '@hive/shared/tabs.js'; - -// ─── derived state ─────────────────────────────────────────────────────────── -let metaInputsState = []; -let metaUpdateRunning = false; -let rebuildQueueState = []; - -function syncFromSnapshot(s) { - metaInputsState = (s.meta_inputs || []).slice(); - metaUpdateRunning = !!s.meta_update_running; - rebuildQueueState = (s.rebuild_queue || []).slice(); -} - -// ─── meta inputs ───────────────────────────────────────────────────────────── -function renderMetaInputs(s) { - const root = $('meta-inputs-section'); - if (!root) return; - // Snapshot ticked checkboxes before wiping so a concurrent - // MetaInputsChanged doesn't silently clear a pending selection. - const checkedInputs = new Set( - Array.from(root.querySelectorAll('input[type="checkbox"][data-meta-input]:checked')) - .map((cb) => cb.dataset.metaInput), - ); - root.replaceChildren(); - const inputs = s.meta_inputs || []; - if (!inputs.length) { - root.append(el('p', { class: 'empty' }, 'meta repo not seeded yet')); - return; - } - if (metaUpdateRunning) { - root.append(el('p', { class: 'meta-update-running' }, - '⏳ meta-update running — flake lock bump + affected agents rebuilding. ' - + 'watch the agent cards for per-rebuild progress.')); - } - const f = el('form', { - method: 'POST', - action: '/api/meta-update', - class: 'meta-inputs-form', - 'data-async': '', - 'data-no-refresh': '', - 'data-confirm': 'update selected meta flake inputs + rebuild affected agents?', - }); - const bulk = el('div', { class: 'meta-inputs-bulk' }); - const selAll = el('button', { type: 'button', class: 'meta-bulk-btn' }, 'select all'); - const selNone = el('button', { type: 'button', class: 'meta-bulk-btn' }, 'select none'); - bulk.append('bulk: ', selAll, ' ', selNone); - f.append(bulk); - const ul = el('ul', { class: 'meta-inputs' }); - for (const inp of inputs) { - const depth = (inp.name.match(/\//g) || []).length; - const leaf = inp.name.slice(inp.name.lastIndexOf('/') + 1); - const li = el('li'); - if (depth > 0) li.style.marginLeft = (depth * 1.3) + 'em'; - const id = 'meta-input-' + inp.name.replace(/[^a-z0-9-]/gi, '_'); - const cb = el('input', { - type: 'checkbox', - name: 'meta_input_' + inp.name, - id, - value: inp.name, - 'data-meta-input': inp.name, - }); - if (checkedInputs.has(inp.name)) cb.checked = true; - const label = el('label', { for: id, title: inp.name }); - label.append(cb); - if (depth > 0) label.append(el('span', { class: 'meta-input-twig' }, '└ ')); - label.append( - el('span', { class: 'meta-input-name' }, leaf), ' ', - el('code', { class: 'meta-input-rev' }, inp.rev.slice(0, 12)), ' ', - el('span', { class: 'meta-input-ts' }, fmtAgo(inp.last_modified)), - ); - if (inp.url) { - label.append(' ', el('span', { class: 'meta-input-url', title: inp.url }, - '· ' + truncate(inp.url, 48))); - } - li.append(label); - ul.append(li); - } - f.append(ul); - const hidden = el('input', { type: 'hidden', name: 'inputs', value: '' }); - f.append(hidden); - const btn = el('button', { - type: 'submit', - class: 'btn btn-meta-update', - disabled: '', - }, metaUpdateRunning ? '⏳ UPD4T1NG…' : '◆ UPD4TE & R3BU1LD'); - f.append(btn); - function refreshDisabled() { - const any = f.querySelectorAll('input[data-meta-input]:checked').length > 0; - if (any && !metaUpdateRunning) btn.removeAttribute('disabled'); - else btn.setAttribute('disabled', ''); - } - f.addEventListener('change', refreshDisabled); - function setAllChecked(val) { - for (const b of f.querySelectorAll('input[data-meta-input]')) b.checked = val; - refreshDisabled(); - } - selAll.addEventListener('click', () => setAllChecked(true)); - selNone.addEventListener('click', () => setAllChecked(false)); - f.addEventListener('submit', () => { - const selected = Array.from(f.querySelectorAll('input[data-meta-input]:checked')) - .map((b) => b.dataset.metaInput); - hidden.value = selected.join(','); - }); - root.append(f); -} - -// ─── rebuild queue ──────────────────────────────────────────────────────────── -const rebuildQueueRowCache = new Map(); -const QUEUE_KIND_GLYPH = { - rebuild: '↻', - meta_update: '◆', - spawn: '✨', - destroy: '🗑', - restart: '↺', - startup_sweep: '⚡', - perm_change: '🔑', -}; -const QUEUE_STATE_GLYPH = { - queued: '⏸', - running: '▶', - done: '✔', - failed: '✖', - cancelled: '⊘', -}; - -function rebuildQueueEntryFingerprint(entry, isChild) { - return JSON.stringify({ - state: entry.state, - step: entry.step, - kind: entry.kind, - agent: entry.agent, - source: entry.source, - started_at: entry.started_at, - enqueued_at: entry.enqueued_at, - finished_at: entry.finished_at, - reason: entry.reason, - error: entry.error, - build_log_id: entry.build_log_id, - isChild, - }); -} - -function renderRebuildQueue(s) { - const queue = s.rebuild_queue || []; - renderRebuildLiveLog(queue); - const root = $('rebuild-queue-section'); - if (!root) return; - - if (!queue.length) { - rebuildQueueRowCache.clear(); - root.replaceChildren(el('p', { class: 'empty' }, 'queue is empty — nothing pending or in flight.')); - return; - } - - const byId = new Map(queue.map((e) => [e.id, e])); - const tops = queue.filter((e) => e.parent_id == null); - const childrenOf = new Map(); - for (const e of queue) { - if (e.parent_id != null) { - if (!childrenOf.has(e.parent_id)) childrenOf.set(e.parent_id, []); - childrenOf.get(e.parent_id).push(e); - } - } - const orphans = queue.filter((e) => e.parent_id != null && !byId.has(e.parent_id)); - - const orderedLis = []; - function addEntry(entry, isChild) { - const fp = rebuildQueueEntryFingerprint(entry, isChild); - const cached = rebuildQueueRowCache.get(entry.id); - let li; - if (cached && cached.fingerprint === fp) { - li = cached.el; - } else { - li = renderQueueEntry(entry, byId, isChild); - rebuildQueueRowCache.set(entry.id, { el: li, fingerprint: fp }); - } - orderedLis.push(li); - } - for (const top of tops) { - addEntry(top, false); - for (const child of childrenOf.get(top.id) || []) addEntry(child, true); - } - for (const o of orphans) addEntry(o, true); - - const liveIds = new Set(queue.map((e) => e.id)); - for (const [id, entry] of rebuildQueueRowCache) { - if (!liveIds.has(id)) { - entry.el.remove(); - rebuildQueueRowCache.delete(id); - } - } - - let ul = root.querySelector('ul.rebuild-queue'); - if (!ul) { - ul = el('ul', { class: 'rebuild-queue' }); - root.replaceChildren(ul); - } - for (let i = 0; i < orderedLis.length; i++) { - if (ul.children[i] !== orderedLis[i]) { - ul.insertBefore(orderedLis[i], ul.children[i] ?? null); - } - } - while (ul.children.length > orderedLis.length) ul.lastChild.remove(); -} - -function renderQueueEntry(entry, _byId, isChild) { - const li = el('li', { - class: 'rebuild-queue-entry rqe-' + entry.state, - 'data-id': String(entry.id), - }); - if (isChild) li.classList.add('rqe-child'); - li.append( - el('span', { class: 'rqe-state', title: entry.state }, QUEUE_STATE_GLYPH[entry.state] || '?'), - ' ', - el('span', { class: 'rqe-kind', title: entry.kind }, - (QUEUE_KIND_GLYPH[entry.kind] || '?') + ' ' + entry.kind), - ' ', - el('code', { class: 'rqe-agent' }, entry.agent), - ); - li.append(' ', el('span', { class: 'rqe-source rqe-source-' + entry.source }, entry.source)); - if (entry.state === 'queued') { - li.append(' ', el('span', { - class: 'rqe-when', - 'data-rqe-enqueued': String(entry.enqueued_at), - }, '· queued ' + fmtAgo(entry.enqueued_at))); - } else if (entry.state === 'running' && entry.started_at) { - const elapsed = Math.max(0, Math.floor(Date.now() / 1000 - entry.started_at)); - li.append(' ', el('span', { - class: 'rqe-when', - 'data-rqe-elapsed': String(entry.started_at), - }, '· ' + fmtElapsed(elapsed))); - } else if (entry.finished_at) { - li.append(' ', el('span', { - class: 'rqe-when', - 'data-rqe-finished': String(entry.finished_at), - 'data-rqe-state': entry.state, - }, '· ' + entry.state + ' ' + fmtAgo(entry.finished_at))); - } - if (entry.reason) { - const r = entry.reason.split('\n')[0]; - li.append(' ', el('span', { class: 'rqe-reason', title: entry.reason }, '— ' + truncate(r, 60))); - } - if (entry.step) { - li.append(el('div', { class: 'rqe-step' }, '↳ ' + entry.step)); - } - if (entry.build_log_id != null) { - li.append( - ' ', - el('a', { - class: 'rqe-log-link', - href: '/builds.html?id=' + entry.build_log_id + '#buildlogs', - target: '_blank', - title: 'view build log #' + entry.build_log_id, - }, 'logs →'), - ); - } - if (entry.error) { - li.append(el('pre', { class: 'rqe-error', title: entry.error }, truncate(entry.error, 200))); - } - if (entry.state === 'queued') { - const cancelForm = el('form', { - method: 'POST', - action: '/api/rebuild-queue/' + entry.id + '/cancel', - class: 'inline rqe-cancel', - 'data-async': '', - 'data-confirm': - `cancel ${entry.kind} for \`${entry.agent}\` (queue id ${entry.id})? ` + - `the row drops from the queue and never runs. running / done / failed entries can't be cancelled this way.`, - }); - cancelForm.append(el('button', { - type: 'submit', - class: 'rqe-cancel-btn', - title: 'cancel this queued ' + entry.kind, - 'aria-label': 'cancel queued ' + entry.kind + ' for ' + entry.agent, - }, '✗')); - li.append(cancelForm); - } - return li; -} - -// ─── running-rebuild live log ───────────────────────────────────────────────── -// One persistent live-log panel for the currently-running rebuild (the queue -// runs one build at a time). Keyed to the running entry's build_log_id and -// kept in its own container (#rebuild-live-log) so the queue's row re-render -// — which rebuilds rows as the build step advances — never tears down the open -// SSE stream. -let liveLogEs = null; -let liveLogId = null; -let liveLogDone = false; -let liveLogCollapsed = false; - -function closeLiveLogStream() { - if (liveLogEs) { liveLogEs.close(); liveLogEs = null; } -} - -function renderRebuildLiveLog(queue) { - const root = $('rebuild-live-log'); - if (!root) return; - const running = (queue || []).find( - (e) => e.state === 'running' && e.build_log_id != null); - - if (!running) { - closeLiveLogStream(); - liveLogId = null; - liveLogDone = false; - if (!root.hidden) { root.hidden = true; root.replaceChildren(); } - return; - } - if (running.build_log_id === liveLogId && (liveLogEs || liveLogDone)) return; - - closeLiveLogStream(); - liveLogId = running.build_log_id; - liveLogDone = false; - root.hidden = false; - root.replaceChildren(); - - const pre = el('pre', { class: 'rebuild-live-log-output' }, ''); - pre.hidden = liveLogCollapsed; - const badge = el('span', { class: 'rebuild-live-log-badge rll-running' }, 'live'); - const toggle = el('button', { - type: 'button', - class: 'rebuild-live-log-toggle', - 'aria-expanded': String(!liveLogCollapsed), - title: liveLogCollapsed ? 'expand live log' : 'collapse live log', - }, liveLogCollapsed ? '▸' : '▾'); - toggle.addEventListener('click', () => { - liveLogCollapsed = !liveLogCollapsed; - pre.hidden = liveLogCollapsed; - toggle.textContent = liveLogCollapsed ? '▸' : '▾'; - toggle.setAttribute('aria-expanded', String(!liveLogCollapsed)); - toggle.title = liveLogCollapsed ? 'expand live log' : 'collapse live log'; - }); - const header = el('div', { class: 'rebuild-live-log-header' }, - toggle, ' ', - el('span', { class: 'rebuild-live-log-title' }, 'live build log — '), - el('code', { class: 'rqe-agent' }, running.agent), - ' ', el('span', { class: 'rqe-kind' }, running.kind || 'rebuild'), - ' ', badge, ' ', - el('a', { - class: 'rebuild-live-log-raw', - href: '/api/build-logs/id/' + running.build_log_id + '/raw', - download: 'build-log-' + running.build_log_id + '.txt', - }, '↓ raw'), - ); - root.append(header, pre); - - liveLogEs = openBuildLogStream(running.build_log_id, pre, { - onDone: (status) => { - liveLogDone = true; - badge.className = 'rebuild-live-log-badge ' + (status === 'ok' ? 'rll-ok' : 'rll-fail'); - badge.textContent = status; - liveLogEs = null; - }, - onError: () => { - badge.className = 'rebuild-live-log-badge rll-fail'; - badge.textContent = 'stream error'; - liveLogEs = null; - }, - }); -} - -// ─── rebuild-queue count pill ───────────────────────────────────────────────── -function updateRebuildCount() { - const pill = $('builds-tab-count-rebuild'); - if (!pill) return; - let n = 0; - for (const e of rebuildQueueState) { - if (e.state === 'queued' || e.state === 'running') n++; - } - if (n > 0) { pill.textContent = String(n); pill.hidden = false; } - else { pill.hidden = true; } -} - -// ─── render-all (cold load + any full re-render) ────────────────────────────── -function renderAll() { - renderRebuildQueue({ rebuild_queue: rebuildQueueState }); - renderMetaInputs({ meta_inputs: metaInputsState }); - updateRebuildCount(); -} - -// ─── elapsed-time tickers ───────────────────────────────────────────────────── -setInterval(() => { - const now = Math.floor(Date.now() / 1000); - for (const span of document.querySelectorAll('.rqe-when[data-rqe-elapsed]')) { - const started = parseInt(span.dataset.rqeElapsed, 10); - if (!started) continue; - span.textContent = '· ' + fmtElapsed(Math.max(0, now - started)); - } -}, 1000); -setInterval(() => { - for (const span of document.querySelectorAll('.rqe-when[data-rqe-enqueued]')) { - const enqueued = parseInt(span.dataset.rqeEnqueued, 10); - if (!enqueued) continue; - span.textContent = '· queued ' + fmtAgo(enqueued); - } - for (const span of document.querySelectorAll('.rqe-when[data-rqe-finished]')) { - const finished = parseInt(span.dataset.rqeFinished, 10); - if (!finished) continue; - const state = span.dataset.rqeState || ''; - span.textContent = '· ' + state + ' ' + fmtAgo(finished); - } -}, 30_000); - -// ─── BUILD L0GS tab ─────────────────────────────────────────────────────────── -// All-agents build log history with expand-to-detail. Live builds stream in -// real time. Lazy-loaded on first tab activation; auto-refreshes (debounced -// 2s) when rebuild_queue_changed fires while the BUILD L0GS tab is active. -// Deep-link: ?id=N#buildlogs auto-expands the target row. - -const buildList = $('build-list'); -const buildRefresh = $('build-refresh'); -let buildLogsLoaded = false; -let buildTabs; // set in init - -function fmtTs(unixSecs) { - if (!unixSecs) return ''; - const age = Math.floor(Date.now() / 1000) - unixSecs; - return fmtAgeSecs(Math.max(0, age)) + ' ago'; -} - -async function fetchBuild() { - if (!buildList) return; - buildList.replaceChildren(); - buildList.append(el('p', { class: 'meta' }, 'loading…')); - try { - const resp = await fetch('/api/build-logs?limit=30'); - if (!resp.ok) throw new Error('http ' + resp.status); - const rows = await resp.json(); - buildList.replaceChildren(); - if (!rows || rows.length === 0) { - buildList.append(el('p', { class: 'meta' }, '(no build logs yet)')); - return; - } - const ul = el('ul', { class: 'build-logs-list' }); - for (const h of rows) { - const li = el('li', { class: 'build-logs-item', 'data-log-id': String(h.id) }); - const live = !h.status; - const ok = h.status === 'ok'; - 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))) - : live - ? el('span', { class: 'build-logs-runtime meta build-logs-live-dur' }, '…') - : el('span', { class: 'build-logs-runtime meta' }, ''); - - let durTimer = null; - if (live && h.started_at) { - const updateDur = () => { - const elapsed = Math.floor(Date.now() / 1000) - h.started_at; - runtime.textContent = fmtDuration(Math.max(0, elapsed)); - }; - updateDur(); - durTimer = setInterval(updateDur, 1000); - } - - const rowBtn = el('button', { - type: 'button', - class: 'build-logs-row-btn', - 'aria-expanded': 'false', - }, - el('span', { class: statusClass }, statusLabel), - el('span', { class: 'build-logs-agent' }, h.agent), - runtime, - 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 streamEs = null; - rowBtn.addEventListener('click', async () => { - const expanded = rowBtn.getAttribute('aria-expanded') === 'true'; - rowBtn.setAttribute('aria-expanded', String(!expanded)); - detail.hidden = expanded; - if (expanded) { - if (streamEs) { streamEs.close(); streamEs = null; } - return; - } - if (detail.dataset.loaded) return; - detail.replaceChildren(); - detail.append( - el('a', { - href: '/api/build-logs/id/' + h.id + '/raw', - download: 'build-log-' + h.id + '.txt', - class: 'build-logs-dl', - }, '↓ download raw'), - ); - if (live) { - const pre = el('pre', { class: 'build-logs-output build-logs-live' }, ''); - const badge = el('span', { class: 'build-logs-live-badge badge badge-running' }, 'live'); - detail.append(badge, pre); - - streamEs = openBuildLogStream(h.id, pre, { - onDone: (status) => { - if (durTimer) { clearInterval(durTimer); durTimer = null; } - if (h.started_at) { - const elapsed = Math.floor(Date.now() / 1000) - h.started_at; - runtime.textContent = fmtDuration(Math.max(0, elapsed)); - } - badge.className = status === 'ok' ? 'badge badge-ok' : 'badge badge-fail'; - badge.textContent = status; - streamEs = null; - detail.dataset.loaded = '1'; - }, - onError: () => { - if (durTimer) { clearInterval(durTimer); durTimer = null; } - badge.textContent = 'stream error'; - badge.className = 'badge badge-fail'; - streamEs = null; - }, - }); - } else { - const pre = el('pre', { class: 'build-logs-output' }, 'fetching…'); - detail.append(pre); - try { - const r2 = await fetch('/api/build-logs/id/' + h.id); - if (!r2.ok) { - pre.textContent = 'error ' + r2.status; - } else { - const full = await r2.json(); - const out = [full.stdout, full.stderr].filter(Boolean).join('\n--- stderr ---\n'); - pre.textContent = out || '(empty)'; - } - detail.dataset.loaded = '1'; - } catch (err) { - pre.textContent = 'fetch failed: ' + err; - } - } - }); - li.append(rowBtn, detail); - ul.append(li); - } - buildList.append(ul); - // Deep-link: ?id=N auto-expands the target row. - const deepId = new URLSearchParams(location.search).get('id'); - if (deepId) { - const target = ul.querySelector('[data-log-id="' + deepId + '"]'); - if (target) { - const btn = target.querySelector('.build-logs-row-btn'); - if (btn) { - btn.click(); - target.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } - } - } - } catch (err) { - buildList.replaceChildren(); - buildList.append(el('p', { class: 'meta' }, 'fetch failed: ' + err)); - } -} - -if (buildRefresh) buildRefresh.addEventListener('click', fetchBuild); - -// ─── SSE handlers ───────────────────────────────────────────────────────────── -let buildRefreshTimer = null; -const SSE_HANDLERS = { - rebuild_queue_changed(ev) { - rebuildQueueState = (ev.queue || []).slice(); - renderRebuildQueue({ rebuild_queue: rebuildQueueState }); - updateRebuildCount(); - // Auto-refresh build log list when the queue changes and BUILD L0GS is active. - if (buildTabs && buildTabs.active() === 'buildlogs') { - if (buildRefreshTimer) clearTimeout(buildRefreshTimer); - buildRefreshTimer = setTimeout(fetchBuild, 2000); - } - }, - meta_inputs_changed(ev) { - metaInputsState = (ev.inputs || []).slice(); - renderMetaInputs({ meta_inputs: metaInputsState }); - }, - meta_update_running(ev) { - metaUpdateRunning = !!ev.running; - renderMetaInputs({ meta_inputs: metaInputsState }); - }, -}; - -// ─── boot ───────────────────────────────────────────────────────────────────── -async function refreshState() { - try { - const resp = await fetch('/api/state'); - if (resp.ok) syncFromSnapshot(await resp.json()); - } catch { - // best-effort - } - renderAll(); -} - -async function init() { - initServerWarnings(); - bindAsyncForms(() => refreshState()); - - buildTabs = createTabStrip(document.getElementById('builds-tabbar'), { - defaultId: 'rebuild', - onShow: (id) => { - if (id === 'buildlogs' && !buildLogsLoaded) { - buildLogsLoaded = true; - fetchBuild(); - } - }, - }); - - await refreshState(); - - const es = openStream('/api/dashboard/stream'); - if (es) { - es.onmessage = (e) => { - let ev; - try { ev = JSON.parse(e.data); } catch { return; } - const h = SSE_HANDLERS[ev.kind]; - if (h) h(ev); - }; - } -} - -init(); diff --git a/frontend/packages/dashboard/src/core.css b/frontend/packages/dashboard/src/core.css index f17ac635..57509df9 100644 --- a/frontend/packages/dashboard/src/core.css +++ b/frontend/packages/dashboard/src/core.css @@ -1,21 +1,18 @@ /* ─── /core.html — C0R3 page ─────────────────────────────────────── - Standalone page for kept state (tombstones) and container load. - Same minimal-chrome pattern as /logs.html (a `← home` back-link + - a createTabStrip sub-tab strip; base tab + page-header styling comes + Standalone page carved out of the dashboard's old SYST3M tab. Same + minimal-chrome pattern as /logs.html (a `← home` back-link + a + createTabStrip sub-tab strip; base tab + page-header styling comes from @hive/shared via common.css, linked by core.html). - The section renderers (kept state, container load) emit the same - class names as before. Those section-internal rules live in - system-sections.css (shared with dashboard.css and builds.css, which - references `.rqe-source*` from the schedules view). The + The four section renderers (rebuild queue, meta inputs, kept state, + container load) are ported verbatim from the dashboard, so they emit + the exact same class names. Those section-internal rules live in + system-sections.css (shared with the dashboard, which references a + couple of them — `.rqe-source*` — from its schedules view). The `.hive-stats-table` the container-load table reuses lives in common.css, linked directly by core.html. Importing just the section rules keeps the C0R3 bundle small instead of dragging in all of - dashboard.css. - - Rebuild queue + meta inputs have moved to /builds.html; the - `.rebuild-live-log-*` styles that used to live here have moved to - system-sections.css so builds.css can share them via @import. */ + dashboard.css. */ @import "./system-sections.css"; body.core-shell { @@ -36,3 +33,70 @@ body.core-shell { /* createTabStrip toggles the native `hidden` attribute on inactive panes; ensure it wins over any inherited display. */ .core-pane[hidden] { display: none; } + +/* ─── running-rebuild live log (renderRebuildLiveLog in core.js) ────────── + Streams the currently-running rebuild's build log inline under the queue. + Hidden (native `hidden` attr) when nothing is building. */ +.rebuild-live-log { + margin-top: 0.8em; + border: 1px solid var(--purple-dim); + border-radius: 4px; + background: color-mix(in srgb, var(--crust) 70%, transparent); +} +.rebuild-live-log-header { + display: flex; + align-items: center; + gap: 0.4em; + padding: 0.4em 0.6em; + font-size: 0.85em; + border-bottom: 1px solid var(--border); +} +.rebuild-live-log-toggle { + background: transparent; + border: 0; + color: var(--muted); + cursor: pointer; + font-family: inherit; + font-size: 1em; + padding: 0 0.2em; +} +.rebuild-live-log-toggle:hover { color: var(--fg); } +.rebuild-live-log-title { color: var(--muted); } +.rebuild-live-log-raw { + margin-left: auto; + color: var(--purple); + text-decoration: none; + font-size: 0.9em; +} +.rebuild-live-log-raw:hover { text-decoration: underline; } +.rebuild-live-log-badge { + font-size: 0.75em; + font-weight: bold; + padding: 0.05em 0.5em; + border-radius: 999px; + letter-spacing: 0.05em; +} +.rebuild-live-log-badge.rll-running { + color: var(--amber); + background: color-mix(in srgb, var(--amber) 18%, transparent); +} +.rebuild-live-log-badge.rll-ok { + color: var(--green); + background: color-mix(in srgb, var(--green) 18%, transparent); +} +.rebuild-live-log-badge.rll-fail { + color: var(--red); + background: color-mix(in srgb, var(--red) 18%, transparent); +} +.rebuild-live-log-output { + margin: 0; + padding: 0.5em 0.7em; + max-height: 22em; + overflow-y: auto; + white-space: pre-wrap; + word-break: break-word; + font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "Source Code Pro", monospace; + font-size: 0.8em; + line-height: 1.4; + color: var(--fg); +} diff --git a/frontend/packages/dashboard/src/core.html b/frontend/packages/dashboard/src/core.html index b538a3d3..118afcf9 100644 --- a/frontend/packages/dashboard/src/core.html +++ b/frontend/packages/dashboard/src/core.html @@ -16,14 +16,23 @@ /core.html) carved out of the dashboard's old SYST3M tab so the dashboard tab strip stays lean. Same minimal chrome as /logs.html — a `← home` back-link to the H0M3 hub + a - createTabStrip sub-tab nav. Two sub-tabs: kept state (tombstones) - and container load. Rebuild queue + meta inputs have moved to - /builds.html (the build lifecycle hub). Default tab: K3PT ST4T3. - The section
ids (tombstones-section, container-load-section) - match what core.js's renderers target. --> + createTabStrip sub-tab nav. Default sub-tab is the rebuild queue + (the thing the operator actually watches). The section
ids + (rebuild-queue-section, meta-inputs-section, tombstones-section, + container-load-section) match what core.js's renderers target, + which are the same renderers the dashboard SYST3M tab used. -->