Compare commits

..
5 changed files with 9 additions and 91 deletions

View file

@ -410,12 +410,14 @@ 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. A `?agent=<name>` or `↻ refresh`. Output rendered as a `<pre>` block. The last-selected
and/or `?unit=<svc>` URL param pre-selects the agent + unit on page agent name and unit are persisted to `localStorage` (`logs-agent` /
load — the per-agent `⋮` menu's **journal logs →** entry uses this to `logs-unit`) and restored on page load. A `?agent=<name>` and/or
deep-link directly to a specific agent's journal. A "fetched N ago" `?unit=<svc>` URL param overrides the stored values — the per-agent
chip appears after the `↻ refresh` button following each successful `⋮` menu's **journal logs →** entry uses this to deep-link directly
fetch and ticks every 30 s. 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 **SYSTEM sub-tab** — host-side service logs. Unit selector (currently
only `hive-c0re.service`). Fetches only `hive-c0re.service`). Fetches

View file

@ -71,8 +71,6 @@ 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);

View file

@ -56,7 +56,6 @@
<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>
@ -70,7 +69,6 @@
<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>

View file

@ -13,11 +13,6 @@
// - 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,
@ -270,16 +265,8 @@ 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');
// Format a last-fetched timestamp: "fetched just now" / "fetched 3m ago". // Populate agent selector from /api/state
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');
@ -291,26 +278,10 @@ 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;
@ -318,7 +289,6 @@ 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);
@ -327,13 +297,6 @@ 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 {
@ -350,16 +313,13 @@ 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);
@ -368,13 +328,6 @@ 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 {
@ -393,15 +346,4 @@ 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);
})(); })();

View file

@ -426,20 +426,6 @@ window.marked = marked;
return el('li', { class: 'agent-menu-sep', role: 'separator' }); return el('li', { class: 'agent-menu-sep', role: 'separator' });
} }
// Navigation link item (opens in same tab by default).
function menuLink(label, href, title) {
const li = el('li', { role: 'presentation' });
const a = el('a', {
class: 'agent-menu-item',
href,
role: 'menuitem',
title: title || '',
}, label);
a.addEventListener('click', () => closeAllAgentMenus());
li.append(a);
return li;
}
// Show only actions that are applicable in the current state. // Show only actions that are applicable in the current state.
if (c.running) { if (c.running) {
dropdown.append( dropdown.append(
@ -453,14 +439,6 @@ window.marked = marked;
} }
dropdown.append( dropdown.append(
menuItem('↻ R3BU1LD', { action: '/rebuild/', confirm: `rebuild ${c.name}? hot-reloads the container.` }), menuItem('↻ R3BU1LD', { action: '/rebuild/', confirm: `rebuild ${c.name}? hot-reloads the container.` }),
menuSep(),
// Deep-link to the AGENT log tab pre-filtered to this container.
// The ?agent= param is read by logs.js on load; the stored localStorage
// selection is overridden so the operator lands directly in this
// agent's journal without extra clicks.
menuLink('journal logs →',
`/logs.html?agent=${encodeURIComponent(c.name)}#agent`,
`view ${c.name} journal logs`),
); );
{ {