Tenant Admin Console · Surface walkthrough

Bulk data operations

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 /data · auth signed-in (tenant) + scope:tenant:admin:{tenantId} (or tenant:admin:*) · source apps/oshun/tenant-admin/src/app/data/page.tsx

Last walked.

Purpose#

Demonstrates the tenant's controlled bulk-data lifecycle — import dry-run, staged commit, selected-set bulk action gating, and xAPI export with a hashed manifest — each evaluated against tenant residency and audit requirements before execution. It is the data workspace (group: governance) in @oshun/navigation.

Entry points#

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

Layout regions#

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

  • Sidebar / Header: standard chrome (header crumb "Bulk data operations").
  • Main (<section data-testid="tenant-data-panel" data-tenant-id={...}>):
    • <h1>Bulk data operations</h1> + subtitle on residency/audit evaluation.
    • styles.summaryGrid of four tiles: Rows (dryRun.rowCount), Residency holds (dryRun.residencyViolationCount), Committed before hold (commit.committedRowIds.length), Bulk action (actionGate.verdict).
    • styles.table columns Operation / Evidence / Outcome with three rows:
      • Import dry-run{add} add, {update} update, {conflict} conflict; ready/blocked badge from dryRun.readyToCommit.
      • Staged commitresume from row {index|complete}; skipped {ids|none}; neutral badge {auditEventIds.length} audit events.
      • xAPI export — truncated manifestHashSha256 (or failed); manifested/error badge from exportOutcome.ok.

States#

  • Loading — N/A; synchronous server render from dryRunImport, planStagedCommit, gateBulkAction, executeBulkExport over hard-coded BulkRow[] seed (no fetch).
  • Empty — N/A; three seed rows (u-1 existing→update, u-2 add, u-3 EU-region residency hold) always produce output.
  • Populated (short) — always three operation rows. With the seed: u-3 triggers a residency hold (tenantResidencyRegion: 'us'), u-1 is a conflict/update (in existingExternalIds), planStagedCommit commits only region === 'us' rows.
  • Populated (long) — N/A; fixed dataset.
  • Error (recoverable) — N/A; no query. The xAPI export row models a failure path via exportOutcome.ok === false ("error" badge), but the seed produces a success.
  • Error (unrecoverable) — no custom handler; render throw → Next default.
  • Offline — N/A (server-rendered).
  • Gated — no session → redirect('/unauthorized?reason=missing-session&returnTo=/data'); middleware blocks unauthenticated requests upstream.
  • Standalone PWA — N/A; no manifest. (verify.)

Interactions#

  • Summary tiles (styles.summaryItem × 4) — display-only (Rows / Residency holds / Committed before hold / Bulk action verdict); not focusable, no telemetry.
  • Operation table rows (<tr> × 3: import dry-run, staged commit, xAPI export)
    • Function: render-only evidence + outcome badge per lifecycle stage. No "run import", "commit", or "download export" buttons exist in code — the page displays precomputed results only.
    • Keyboard: table is not interactive.
    • Screen reader: standard table semantics.
    • Touch target: N/A.
    • Mobile (≤ 640 px): workspace.module.css table styling.
    • Disabled when: N/A.
    • Offline behavior: N/A.
    • Telemetry: None.

Data & contracts#

  • Reads / pure-function calls (all from @oshun/tenant-console, all with hard-coded seed input, no BFF call):
    • dryRunImport({ opId, tenantId, resourceKind, rows, validators, existingExternalIds, tenantResidencyRegion, rowResidencyRegion }) → counts + readyToCommit.
    • planStagedCommit({ opId, rows, committable })committedRowIds, skippedRowIds, resumeFromRowIndex, auditEventIds.
    • gateBulkAction({ actionId, tenantId, resourceKind, selection, actionVerb, rationale, previewLimit, safetyLimit })verdict.
    • executeBulkExport({ opId, tenantId, resourceKind, format: 'xapi', records, residencyRegion, recordResidencyRegion, generatedAtUnixSeconds, hashSha256 }){ ok, result.manifest.manifestHashSha256 }. hashSha256 is the in-file FNV-1a deterministicHash.
  • Writes: None in the page (the lifecycle functions are pure; nothing is persisted).
  • Realtime: None.
  • Caching: SSR (dynamic; cookie read). Deterministic per tenant id.
  • Auth/role check: middleware.ts + page getTenantServerSession()canEnterTenantShell.

Cross-references#

  • Domain logic: libs/oshun/tenant-console/src/bulk-ops/index.ts (dryRunImport, planStagedCommit, gateBulkAction, executeBulkExport, BulkRow).
  • Workspace model: libs/oshun/navigation/src/tenant-ia.ts (data, group: governance).
  • Sibling routes: home.md, audit.md (export bundles reference the same audit stream).

Open questions / known gaps#

  • All rows/records are hard-coded seed data; no real import upload, commit trigger, or export download is wired. Document the eventual file-ingest + BFF persistence path.
  • deterministicHash is an FNV-1a stand-in for SHA-256 manifest hashing; confirm whether a real digest is required for production manifests.
  • Page is read-only (no action buttons); confirm the interactive workflow is a later phase.