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.summaryGridof four tiles: Rows (dryRun.rowCount), Residency holds (dryRun.residencyViolationCount), Committed before hold (commit.committedRowIds.length), Bulk action (actionGate.verdict).styles.tablecolumns Operation / Evidence / Outcome with three rows:- Import dry-run —
{add} add, {update} update, {conflict} conflict;ready/blockedbadge fromdryRun.readyToCommit. - Staged commit —
resume from row {index|complete}; skipped {ids|none}; neutral badge{auditEventIds.length} audit events. - xAPI export — truncated
manifestHashSha256(orfailed);manifested/errorbadge fromexportOutcome.ok.
- Import dry-run —
States#
- Loading — N/A; synchronous server render from
dryRunImport,planStagedCommit,gateBulkAction,executeBulkExportover hard-codedBulkRow[]seed (no fetch). - Empty — N/A; three seed rows (
u-1existing→update,u-2add,u-3EU-region residency hold) always produce output. - Populated (short) — always three operation rows. With the seed:
u-3triggers a residency hold (tenantResidencyRegion: 'us'),u-1is a conflict/update (inexistingExternalIds),planStagedCommitcommits onlyregion === '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.csstable 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 }.hashSha256is the in-file FNV-1adeterministicHash.
- 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+ pagegetTenantServerSession()→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.
-
deterministicHashis 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.