diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index b87a4c08..f2d75b38 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -410,14 +410,12 @@ immediate re-fetch. name (populated from `GET /api/state`) and unit filter (`hive-ag3nt.service` / `(full machine journal)`). Fetches `GET /api/journal/{name}?unit=&lines=500` on selection change -or `↻ refresh`. Output rendered as a `
` block. The last-selected
-agent name and unit are persisted to `localStorage` (`logs-agent` /
-`logs-unit`) and restored on page load. A `?agent=` and/or
-`?unit=` URL param overrides the stored values — the per-agent
-`⋮` menu's **journal logs →** entry uses this to deep-link directly
-to a specific agent's journal. A "fetched N ago" chip appears after
-the `↻ refresh` button following each successful fetch and ticks
-every 30 s.
+or `↻ refresh`. Output rendered as a `
` block. A `?agent=`
+and/or `?unit=` URL param pre-selects the agent + unit on page
+load — the per-agent `⋮` menu's **journal logs →** entry uses this to
+deep-link directly to a specific agent's journal. A "fetched N ago"
+chip appears after the `↻ refresh` button following each successful
+fetch and ticks every 30 s.
 
 **SYSTEM sub-tab** — host-side service logs. Unit selector (currently
 only `hive-c0re.service`). Fetches
diff --git a/frontend/packages/dashboard/src/logs.css b/frontend/packages/dashboard/src/logs.css
index 8069c779..8105be9c 100644
--- a/frontend/packages/dashboard/src/logs.css
+++ b/frontend/packages/dashboard/src/logs.css
@@ -71,6 +71,8 @@ body.logs-shell {
   font-size: 0.85em;
 }
 .journal-refresh { font-size: 0.75em; padding: 0.15em 0.5em; }
