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
|
name (populated from `GET /api/state`) and unit filter
|
||||||
(`hive-ag3nt.service` / `(full machine journal)`). Fetches
|
(`hive-ag3nt.service` / `(full machine journal)`). Fetches
|
||||||
`GET /api/journal/{name}?unit=<unit>&lines=500` on selection change
|
`GET /api/journal/{name}?unit=<unit>&lines=500` on selection change
|
||||||
or `↻ refresh`. Output rendered as a `<pre>` block. The last-selected
|
or `↻ refresh`. Output rendered as a `<pre>` block. A `?agent=<name>`
|
||||||
agent name and unit are persisted to `localStorage` (`logs-agent` /
|
and/or `?unit=<svc>` URL param pre-selects the agent + unit on page
|
||||||
`logs-unit`) and restored on page load. A `?agent=<name>` and/or
|
load — the per-agent `⋮` menu's **journal logs →** entry uses this to
|
||||||
`?unit=<svc>` URL param overrides the stored values — the per-agent
|
deep-link directly to a specific agent's journal. A "fetched N ago"
|
||||||
`⋮` menu's **journal logs →** entry uses this to deep-link directly
|
chip appears after the `↻ refresh` button following each successful
|
||||||
to a specific agent's journal. A "fetched N ago" chip appears after
|
fetch and ticks every 30 s.
|
||||||
the `↻ refresh` button following each successful fetch and ticks
|
|
||||||
every 30 s.
|
|
||||||
|
|
||||||
**SYSTEM sub-tab** — host-side service logs. Unit selector (currently
|
**SYSTEM sub-tab** — host-side service logs. Unit selector (currently
|
||||||
only `hive-c0re.service`). Fetches
|
only `hive-c0re.service`). Fetches
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,8 @@ body.logs-shell {
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
}
|
}
|
||||||
.journal-refresh { font-size: 0.75em; padding: 0.15em 0.5em; }
|
.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 {
|
.journal-output {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: var(--crust);
|
background: var(--crust);
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@
|
||||||
<option value="">(full machine journal)</option>
|
<option value="">(full machine journal)</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="button" class="btn btn-restart" id="agent-refresh">↻ refresh</button>
|
<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>
|
</div>
|
||||||
<pre id="agent-output" class="journal-output">select an agent above</pre>
|
<pre id="agent-output" class="journal-output">select an agent above</pre>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -69,6 +70,7 @@
|
||||||
<option value="hive-c0re.service">hive-c0re.service</option>
|
<option value="hive-c0re.service">hive-c0re.service</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="button" class="btn btn-restart" id="system-refresh">↻ refresh</button>
|
<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>
|
</div>
|
||||||
<pre id="system-output" class="journal-output">loading…</pre>
|
<pre id="system-output" class="journal-output">loading…</pre>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,11 @@
|
||||||
// - The build list auto-refreshes (debounced 2s) when rebuild_queue_changed
|
// - The build list auto-refreshes (debounced 2s) when rebuild_queue_changed
|
||||||
// fires from the dashboard SSE stream, so new log entries appear without
|
// fires from the dashboard SSE stream, so new log entries appear without
|
||||||
// a manual page refresh.
|
// 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 {
|
import {
|
||||||
$, el, fmtAgeSecs, openStream,
|
$, el, fmtAgeSecs, openStream,
|
||||||
|
|
@ -265,8 +270,16 @@ import {
|
||||||
const agentUnitSelect = $('agent-unit-select');
|
const agentUnitSelect = $('agent-unit-select');
|
||||||
const agentRefresh = $('agent-refresh');
|
const agentRefresh = $('agent-refresh');
|
||||||
const agentOutput = $('agent-output');
|
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() {
|
async function loadAgentList() {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/state');
|
const resp = await fetch('/api/state');
|
||||||
|
|
@ -278,10 +291,26 @@ import {
|
||||||
for (const c of (state.containers || [])) {
|
for (const c of (state.containers || [])) {
|
||||||
agentSelect.append(el('option', { value: c.name }, c.name));
|
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 { /**/ }
|
} catch { /**/ }
|
||||||
}
|
}
|
||||||
|
|
||||||
let agentFetching = false;
|
let agentFetching = false;
|
||||||
|
let agentLastFetch = 0;
|
||||||
async function fetchAgent() {
|
async function fetchAgent() {
|
||||||
if (!agentSelect || !agentOutput) return;
|
if (!agentSelect || !agentOutput) return;
|
||||||
const name = agentSelect.value;
|
const name = agentSelect.value;
|
||||||
|
|
@ -289,6 +318,7 @@ import {
|
||||||
if (agentFetching) return;
|
if (agentFetching) return;
|
||||||
agentFetching = true;
|
agentFetching = true;
|
||||||
agentOutput.textContent = 'fetching…';
|
agentOutput.textContent = 'fetching…';
|
||||||
|
if (agentFetchTs) agentFetchTs.hidden = true;
|
||||||
const unit = agentUnitSelect ? agentUnitSelect.value : '';
|
const unit = agentUnitSelect ? agentUnitSelect.value : '';
|
||||||
const params = new URLSearchParams({ lines: '500' });
|
const params = new URLSearchParams({ lines: '500' });
|
||||||
if (unit) params.set('unit', unit);
|
if (unit) params.set('unit', unit);
|
||||||
|
|
@ -297,6 +327,13 @@ import {
|
||||||
const text = await resp.text();
|
const text = await resp.text();
|
||||||
agentOutput.textContent = resp.ok ? (text || '(empty)') : 'error ' + resp.status + '\n' + text;
|
agentOutput.textContent = resp.ok ? (text || '(empty)') : 'error ' + resp.status + '\n' + text;
|
||||||
agentOutput.scrollTop = agentOutput.scrollHeight;
|
agentOutput.scrollTop = agentOutput.scrollHeight;
|
||||||
|
if (resp.ok) {
|
||||||
|
agentLastFetch = Date.now();
|
||||||
|
if (agentFetchTs) {
|
||||||
|
agentFetchTs.textContent = fmtFetchTs(agentLastFetch);
|
||||||
|
agentFetchTs.hidden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
agentOutput.textContent = 'fetch failed: ' + err;
|
agentOutput.textContent = 'fetch failed: ' + err;
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -313,13 +350,16 @@ import {
|
||||||
const systemUnitSelect = $('system-unit-select');
|
const systemUnitSelect = $('system-unit-select');
|
||||||
const systemRefresh = $('system-refresh');
|
const systemRefresh = $('system-refresh');
|
||||||
const systemOutput = $('system-output');
|
const systemOutput = $('system-output');
|
||||||
|
const systemFetchTs = $('system-fetch-ts');
|
||||||
|
|
||||||
let systemFetching = false;
|
let systemFetching = false;
|
||||||
|
let systemLastFetch = 0;
|
||||||
async function fetchSystem() {
|
async function fetchSystem() {
|
||||||
if (!systemOutput) return;
|
if (!systemOutput) return;
|
||||||
if (systemFetching) return;
|
if (systemFetching) return;
|
||||||
systemFetching = true;
|
systemFetching = true;
|
||||||
systemOutput.textContent = 'fetching…';
|
systemOutput.textContent = 'fetching…';
|
||||||
|
if (systemFetchTs) systemFetchTs.hidden = true;
|
||||||
const unit = systemUnitSelect ? systemUnitSelect.value : 'hive-c0re.service';
|
const unit = systemUnitSelect ? systemUnitSelect.value : 'hive-c0re.service';
|
||||||
const params = new URLSearchParams({ lines: '500' });
|
const params = new URLSearchParams({ lines: '500' });
|
||||||
if (unit) params.set('unit', unit);
|
if (unit) params.set('unit', unit);
|
||||||
|
|
@ -328,6 +368,13 @@ import {
|
||||||
const text = await resp.text();
|
const text = await resp.text();
|
||||||
systemOutput.textContent = resp.ok ? (text || '(empty)') : 'error ' + resp.status + '\n' + text;
|
systemOutput.textContent = resp.ok ? (text || '(empty)') : 'error ' + resp.status + '\n' + text;
|
||||||
systemOutput.scrollTop = systemOutput.scrollHeight;
|
systemOutput.scrollTop = systemOutput.scrollHeight;
|
||||||
|
if (resp.ok) {
|
||||||
|
systemLastFetch = Date.now();
|
||||||
|
if (systemFetchTs) {
|
||||||
|
systemFetchTs.textContent = fmtFetchTs(systemLastFetch);
|
||||||
|
systemFetchTs.hidden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
systemOutput.textContent = 'fetch failed: ' + err;
|
systemOutput.textContent = 'fetch failed: ' + err;
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -346,4 +393,15 @@ import {
|
||||||
fetchBuild();
|
fetchBuild();
|
||||||
if (tab === 'system') fetchSystem();
|
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