feat(#986): dedicated logs page with build/agent/system sub-tabs

Add /logs.html as a standalone page (same back-link pattern as flow.html):
- BUILD tab: all-agents build log history via new GET /api/build-logs endpoint
- AGENT tab: per-container journald viewer with agent selector + unit filter
- SYSTEM tab: host-side hive-c0re.service logs via new GET /api/journal-host endpoint

Remove inline log drill-ins from SW4RM container rows (buildJournalTrigger
and buildBuildLogsTrigger) — log viewing now lives on the dedicated page.

flow.html: strip the full dashboard tabbar, replace with a simple back link
matching the new logs page chrome.

index.html: add L0GS tab link to /logs.html in the tab strip.

Backend additions:
- build_logs::list_recent_all — cross-agent query (newest first, cap 100)
- GET /api/build-logs — all-agents variant backed by list_recent_all
- GET /api/journal-host — host journald (no -M container flag), restricted
  to allow-listed units (hive-c0re.service)
This commit is contained in:
iris 2026-06-01 19:11:47 +02:00
commit 0b15cad93f
9 changed files with 534 additions and 299 deletions

View file

@ -318,6 +318,25 @@ impl BuildLogs {
Ok(out)
}
/// Return the most recent `limit` rows across all agents, newest first.
/// Same header-only shape as `list_recent_for_agent`. Limit clamped to 100.
pub fn list_recent_all(&self, limit: usize) -> Result<Vec<BuildLogHeader>> {
let limit = limit.min(100);
let conn = self.conn.lock().unwrap();
let mut stmt = conn.prepare(
"SELECT id, agent, kind, cmdline, started_at, finished_at, status
FROM build_logs
ORDER BY started_at DESC
LIMIT ?1",
)?;
let rows = stmt.query_map(params![i64::try_from(limit).unwrap_or(100)], row_to_header)?;
let mut out = Vec::new();
for r in rows {
out.push(r?);
}
Ok(out)
}
/// Fetch a single full row (with stdout/stderr text) by id.
/// Returns `None` when the id doesn't exist (vacuum sweep already
/// reaped it, or the operator passed a stale id from a refresh