Context. surface customer · domain account · route /profile/data · auth signed-in · source apps/oshun/web/src/app/profile/data/page.tsx
Last walked. 2026-05-29 automated runtime walk (Playwright headless) — render, /v1 data (2xx), console/page-errors, expected content, screenshot verified; live screen-reader, touch, offline, and telemetry-delivery checks pending a manual AT pass. Evidence: WALKTHROUGH/results/runtime-sweep-2026-05-29.md
Purpose#
The privacy/DSAR surface — "the levers, in plain language." Renders three
read-only sections sourced from the real privacy BFF routes: which consents the
user has granted (/v1/data-rights/consent, a projection of
customerConsentStateStore), prior data exports (with download links), and any
deletion request in flight. The page explicitly states the actual
export/deletion mutations live behind two BFF POST endpoints
(/v1/data-rights/exports, /v1/data-rights/deletions) — this page is the read
surface; the write controls ship on /profile?path=data-rights.
Entry points#
- Cross-domain footer button from
/profile/notifications("Data rights") - Cross-domain footer button from
/profile/persona("Data rights") - "Back to profile" / sub-route navigation from
/profile - Direct URL / bookmark — yes; auth required (
proxy.tsredirects no-session to/welcome)
Layout regions#
page.tsx is an async server component wrapped in
<LWebShell top={<LCustomerNav active="library" />}>, constrained to
maxWidth: 880. Three Section components stack below the masthead.
- Header:
LCustomerNav(Lilith customer nav,active="library"; Lilith wordmark, Today/Explore/Activity/Library tabs, presentational ⌕ Search / ⌘K / Ask Lilith + avatar) - Masthead:
LMasthead— leftProfile · data, rightrights, kicker "The levers, in plain language", italic title "Your data rights." - Intro:
LEyebrow"How this opens" + "Three sections, no surprises." - Main: three
Sections (data-profile-data-section="consents" | "exports" | "deletions") — "Consents", "Exports", "Deletion requests" - Footer / actions: a row of four
LBtns — "Open data request controls" (primary,/profile?path=data-rights), "← Back to profile" (ghost), "Notifications" (ghost), "Open Memory controls" (ghost). Each footer action is forced to a 44 px high tap target on this route.
States#
- Loading — server
Promise.allof threebffGetcalls blocks render; no client skeleton from this page - Empty (consents fallback) —
consents.length === 0→ dashedEmptyRow(data-profile-data-empty="consents") "No consent state available right now." In healthy real dev infra this does not appear because the consent-store projection seeds three rows. - Empty (exports) — no export requests →
EmptyRow"You have never requested an export. Open the data request controls to start one." (data-profile-data-empty="exports") - Empty (deletions) — no pending requests →
EmptyRow(data-profile-data-empty="deletions") "Nothing in flight. Deletion happens with a thirty-day quiet window." - Populated (short) — real BFF consent rows, one completed export row, and one scheduled deletion row render in the stacked grid
- Populated (long) — export history is capped by the BFF at 50 rows per
user (
MAX_REQUESTS_PER_USER); Playwright seeds 52 completed exports, verifies 50 render, verifies the newest and oldest-kept rows remain, and verifies the two evicted FIFO rows are absent. No pagination or virtualization is rendered. - Export ready vs in progress —
status === 'completed'→ "Ready —" + "Download →" link when downloadUrlexists; pending / in-progress states show "In progress — OSHUN will write when it lands." - Scheduled deletion — accent-bordered row with requested date, scheduled effective date, scope/category count, grace period, and cancellation handoff to the data request controls
- Error (recoverable) — N/A:
bffGetreturnsnullon any failure; page falls back to empty arrays (quiet empty, no retry UI) - Offline — the real service worker treats
/profile/dataas an exact shell-navigation document, warms the last server-rendered HTML while online, and replays that document whilenavigator.onLine === false; the completed export and scheduled deletion rows remain visible without the generic offline-fallback card - Gated / anonymous —
proxy.tsredirects to/welcomebefore render - Standalone PWA — 390 px standalone launch renders the live BFF-backed read surface cleanly with display-mode asserted, consent/export/deletion rows visible, 44 px controls, and no horizontal overflow
Interactions#
This page renders no write controls (consents/exports/deletions are all static reads). The only interactive elements are the per-export download link and the footer navigation buttons.
- Open data request controls (LBtn primary, href
/profile?path=data-rights)- Function: navigates to the canonical export/deletion write surface in
ProfileSettingsPanel - Keyboard: in document order before the footer sibling links; Enter activates
- Screen reader: announces button text; rendered as an
<a>(LBtn with href)
- Function: navigates to the canonical export/deletion write surface in
- Download → (anchor, per completed export row)
- Function:
<a href={row.downloadUrl} download>— downloads the export artifact; only rendered whenstatus === 'completed'anddownloadUrlexists - Keyboard: in document order within its export row; Enter activates
- Screen reader:
aria-label="Download export requested <date>" - Touch target: ≥ 44 px tap height on mobile route coverage
- Mobile (≤ 640 px): stacks within the export card
- Offline behavior: download fails if
downloadUrlis remote and offline - Telemetry: none wired (verify)
- Function:
- ← Back to profile (LBtn ghost, href
/profile)- Function: navigates to the account hub
- Screen reader: announces button text; rendered as an
<a>(LBtn with href)
- Notifications (LBtn ghost, href
/profile/notifications)- Function: navigates to channel/cadence prefs
- Open Memory controls (LBtn ghost, href
/profile/memory)- Function: navigates to the Memory controls surface
Data & contracts#
- Reads (all via
bffGet, server-side, session cookie forwarded):/v1/data-rights/consent(apps/oshun/bff/src/routes/consent.ts, projected fromcustomerConsentStateStore) →ConsentResponse { generatedAt, consents: [{ id, label, scope: 'analytics'|'research'|'third-party-models', state: 'opted-in'|'opted-out', updatedAtIso }] }/v1/data-rights/exports→OshunCustomerDataExportSnapshotResponse { generatedAt, userId, revision, availableCategories, availableFormats, requests: [{ exportId, format, includeMetadata, categories, artifactTypes, status, requestedAt, startedAt, completedAt, cancelledAt, expiresAt, downloadUrl, bundleSizeBytes, integrityManifest, consentContext, failureReason }] }/v1/data-rights/deletions→OshunCustomerDataDeletionSnapshotResponse { generatedAt, userId, revision, gracePeriodDays, validConfirmationTokens, availableCategories, requests: [{ deletionId, mode, status, scope, categories, artifacts, derivedArtifactJobs, tombstones, reason, confirmationToken, requestedAt, scheduledFor, cancelDeadlineAt, completedAt, cancelledAt, failureReason }] }— page filters tostatus === 'scheduled'
- Writes: none from this page. Per the file header, mutations live behind
POST
/v1/data-rights/exportsand POST/v1/data-rights/deletions(not invoked here) - Realtime: none
- Caching:
bffGetdefaultnext.revalidate: 60s; SW caches the rendered/profile/datadocument after an online visit and replays that same document while offline. Auth/session and non-Nisaba/v1/*reads remain network-only; this route's offline support is document replay, not API response caching. - Auth/role check:
proxy.tsprotected-by-default; no-session →/welcome - Metadata:
title: 'Profile · data rights', description re consent / export / deletion levers,alternates.canonical: '/profile/data'
Cross-references#
- Feature spec:
V1/features.md#account - Architecture:
V1/ARCHITECTURE.md#account - Journeys:
../../journeys/account-deletion-and-dsar.md - Sibling routes:
- Component sources:
apps/oshun/web/src/app/profile/data/page.tsxapps/oshun/web/src/design-system/lilith/shells.tsx(LWebShell,LCustomerNav,LMasthead)apps/oshun/web/src/lib/server/bff-fetch.ts(bffGet)
E2E coverage#
apps/oshun/web/e2e/profile-data-readback.spec.ts— authenticates a customer, verifies the real consent-store projection (including a research consent grant reflected on the page), verifies empty export/deletion readback and the/profile?path=data-rightshandoff, seeds a completed export and scheduled deletion through real BFF POSTs, verifies the server-rendered snapshots with status hooks/truncated manifest hash/download link/category count/grace-period copy, proves anonymous redirect gating, seeds 52 completed exports to verify the real 50-row history cap, checks mobile 44 px touch targets/no horizontal overflow with the shared axe gate, and asserts the completed export download link plus all four footer navigation controls expose nodata-telemetry-event,data-analytics-event, ordata-eventhooks. The spec also runs a Chromium real-service-worker path: it warms/profile/dataonline from live BFF export/deletion rows, verifies the document is cached under the real SW, switches the browser offline, reloads/profile/datafrom the SW cache, and assertsnavigator.onLine === false, the same rows remain visible, and the generic offline-fallback document is not used. A separate mobile standalone PWA launch asserts(display-mode: standalone), live BFF-backed consent/export/deletion rows, 44 px controls, and no horizontal overflow.apps/oshun/bff/src/__tests__/consent-route.test.ts— verifies/v1/data-rights/consentis the authenticated projection of the canonical consent store and reflects a realresearch_studygrant.
Open questions / known gaps#
- No telemetry on the Download link or footer navigation: asserted in
profile-data-readback.spec.tsagainst the completed export download link and all four footer action links. - Offline/PWA replay for this server-rendered account sub-route is covered
by a dedicated real-service-worker run in
profile-data-readback.spec.ts.