diff --git a/frontend/packages/dashboard/src/common.js b/frontend/packages/dashboard/src/common.js index cd60f399..8a05f6ee 100644 --- a/frontend/packages/dashboard/src/common.js +++ b/frontend/packages/dashboard/src/common.js @@ -46,10 +46,11 @@ export const form = (action, btnClass, btnLabel, confirmMsg, extra = {}, opts = return f; }; -// `truncate`, `fmtAgo`, `fmtElapsed`, `fmtDuration` stay in tabs.js -// for now — each has display-specific phrasing ("X running", "X ago") -// tied to its caller, so they don't generalise cleanly. We can lift -// them when a second consumer needs the same shape. +// `truncate`, `fmtAgo`, `fmtElapsed`, `fmtDuration` + the `paintAtomic` +// render helper live in the dashboard-internal `./util.js`, not here — +// their phrasing ("X running", "X ago") is dashboard-specific, so they +// stay out of the cross-page `common.js` but are shared across the +// dashboard's own tab modules. // ─── shared-worker SSE pipe ───────────────────────────────────────────── // Returns an EventSource-shaped facade backed by a SharedWorker that diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index e896184e..7345755b 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -20,6 +20,7 @@ import { } from './common.js'; import { createTabStrip } from '@hive/shared/tabs.js'; import { containersState, syncContainersFromSnapshot } from './state.js'; +import { paintAtomic, fmtAgo, truncate, fmtElapsed, fmtDuration } from './util.js'; import { applyCapabilitiesChanged, applyToolGroupsChanged, fetchAndRenderCapabilities, fetchAndRenderToolGroups, @@ -43,16 +44,6 @@ window.marked = marked; const CTX_WARN_TOKENS = 150_000; // fallback red threshold (≈ 75% of 200k) const CTX_CAUTION_TOKENS = 100_000; // fallback yellow threshold (≈ 50% of 200k) - // Atomic-swap render helper: build into a DocumentFragment off-DOM, - // commit with one `replaceChildren`. See docs/web-ui.md::Atomic - // section repaint for why (no intermediate empty-state flash on - // poll cycles even when the builder allocates a lot of `el()`). - function paintAtomic(liveRoot, build) { - const buf = document.createDocumentFragment(); - build(buf); - liveRoot.replaceChildren(buf); - } - // Track which items we've already notified about so a re-render // doesn't re-fire for the same row. Keyed by stable ids; reset only // when the page reloads. @@ -2110,17 +2101,6 @@ window.marked = marked; root.append(ul); } - // Relative time, anchored to now. resolved_at is unix seconds (server- - // authored), so we don't have to worry about client/server clock skew - // for sub-minute precision. - function fmtAgo(unixSecs) { - const ageSec = Math.max(0, Math.floor(Date.now() / 1000 - unixSecs)); - if (ageSec < 60) return ageSec + 's ago'; - if (ageSec < 3600) return Math.floor(ageSec / 60) + 'm ago'; - if (ageSec < 86400) return Math.floor(ageSec / 3600) + 'h ago'; - return Math.floor(ageSec / 86400) + 'd ago'; - } - function renderMetaInputs(s) { const root = $('meta-inputs-section'); if (!root) return; @@ -2230,10 +2210,6 @@ window.marked = marked; root.append(form); } - function truncate(s, n) { - return s.length <= n ? s : s.slice(0, n - 1) + '…'; - } - // ─── rebuild queue ────────────────────────────────────────────────────── // Keyed row cache for the rebuild-queue list. Maps entry.id → { el, fingerprint }. // Same pattern as containerRowCache: reuse
  • nodes whose state hasn't @@ -2461,12 +2437,6 @@ window.marked = marked; return li; } - function fmtElapsed(secs) { - if (secs < 60) return secs + 's running'; - if (secs < 3600) return Math.floor(secs / 60) + 'm ' + (secs % 60) + 's running'; - return Math.floor(secs / 3600) + 'h ' + Math.floor((secs % 3600) / 60) + 'm running'; - } - // Tick once per second to refresh "running Xs" badges in place // (mirrors the question-TTL ticker pattern above). // Tick rebuild-queue elapsed-time badges once per second. @@ -2605,13 +2575,6 @@ window.marked = marked; root.append(ul); }); } - function fmtDuration(secs) { - if (secs < 60) return secs + 's'; - if (secs < 3600) return Math.floor(secs / 60) + 'm ' + (secs % 60) + 's'; - if (secs < 86400) return Math.floor(secs / 3600) + 'h ' + Math.floor((secs % 3600) / 60) + 'm'; - return Math.floor(secs / 86400) + 'd ' + Math.floor((secs % 86400) / 3600) + 'h'; - } - // ─── scheduled prompts ───────────────────────────────────────────────── // Backend exposes `/api/schedules` (snapshot), `/api/schedules` // (POST, operator-direct submit), `/api/schedules/{id}/cancel` diff --git a/frontend/packages/dashboard/src/util.js b/frontend/packages/dashboard/src/util.js new file mode 100644 index 00000000..5ebb1e57 --- /dev/null +++ b/frontend/packages/dashboard/src/util.js @@ -0,0 +1,49 @@ +// Shared dashboard render + format helpers. +// +// Small pure utilities used across most dashboard tabs (the container +// tree, the rebuild queue, questions / approvals, schedules + reminders): +// an atomic-swap render helper and a handful of relative-time / duration +// formatters. They live here — a dashboard-internal module — rather than +// in the cross-page `common.js`, because they're specific to the +// dashboard's render style and not needed by the stand-alone pages. +// +// All pure: no DOM/module state captured, so any tab module can import +// them freely without ordering concerns. + +// Atomic-swap render: build into an off-DOM DocumentFragment, then commit +// with a single `replaceChildren`. Avoids an intermediate empty-state +// flash on poll cycles even when the builder allocates a lot of nodes. +export function paintAtomic(liveRoot, build) { + const buf = document.createDocumentFragment(); + build(buf); + liveRoot.replaceChildren(buf); +} + +// Relative age of a unix timestamp, coarsened to one unit ("5m ago"). +export function fmtAgo(unixSecs) { + const ageSec = Math.max(0, Math.floor(Date.now() / 1000 - unixSecs)); + if (ageSec < 60) return ageSec + 's ago'; + if (ageSec < 3600) return Math.floor(ageSec / 60) + 'm ago'; + if (ageSec < 86400) return Math.floor(ageSec / 3600) + 'h ago'; + return Math.floor(ageSec / 86400) + 'd ago'; +} + +// Truncate a string to `n` chars, appending an ellipsis when clipped. +export function truncate(s, n) { + return s.length <= n ? s : s.slice(0, n - 1) + '…'; +} + +// Running-duration label for in-flight items ("3m 12s running"). +export function fmtElapsed(secs) { + if (secs < 60) return secs + 's running'; + if (secs < 3600) return Math.floor(secs / 60) + 'm ' + (secs % 60) + 's running'; + return Math.floor(secs / 3600) + 'h ' + Math.floor((secs % 3600) / 60) + 'm running'; +} + +// Compact duration label, two units deep ("1h 5m", "2d 3h"). +export function fmtDuration(secs) { + if (secs < 60) return secs + 's'; + if (secs < 3600) return Math.floor(secs / 60) + 'm ' + (secs % 60) + 's'; + if (secs < 86400) return Math.floor(secs / 3600) + 'h ' + Math.floor((secs % 3600) / 60) + 'm'; + return Math.floor(secs / 86400) + 'd ' + Math.floor((secs % 86400) / 3600) + 'h'; +}