diff --git a/frontend/packages/dashboard/build.mjs b/frontend/packages/dashboard/build.mjs index 6814cd62..b0d79747 100644 --- a/frontend/packages/dashboard/build.mjs +++ b/frontend/packages/dashboard/build.mjs @@ -6,9 +6,6 @@ // at GET /dashboard.html // dist/flow.html served at GET /flow.html // 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 + @@ -55,7 +52,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')], + entryPoints: [src('tabs.js'), src('flow.js'), src('logs.js'), src('home.js'), src('settings.js'), src('stats.js')], outdir: staticDir(''), bundle: true, format: 'esm', @@ -94,7 +91,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']) { +for (const entry of ['colors.css', 'theme.css', 'common.css', 'dashboard.css', 'flow.css', 'logs.css', 'home.css', 'settings.css', 'stats.css']) { await build({ entryPoints: [src(entry)], outfile: staticDir(entry), @@ -104,7 +101,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']) { +for (const html of ['index.html', 'dashboard.html', 'flow.html', 'logs.html', 'settings.html', 'stats.html']) { copyFileSync(src(html), dist(html)); } diff --git a/frontend/packages/dashboard/src/core.css b/frontend/packages/dashboard/src/core.css deleted file mode 100644 index 50fa2c28..00000000 --- a/frontend/packages/dashboard/src/core.css +++ /dev/null @@ -1,36 +0,0 @@ -/* ─── /core.html — C0R3 page ─────────────────────────────────────── - 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 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 - dashboard.css; rather than duplicate the (scattered) blocks here, we - @import dashboard.css wholesale — transitional. dashboard.css does - NOT @import common.css (it relies on link order), so this doesn't - double-load anything; the dashboard-only chrome rules it carries - (`body.dashboard-shell`, `.dashboard-chrome`, `.tabbar`) simply match - nothing on /core.html. The follow-up that removes the SYST3M tab can - split the shared section rules into their own stylesheet. */ -@import "./dashboard.css"; - -body.core-shell { - margin: 0; - padding: 0; -} - -/* Fill the header row next to the ← home back-link, same as the logs - sub-tab strip. */ -.core-tabbar { - flex: 1; -} - -.core-main { - padding: 1.2em 1.5em 2em; -} - -/* createTabStrip toggles the native `hidden` attribute on inactive - panes; ensure it wins over any inherited display. */ -.core-pane[hidden] { display: none; } diff --git a/frontend/packages/dashboard/src/core.html b/frontend/packages/dashboard/src/core.html deleted file mode 100644 index 4a56f22c..00000000 --- a/frontend/packages/dashboard/src/core.html +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - hyperhive // C0R3 - - - - - - - - - - - -
- - -
-

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…

-
-
- - -
-

kept state from previously tombstoned agents — recreating an agent with the same name reuses it.

-
-

loading…

-
-
- - -
-

live cpu + memory per agent container, from cgroup v2 on the host. cpu is % of total host capacity (all cores), sampled over ~200ms each refresh; polled every 5s while this tab is open. network is omitted on purpose — agents share the host netns, so there is no per-container counter (see docs/network.md).

-
-

loading…

