feat(dashboard): live-append the LOGS AUDIT tab via audit_entry_added

Completes the audit log end-to-end. The backend now emits an
audit_entry_added event on /dashboard/stream per privileged action (the
new row flattened at top level). logs.js already subscribes to that stream
for the BUILD tab; add a branch that prepends the new row to the AUDIT
table's cached list (de-duped by id against the cold fetch), bumps the
'latest N of M' total, and re-renders while the AUDIT tab is in view — so
a restart shows up without a manual refresh. Docs updated to match.
This commit is contained in:
iris 2026-06-13 18:41:17 +02:00
commit 24cbafc64a
2 changed files with 24 additions and 2 deletions

View file

@ -568,7 +568,10 @@ refusal) shown amber and labelled `denied` so it reads apart from an
execution failure. `ts_unix` is unix seconds; a 30 s ticker keeps the
relative "ago" column honest while the tab is in view. The backing
`audit_log` store records every privileged-action attempt (ok / err /
denied); a live-append off `/dashboard/stream` is a planned follow-up.
denied). New entries live-append without a refresh: an `audit_entry_added`
event on `/dashboard/stream` (the flattened row) is prepended to the table
and the "latest N of M" count bumped, de-duped by id against the cold
fetch.
## Container row

View file

@ -3,7 +3,8 @@
// BUILD — all-agents build log history, backed by GET /api/build-logs
// AGENT — per-container journald viewer, backed by GET /api/journal/{name}
// SYSTEM — host service logs, backed by GET /api/journal-host
// AUDIT — agent-initiated privileged-action trail, GET /api/audit-log
// AUDIT — agent-initiated privileged-action trail, GET /api/audit-log;
// live-appends via the `audit_entry_added` /dashboard/stream event
//
// Tab routing via URL hash (#build, #agent, #system, #audit). Default: #build.
//
@ -245,6 +246,7 @@ import { createTabStrip } from '@hive/shared/tabs.js';
let ev;
try { ev = JSON.parse(e.data); } catch { return; }
if (ev.kind === 'rebuild_queue_changed') debouncedRefreshBuild();
else if (ev.kind === 'audit_entry_added') onAuditEntryAdded(ev);
};
}
}
@ -489,6 +491,23 @@ import { createTabStrip } from '@hive/shared/tabs.js';
if (auditRefresh) auditRefresh.addEventListener('click', fetchAudit);
if (auditFilter) auditFilter.addEventListener('input', renderAudit);
// Live-append: an `audit_entry_added` event on /dashboard/stream carries a
// new row flattened at the top level ({ kind, seq, id, ts_unix, agent,
// action, target, outcome, detail }). Prepend it (newest-first), de-duped
// by id against whatever the cold fetch already returned, and bump the
// total so the "latest N of M" header stays right. Re-render only while
// the AUDIT tab is in view; otherwise the next tab-show fetch is
// authoritative anyway. Wired into the shared stream onmessage above.
function onAuditEntryAdded(ev) {
if (auditEntries.some((e) => e.id === ev.id)) return;
auditEntries.unshift({
id: ev.id, ts_unix: ev.ts_unix, agent: ev.agent, action: ev.action,
target: ev.target, outcome: ev.outcome, detail: ev.detail,
});
auditTotal += 1;
if (logTabs.active() === 'audit') renderAudit();
}
// ─── init ─────────────────────────────────────────────────────────────
// Wire the shared tab strip now that fetchSystem + the element refs it