Context. surface operator · domain operator (house — audit/event log) · route /operator/audit · auth signed-in · source apps/oshun/web/src/app/operator/audit/page.tsx
Last walked. 2026-06-25 audit ledger row-cell addendum — direct signed-in /operator/audit render, generatedAt/cursor anchors, exact 3-row masthead/table count, real /v1/audit fixture rows sorted newest-first, row metadata attributes, visible When/Actor/Action/Target cells, footer targets, explorer mount, and anonymous redirect-before-render verified in tenant-audit-log-investigation.spec.ts. 2026-06-26 addendum — explorer Export Markdown is now browser-driven through the real BFF export route and the downloaded Markdown body is asserted. 2026-06-26 meta-audit addendum — investigation create/update/share/export/ delete writes audit.investigation.* rows and the real-HTTP spec reads the chain back through the explorer event API. Evidence: WALKTHROUGH/results/v1-real-infra-run-2026-06-22.md §90.
Purpose#
The append-only audit ledger: the most recent rows from GET /v1/audit, newest
first. Every actor, action, and target laid out in a mono table for
investigation — never edited in place. The static ledger is followed by the
OperatorAuditExplorer, which provides search/filter, save-as-investigation,
and export/delete controls over the live /v1/admin/audit-log/* investigation
store.
Entry points#
- From
/operator(hub) — operator-surface card; the foot row's "← Operator hub" returns there - Cross-links from sibling operator pages —
/operator/incidentsand/operator/ssofoot rows both link to the audit log;/operator/personascross-links back here through its audit affordance - From
/operator/incidentsduring a triage — investigating an incident's actor trail leads here - Direct URL — session-gated only (
proxy.tsredirects sessionless →/welcome?redirect=); no operator-role gate in the proxy - Bookmark —
canonical = '/operator/audit'
Layout regions#
page.tsx is an async server component:
await bffGet<AuditResponse>('/v1/audit') (falling back to { events: [] } on
null), client-side re-sorted newest-first by occurredAtIso, rendered inside
<LWebShell top={<LCustomerNav active="library" />}> (note: it reuses the
customer nav with Library active, not an operator-specific bar). Centered
column maxWidth: 1080.
- Header:
LCustomerNav(Library highlighted — verify whether an operator shell bar is intended instead) - Page anchor: outer content column exposes
data-operator-audit-page,data-operator-audit-generated-at, anddata-operator-audit-cursor - Masthead:
LMastheadwrapped bydata-operator-audit-mastheadwithdata-operator-audit-row-count; visible copy is left "Operator · audit", right${sorted.length} rows shown, kicker "Append-only", title "The audit ledger." - Ledger: a mono
<table data-operator-audit-table>(When / Actor / Action / Target) withdata-operator-audit-table-countOR the empty card<thead>:Thcells (uppercase eyebrow style) — When, Actor, Action, Target, withscope="col"anddata-operator-audit-column<tbody>: one<tr data-operator-audit-row>per event;Tdcells — local timestamp, actorId, accent-colored action, andtargetKind:targetId; row attributes exposedata-operator-audit-id,data-operator-audit-actor,data-operator-audit-action,data-operator-audit-target-kind,data-operator-audit-target-id, anddata-operator-audit-occurred-at; visible cells exposedata-operator-audit-cell="when" | "actor" | "action" | "target"
- Explorer:
<OperatorAuditExplorer data-audit-explorer>— filter fields for text, event type prefix, actor ids, workspace ids, and time window; search and reset actions; save-as-investigation; saved investigation load/export JSON/export Markdown/delete controls; event result rows with optional deep links - Footer action row:
LBtnghost "← Operator hub" (/operator),LBtnghost "Incidents" (/operator/incidents),LBtnghost "Personas" (/operator/personas),LBtnprimary "SSO" (/operator/sso)
States#
- Loading — source-verified 2026-06-25: no route-level
loading.tsx; server-sideawait bffGetblocks render, so there is no skeleton state - Anonymous user — Playwright 2026-06-25: sessionless
/operator/auditredirects to/welcome?...before[data-operator-audit-page]or[data-operator-audit-table]render - Signed-in user — Playwright 2026-06-25: primed signed-in session renders the page, generatedAt/cursor anchors, exact row counts, ledger, footer, and explorer. Still session-gated only; no operator-role gate in the proxy or in-page guard
- Empty — source-verified 2026-06-25:
sorted.length === 0renders[data-operator-audit-empty]with "No audit events available for the current cursor." Masthead right reads "0 rows shown" - Populated (short) — Playwright 2026-06-25: real
/v1/auditfixture renders exactly three rows, newest first:evt-3tenant member added,evt-2SSO connection activated, andevt-1incident acknowledged - Populated (long) — table with the full page of rows; no pagination
control despite
data.cursor— only the rows returned by one/v1/auditcall are shown (verify the server-side page size and how to advance the cursor) - BFF unreachable / non-2xx — source-verified 2026-06-25:
bffGetreturnsnull; the page substitutes{ cursor: null, events: [] }→ empty state (no fabricated rows). Dedicated outage-route E2E still pending - Offline — RSC fetch (
revalidate 60); SW behavior for/operator/*should be network-only (verify SW excludes operator routes) - Standalone PWA — renders; operator surfaces are robots-noindex
elsewhere — verify this route's robots policy (no
robotsexport in this file)
Interactions#
Masthead#
- Masthead labels — non-interactive; Playwright asserts "Operator · audit", "Append-only", "The audit ledger.", exact "3 rows shown", and the matching stable row-count attribute
Audit table#
- Header row (
<thead>/Th) — non-interactive; not sortable (sort is fixed newest-first in code);scope="col"is present and Playwright asserts the fourcolumnheadernames plus stable column identifiers - Each body row (
<tr>) — non-interactive display: When (localizedoccurredAtIso), Actor (actorId), Action (accent textaction), Target (targetKind:targetId). Playwright asserts all three real fixture rows in newest-first order with stable id/actor/action/target-kind/target-id/ occurred-at attributes and visible When/Actor/Action/Target cells. No row click / drill-in wired- Screen reader: announces cell contents in reading order; verify the
targetKind:targetIdreads clearly
- Screen reader: announces cell contents in reading order; verify the
Footer actions#
- "← Operator hub" (
LBtnghost, link) →/operator - "Incidents" (
LBtnghost, link) →/operator/incidents - "Personas" (
LBtnghost, link) →/operator/personas - "SSO" (
LBtnprimary, link) →/operator/sso
Audit explorer#
- Filter fields — text, event type prefix, actor ids, workspace ids, and datetime range fields render below the static ledger
- Search — Playwright 2026-06-10/2026-06-25: needle and actor filters
land on the real
/v1/admin/audit-log/eventsrequest URL through the live BFF bridge - Save investigation — Playwright creates a named investigation through the UI and asserts it appears in the saved-investigations list
- Export JSON — Playwright triggers a real Blob download for the saved investigation
- Export Markdown — Playwright 2026-06-26: the browser clicks the
explorer control, the live BFF bridge serves
GET /v1/admin/audit-log/investigations/:id/export?format=markdown, and the downloaded.mdbody includes the investigation title, created/updated provenance line, and## Matched events (...) - Delete investigation — Playwright deletes through the UI and verifies both the row removal and API read-back absence
Data & contracts#
- Reads:
bffGet<AuditResponse>('/v1/audit')—{ generatedAt: string; cursor: string | null; events: AuditEvent[] }whereAuditEvent = { id, actorId, action, targetKind, targetId, occurredAtIso }. Onnullsubstitutes an empty list - Writes: the static ledger is read-only. The explorer writes investigation
records through
/v1/admin/audit-log/investigations; successful create/update/share/export/delete also appendaudit.investigation.created | updated | shared | exported | deletedrows to the same admin audit-event store. The share event records only token issuance/hash presence, never the raw share token - Realtime: None observed.
- Caching: RSC fetch,
next.revalidate = 60, 5 s abort; SW policy for operator routes TBD - Auth/role check: session-gated only (
proxy.tsredirects sessionless →/welcome?redirect=); no operator-role gate in the proxy and no in-page guard.bffGetforwards the session cookie
Cross-references#
- Parent:
operator-overview.md - Sibling operator routes:
operator-incidents.mdoperator-sso.mdoperator-tenants.mdoperator-admin.md— the inbox triage surface
- Feature spec:
V1/features.md - Architecture:
V1/ARCHITECTURE.md - Journeys traversing this view:
E2E coverage#
apps/oshun/web/e2e/tenant-audit-log-investigation.spec.ts— direct/operator/auditsigned-in render, real/v1/auditfixture table rows sorted newest-first, generatedAt/cursor anchors, exact 3-row masthead/table count, column headers and column identifiers, stable row id/actor/action/target-kind/target-id/occurred-at attributes, visible When/Actor/Action/Target cells, footer targets (/operator,/operator/incidents,/operator/personas,/operator/sso),/operator/incidents"Audit log" entry-point navigation,/operator/adminaudit-entry affordance, anonymous redirect-before-render, real-HTTP BFF audit-log access-control/CRUD/export/share/bookmark legs, and explorer UI filter/save/export-json/export-markdown/delete over the live BFF bridge. The same spec's real-HTTP leg also verifies the investigation meta-audit chain (audit.investigation.created/updated/shared/exported/deleted), confirms invalid create and duplicate delete emit no phantom audit row, and asserts raw share tokens are not present in emitted payloads.- Remaining route-specific E2E gaps are the static ledger empty/outage branch, long/cursor pagination behavior, standalone/offline/SW policy, and the product gaps listed below.
Open questions / known gaps#
-
data.cursoris read but never surfaced; there is no "load more" / next-page control — document how an investigator reaches older rows - This operator route renders the customer
LCustomerNav(Library active) rather than an operator bar; confirm that's intended chrome - No
robots: { index: false }export here unlike/operator/admin; verify operator routes are kept out of SEO at the proxy/layout level - Confirm SW caching policy for
/operator/*(expected network-only) - An operator-role gate appears absent —
proxy.tsonly redirects sessionless users to/welcome; no role check in the proxy or page component (possible gap vs intended scope)