Tenant Admin Console · Surface walkthrough

Tenant audit log

A per-surface walkthrough of the Tenant Admin Console tenant-admin surface: layout, states, interactions, data, and cross-references.

drafted
8sections3 minread

On this page

Context. surface tenant-admin · domain governance · route /audit · auth signed-in (tenant) + scope:tenant:admin:{tenantId} (or tenant:admin:*) · source apps/oshun/tenant-admin/src/app/audit/page.tsx

Last walked.

Purpose#

A tenant-scoped, severity-ranked audit-log explorer. It renders the records that pass applyAuditFilter for a tenant-admin viewer, with a per-severity summary and a hash-chain digest line per record. It is the audit workspace (group: governance) in @oshun/navigation, strictly isolated to the viewer's tenant.

Entry points#

  • Shell sidebar Governance › Audit item (TenantSidebar, shortLabel: 'Audit', path /audit).
  • Direct URL /audit (bookmarkable; scope-gated).

Layout regions#

Wrapped in <TenantShell session currentWorkspaceId="audit">:

  • Sidebar / Header: standard chrome (header crumb "Audit log").
  • Main (<section data-testid="tenant-audit-panel" data-tenant-id={...}>):
    • <h1>Tenant audit log</h1>
    • Subtitle: record count + info N · warning N · critical N summary, noting strict scoping to <code>{tenantId}</code> and that cross-tenant access requires platform-operator scope.
    • <ol data-testid="tenant-audit-records"> (styles.recordList) — one <li> per record with data-record-id, data-severity, data-action:
      • Record header: <time> (localized), severity badge, action <code>, and actorId · actorRole.
      • Record body: target: {class} (+ optional target id + correlation).
      • Record digest: digest {contentDigest} (+ optional prior {digest}).

States#

  • Loading — N/A; synchronous server render from seedAuditRecords() + applyAuditFilter() (no fetch).
  • Empty — reachable only if a filter removes all records; the current AuditFilter is all-null (no filtering), so the seeded 4 records always show. Empty <ol> would render no <li> and the count reads "0 records".
  • Populated (short) — always 4 seeded records: member.invited (info), role.assigned (info), policy.attestation_submitted (warning), member.suspended (critical).
  • Populated (long) — N/A; fixed 4-record seed; no pagination/virtualization.
  • Error (recoverable) — N/A; no query.
  • Error (unrecoverable) — no custom handler; render throw → Next default.
  • Offline — N/A (fully server-rendered).
  • Gated — no session → redirect('/unauthorized?reason=missing-session&returnTo=/audit'); middleware blocks unauthenticated requests upstream. Cross-tenant records are filtered out by applyAuditFilter against the ExplorerViewer.
  • Standalone PWA — N/A; no manifest. (verify.)

Interactions#

  • Audit record list items (<li data-record-id> in data-testid="tenant-audit-records")
    • Function: render-only; severity colorizes via severityClass() (severity_info / severity_warning / severity_critical). No expand, filter, sort, or export controls exist in code — read-only display.
    • Keyboard: not focusable.
    • Screen reader: ordered-list semantics; <time dateTime> carries ISO value; severity rendered as text inside the badge.
    • Touch target: N/A (non-interactive).
    • Mobile (≤ 640 px): audit.module.css record layout.
    • Disabled when: N/A.
    • Offline behavior: N/A.
    • Telemetry: None.
  • Note: the workspace primaryJob describes "filters, saved investigations, and exportable bundles", but none of those controls are present in the current page (see Open questions).

Data & contracts#

  • Reads: getTenantServerSession(); applyAuditFilter({ records, filter, viewer }) from @oshun/tenant-console — input AuditLogRecord[], AuditFilter, ExplorerViewer { role: 'tenant-admin', tenantId }; returns the filtered readonly AuditLogRecord[]. Records come from the in-file seedAuditRecords(tenantId). No BFF call.
  • Writes: None.
  • Realtime: None.
  • Caching: SSR (dynamic; cookie read). Deterministic per tenant id.
  • Auth/role check: middleware.ts + page getTenantServerSession()canEnterTenantShell. Tenant isolation additionally enforced by applyAuditFilter via ExplorerViewer.tenantId.

Cross-references#

  • Domain logic: libs/oshun/tenant-console/src/audit-explorer/index.ts (applyAuditFilter, AuditFilter, AuditLogRecord, ExplorerViewer).
  • Workspace model: libs/oshun/navigation/src/tenant-ia.ts (audit, group: governance).
  • Operator-admin audit explorer (cross-tenant): ../ADMIN_WALKTHROUGH/workspaces/operations/analytics.md (AdminAuditLogExplorerPanel).
  • Sibling routes: home.md, data.md. /policy, /roles, /status lack files yet.

Open questions / known gaps#

  • Records are hard-coded seed data; no BFF read of the canonical tenant audit stream is wired. Document the eventual endpoint + hash-chain verification source.
  • No filter UI, saved-investigation, or export controls despite the primaryJob describing them — the AuditFilter is fixed all-null. Track these as unbuilt.