feat(logs): URL deep-link + fetched-age chip for AGENT/SYSTEM tabs
Adds ?agent=name and ?unit=svc URL params to pre-select the agent on the AGENT tab (used by the per-agent overflow menu journal logs link). Last-fetched timestamp chip shows how stale the output is. Agent selection is NOT persisted to localStorage — URL params only.
This commit is contained in:
parent
d043b1ed4e
commit
b6b93f9fff
4 changed files with 69 additions and 9 deletions
|
|
@ -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=<unit>&lines=500` on selection change
|
||||
or `↻ refresh`. Output rendered as a `<pre>` block. The last-selected
|
||||
agent name and unit are persisted to `localStorage` (`logs-agent` /
|
||||
`logs-unit`) and restored on page load. A `?agent=<name>` and/or
|
||||
`?unit=<svc>` 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 `<pre>` block. A `?agent=<name>`
|
||||
and/or `?unit=<svc>` 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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@
|
|||
<option value="">(full machine journal)</option>
|
||||
</select>
|
||||
<button type="button" class="btn btn-restart" id="agent-refresh">↻ refresh</button>
|
||||
<span id="agent-fetch-ts" class="meta logs-fetch-ts" hidden></span>
|
||||
</div>
|
||||
<pre id="agent-output" class="journal-output">select an agent above</pre>
|
||||
</section>
|
||||
|
|
@ -69,6 +70,7 @@
|
|||
<option value="hive-c0re.service">hive-c0re.service</option>
|
||||
</select>
|
||||
<button type="button" class="btn btn-restart" id="system-refresh">↻ refresh</button>
|
||||
<span id="system-fetch-ts" class="meta logs-fetch-ts" hidden></span>
|
||||
</div>
|
||||
<pre id="system-output" class="journal-output">loading…</pre>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
})();
|
||||
|
|
|
|||
Loading…
Reference in a new issue