---
path: /audit
surface: tenant-admin
domain: governance
auth: signed-in (tenant) + scope:tenant:admin:{tenantId} (or tenant:admin:*)
source: apps/oshun/tenant-admin/src/app/audit/page.tsx
status: drafted
last_walked: '—'
---

# Tenant audit log

## 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`](../ADMIN_WALKTHROUGH/workspaces/operations/analytics.md)
  (`AdminAuditLogExplorerPanel`).
- Sibling routes: [home.md](./home.md), [data.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.
