From 24cbafc64ac60d2658087637f240994eacdc0b47 Mon Sep 17 00:00:00 2001 From: iris Date: Sat, 13 Jun 2026 18:41:17 +0200 Subject: [PATCH] feat(dashboard): live-append the LOGS AUDIT tab via audit_entry_added MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/web-ui/dashboard.md | 5 ++++- frontend/packages/dashboard/src/logs.js | 21 ++++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 1053601b..002bd8ec 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -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 diff --git a/frontend/packages/dashboard/src/logs.js b/frontend/packages/dashboard/src/logs.js index 0ab06764..5febcaf0 100644 --- a/frontend/packages/dashboard/src/logs.js +++ b/frontend/packages/dashboard/src/logs.js @@ -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