+/* "fetched N ago" chip next to the refresh button on AGENT + SYSTEM tabs. */
+.logs-fetch-ts { font-size: 0.8em; color: var(--muted); }
 .journal-output {
   margin: 0;
   background: var(--crust);
diff --git a/frontend/packages/dashboard/src/logs.html b/frontend/packages/dashboard/src/logs.html
index 2a4ad7c3..5bd4d38a 100644
--- a/frontend/packages/dashboard/src/logs.html
+++ b/frontend/packages/dashboard/src/logs.html
@@ -56,6 +56,7 @@
           
         
         
+        
       
       
select an agent above
@@ -69,6 +70,7 @@ +
loading…
diff --git a/frontend/packages/dashboard/src/logs.js b/frontend/packages/dashboard/src/logs.js index b334d1c8..ef5962f0 100644 --- a/frontend/packages/dashboard/src/logs.js +++ b/frontend/packages/dashboard/src/logs.js @@ -13,6 +13,11 @@ // - The build list auto-refreshes (debounced 2s) when rebuild_queue_changed // fires from the dashboard SSE stream, so new log entries appear without // a manual page refresh. +// +// URL params `?agent=name` and `?unit=svc` pre-select the agent + unit on +// the AGENT tab (deep-link from other pages, e.g. the per-agent ⋮ menu). +// Last-fetched timestamp is shown next to the refresh button on AGENT + +// SYSTEM tabs so the operator knows how stale the output is. import { $, el, fmtAgeSecs, openStream, @@ -265,8 +270,16 @@ import { const agentUnitSelect = $('agent-unit-select'); const agentRefresh = $('agent-refresh'); const agentOutput = $('agent-output'); + const agentFetchTs = $('agent-fetch-ts'); - // Populate agent selector from /api/state + // Format a last-fetched timestamp: "fetched just now" / "fetched 3m ago". + function fmtFetchTs(fetchedAt) { + const ageSecs = Math.floor((Date.now() - fetchedAt) / 1000); + return 'fetched ' + (ageSecs < 5 ? 'just now' : fmtAgeSecs(ageSecs) + ' ago'); + } + + // Populate agent selector from /api/state, then honour any `?agent=` / + // `?unit=` URL params (used by the per-agent ⋮ menu's deep-link). async function loadAgentList() { try { const resp = await fetch('/api/state'); @@ -278,10 +291,26 @@ import { for (const c of (state.containers || [])) { agentSelect.append(el('option', { value: c.name }, c.name)); } + // Deep-link: honour ?agent= and ?unit= URL params. + const urlAgent = new URLSearchParams(location.search).get('agent'); + const urlUnit = new URLSearchParams(location.search).get('unit'); + if (urlAgent) { + const found = Array.from(agentSelect.options).some((o) => o.value === urlAgent); + if (found) { + agentSelect.value = urlAgent; + if (urlUnit && agentUnitSelect) { + const unitFound = Array.from(agentUnitSelect.options) + .some((o) => o.value === urlUnit); + if (unitFound) agentUnitSelect.value = urlUnit; + } + fetchAgent(); + } + } } catch { /**/ } } let agentFetching = false; + let agentLastFetch = 0; async function fetchAgent() { if (!agentSelect || !agentOutput) return; const name = agentSelect.value; @@ -289,6 +318,7 @@ import { if (agentFetching) return; agentFetching = true; agentOutput.textContent = 'fetching…'; + if (agentFetchTs) agentFetchTs.hidden = true; const unit = agentUnitSelect ? agentUnitSelect.value : ''; const params = new URLSearchParams({ lines: '500' }); if (unit) params.set('unit', unit); @@ -297,6 +327,13 @@ import { const text = await resp.text(); agentOutput.textContent = resp.ok ? (text || '(empty)') : 'error ' + resp.status + '\n' + text; agentOutput.scrollTop = agentOutput.scrollHeight; + if (resp.ok) { + agentLastFetch = Date.now(); + if (agentFetchTs) { + agentFetchTs.textContent = fmtFetchTs(agentLastFetch); + agentFetchTs.hidden = false; + } + } } catch (err) { agentOutput.textContent = 'fetch failed: ' + err; } finally { @@ -313,13 +350,16 @@ import { const systemUnitSelect = $('system-unit-select'); const systemRefresh = $('system-refresh'); const systemOutput = $('system-output'); + const systemFetchTs = $('system-fetch-ts'); let systemFetching = false; + let systemLastFetch = 0; async function fetchSystem() { if (!systemOutput) return; if (systemFetching) return; systemFetching = true; systemOutput.textContent = 'fetching…'; + if (systemFetchTs) systemFetchTs.hidden = true; const unit = systemUnitSelect ? systemUnitSelect.value : 'hive-c0re.service'; const params = new URLSearchParams({ lines: '500' }); if (unit) params.set('unit', unit); @@ -328,6 +368,13 @@ import { const text = await resp.text(); systemOutput.textContent = resp.ok ? (text || '(empty)') : 'error ' + resp.status + '\n' + text; systemOutput.scrollTop = systemOutput.scrollHeight; + if (resp.ok) { + systemLastFetch = Date.now(); + if (systemFetchTs) { + systemFetchTs.textContent = fmtFetchTs(systemLastFetch); + systemFetchTs.hidden = false; + } + } } catch (err) { systemOutput.textContent = 'fetch failed: ' + err; } finally { @@ -346,4 +393,15 @@ import { fetchBuild(); if (tab === 'system') fetchSystem(); + // Tick the last-fetched timestamps every 30s so "fetched 1m ago" stays + // accurate without a manual refresh. + setInterval(() => { + if (agentLastFetch && agentFetchTs && !agentFetchTs.hidden) { + agentFetchTs.textContent = fmtFetchTs(agentLastFetch); + } + if (systemLastFetch && systemFetchTs && !systemFetchTs.hidden) { + systemFetchTs.textContent = fmtFetchTs(systemLastFetch); + } + }, 30_000); + })();