-
-
- -
- - - - diff --git a/frontend/packages/dashboard/src/core.js b/frontend/packages/dashboard/src/core.js deleted file mode 100644 index 32050261..00000000 --- a/frontend/packages/dashboard/src/core.js +++ /dev/null @@ -1,541 +0,0 @@ -// C0R3 page entry (/core.html). The host/coordinator surface carved out -// of the dashboard's old SYST3M tab into its own standalone page so the -// dashboard tab strip stays lean. Four sub-tabs via the shared -// createTabStrip (default = Rebuild Queue): rebuild queue, meta inputs, -// kept state (tombstones), container load. -// -// This page is its own esbuild bundle with its own runtime: it cold-loads -// /api/state and subscribes to /dashboard/stream (the same broker event -// channel the dashboard uses), maintaining its own copies of the derived -// state. The four section renderers below are ported from tabs.js (the -// dashboard SYST3M tab); the dashboard keeps its own copies for now — -// de-duplication + removing the SYST3M tab is a deliberate follow-up. - -import { $, el, form, openStream, initServerWarnings } from './common.js'; -import { fmtAgo, fmtElapsed, truncate } from './util.js'; -import { createTabStrip } from '@hive/shared/tabs.js'; - -// ─── derived state (own copies; this bundle has its own runtime) ────────── -let metaInputsState = []; -let metaUpdateRunning = false; -let tombstonesState = []; -let rebuildQueueState = []; - -function syncFromSnapshot(s) { - metaInputsState = (s.meta_inputs || []).slice(); - metaUpdateRunning = !!s.meta_update_running; - tombstonesState = (s.tombstones || []).slice(); - 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: '/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 root = $('rebuild-queue-section'); - if (!root) return; - const queue = s.rebuild_queue || []; - - 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: '/logs.html?id=' + entry.build_log_id + '#build', - 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; -} - -// ─── kept state (tombstones) ────────────────────────────────────────────── -function renderTombstones(s) { - const root = $('tombstones-section'); - if (!root) return; - root.replaceChildren(); - if (!s.tombstones || !s.tombstones.length) { - root.append(el('p', { class: 'empty' }, 'no kept state — clean')); - return; - } - const fmtBytes = (n) => { - if (n < 1024) return n + ' B'; - if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB'; - if (n < 1024 * 1024 * 1024) return (n / (1024 * 1024)).toFixed(1) + ' MB'; - return (n / (1024 * 1024 * 1024)).toFixed(2) + ' GB'; - }; - const fmtAgeDays = (ts) => { - if (!ts) return '?'; - const d = Math.floor((Date.now() / 1000 - ts) / 86400); - if (d <= 0) return 'today'; - if (d === 1) return '1 day ago'; - return d + ' days ago'; - }; - const ul = el('ul', { class: 'containers' }); - for (const t of s.tombstones) { - const li = el('li', { class: 'container-row tombstone' }); - const head = el('div', { class: 'head' }); - head.append( - el('span', { class: 'name' }, t.name), - el('span', { class: 'badge badge-muted' }, 'destroyed'), - ); - if (t.has_creds) head.append(el('span', { class: 'badge badge-muted' }, 'creds kept')); - head.append(el('span', { class: 'meta' }, - `${fmtBytes(t.state_bytes)} · ${fmtAgeDays(t.last_seen)}`)); - li.append(head); - - const actions = el('div', { class: 'actions' }); - const respawn = el('form', { - method: 'POST', action: '/request-spawn', - class: 'inline', 'data-async': '', - 'data-confirm': 'queue spawn approval for ' + t.name + '? state will be reused.', - }); - respawn.append( - el('input', { type: 'hidden', name: 'name', value: t.name }), - el('button', { type: 'submit', class: 'btn btn-start' }, '⊕ R3V1V3'), - ); - actions.append(respawn); - actions.append(form( - '/purge-tombstone/' + t.name, 'btn-destroy', 'PURG3', - 'PURGE ' + t.name + '? config history, claude creds, ' - + 'and notes are all WIPED. no undo.', - {}, { noRefresh: true }, - )); - li.append(actions); - ul.append(li); - } - root.append(ul); -} - -// ─── container load (live cgroup poll while the LOAD sub-tab is open) ────── -let containerLoadTimer = null; - -function cloadFmtBytes(n) { - if (!Number.isFinite(n) || n <= 0) return '0'; - const u = ['B', 'KiB', 'MiB', 'GiB', 'TiB']; - let i = 0; let v = n; - while (v >= 1024 && i < u.length - 1) { v /= 1024; i += 1; } - return v.toFixed(v < 10 && i > 0 ? 1 : 0) + ' ' + u[i]; -} -function cloadMeter(pct) { - const p = Math.max(0, Math.min(100, pct)); - const cls = p >= 90 ? 'hot' : (p >= 70 ? 'warn' : ''); - const m = document.createElement('span'); - m.className = 'cload-meter'; - m.title = p.toFixed(0) + '%'; - const fill = document.createElement('span'); - fill.className = 'fill' + (cls ? ' ' + cls : ''); - fill.style.width = p + '%'; - m.append(fill); - return m; -} - -function renderContainerLoad(rows) { - const root = $('container-load-section'); - if (!root) return; - if (!Array.isArray(rows) || rows.length === 0) { - root.replaceChildren(); - const p = document.createElement('p'); p.className = 'meta'; - p.textContent = 'no running agent containers'; root.append(p); - return; - } - const table = document.createElement('table'); - table.className = 'hive-stats-table'; - table.innerHTML = 'agentcpumemory' - + 'peaklimit'; - const tb = document.createElement('tbody'); - for (const r of rows) { - const tr = document.createElement('tr'); - const name = document.createElement('td'); name.textContent = r.name; tr.append(name); - const cpu = document.createElement('td'); cpu.className = 'num'; - cpu.append((Number(r.cpu_pct) || 0).toFixed(1) + '%', cloadMeter(Number(r.cpu_pct) || 0)); - tr.append(cpu); - const mem = document.createElement('td'); mem.className = 'num'; - const memCur = Number(r.mem_current_bytes) || 0; - if (r.mem_max_bytes) { - mem.append(cloadFmtBytes(memCur), cloadMeter(100 * memCur / r.mem_max_bytes)); - } else { - mem.textContent = cloadFmtBytes(memCur); - } - tr.append(mem); - const peak = document.createElement('td'); peak.className = 'num'; - peak.textContent = r.mem_peak_bytes ? cloadFmtBytes(Number(r.mem_peak_bytes)) : '—'; - tr.append(peak); - const lim = document.createElement('td'); lim.className = 'num'; - lim.textContent = r.mem_max_bytes ? cloadFmtBytes(Number(r.mem_max_bytes)) : '∞'; - tr.append(lim); - tb.append(tr); - } - table.append(tb); - root.replaceChildren(table); -} - -async function refreshContainerLoad() { - try { - const resp = await fetch('/api/container-resources'); - if (!resp.ok) throw new Error('http ' + resp.status); - renderContainerLoad(await resp.json()); - } catch (e) { - const root = $('container-load-section'); - if (root) { - root.replaceChildren(); - const p = document.createElement('p'); p.className = 'meta'; - p.textContent = 'container load fetch failed: ' + e; root.append(p); - } - } -} -function startContainerLoadPolling() { - refreshContainerLoad(); - if (containerLoadTimer) return; - containerLoadTimer = setInterval(refreshContainerLoad, 5000); -} -function stopContainerLoadPolling() { - if (containerLoadTimer) { clearInterval(containerLoadTimer); containerLoadTimer = null; } -} - -// ─── rebuild-queue count pill ───────────────────────────────────────────── -function updateRebuildCount() { - const pill = $('core-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 }); - renderTombstones({ tombstones: tombstonesState }); - updateRebuildCount(); -} - -// ─── elapsed-time tickers (same cadence as the dashboard) ───────────────── -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); - -// ─── live SSE handlers (own copies; no SW4RM re-render on this page) ─────── -const SSE_HANDLERS = { - rebuild_queue_changed(ev) { - rebuildQueueState = (ev.queue || []).slice(); - renderRebuildQueue({ rebuild_queue: rebuildQueueState }); - updateRebuildCount(); - }, - meta_inputs_changed(ev) { - metaInputsState = (ev.inputs || []).slice(); - renderMetaInputs({ meta_inputs: metaInputsState }); - }, - meta_update_running(ev) { - metaUpdateRunning = !!ev.running; - renderMetaInputs({ meta_inputs: metaInputsState }); - }, - tombstones_changed(ev) { - tombstonesState = (ev.tombstones || []).slice(); - renderTombstones({ tombstones: tombstonesState }); - }, -}; - -// ─── boot ───────────────────────────────────────────────────────────────── -async function init() { - initServerWarnings(); - - // Hash-routed sub-tab strip; default Rebuild Queue. Container-load - // polling runs only while the LOAD sub-tab is open (cpu is a short - // two-sample read each refresh on the server). - createTabStrip(document.getElementById('core-tabbar'), { - defaultId: 'rebuild', - onShow: (id) => { - if (id === 'load') startContainerLoadPolling(); - else stopContainerLoadPolling(); - }, - }); - - try { - const resp = await fetch('/api/state'); - if (resp.ok) { - syncFromSnapshot(await resp.json()); - } - } catch { - // best-effort: the page still renders its empty states - } - renderAll(); - - const es = openStream('/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/dashboard.html b/frontend/packages/dashboard/src/dashboard.html index fe8546d2..690ee87f 100644 --- a/frontend/packages/dashboard/src/dashboard.html +++ b/frontend/packages/dashboard/src/dashboard.html @@ -35,11 +35,12 @@ ◆ Y3R C4LL ◆ - + + ◆ SYST3M ◆ + + - + +