V1 Web PWA · Surface walkthrough

Profile · Data rights

A per-surface walkthrough of the V1 Web PWA customer surface: layout, states, interactions, data, and cross-references.

walked
9sections6 minread

On this page

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.ts redirects 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 — left Profile · data, right rights, 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.all of three bffGet calls blocks render; no client skeleton from this page
  • Empty (consents fallback)consents.length === 0 → dashed EmptyRow (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 progressstatus === 'completed' → "Ready — " + "Download →" link when downloadUrl exists; 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: bffGet returns null on any failure; page falls back to empty arrays (quiet empty, no retry UI)
  • Offline — the real service worker treats /profile/data as an exact shell-navigation document, warms the last server-rendered HTML while online, and replays that document while navigator.onLine === false; the completed export and scheduled deletion rows remain visible without the generic offline-fallback card
  • Gated / anonymousproxy.ts redirects to /welcome before 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)
  • Download → (anchor, per completed export row)
    • Function: <a href={row.downloadUrl} download> — downloads the export artifact; only rendered when status === 'completed' and downloadUrl exists
    • 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 downloadUrl is remote and offline
    • Telemetry: none wired (verify)
  • ← 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 from customerConsentStateStore) → ConsentResponse { generatedAt, consents: [{ id, label, scope: 'analytics'|'research'|'third-party-models', state: 'opted-in'|'opted-out', updatedAtIso }] }
    • /v1/data-rights/exportsOshunCustomerDataExportSnapshotResponse { 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/deletionsOshunCustomerDataDeletionSnapshotResponse { 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 to status === 'scheduled'
  • Writes: none from this page. Per the file header, mutations live behind POST /v1/data-rights/exports and POST /v1/data-rights/deletions (not invoked here)
  • Realtime: none
  • Caching: bffGet default next.revalidate: 60 s; SW caches the rendered /profile/data document 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.ts protected-by-default; no-session → /welcome
  • Metadata: title: 'Profile · data rights', description re consent / export / deletion levers, alternates.canonical: '/profile/data'

Cross-references#

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-rights handoff, 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 no data-telemetry-event, data-analytics-event, or data-event hooks. The spec also runs a Chromium real-service-worker path: it warms /profile/data online from live BFF export/deletion rows, verifies the document is cached under the real SW, switches the browser offline, reloads /profile/data from the SW cache, and asserts navigator.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/consent is the authenticated projection of the canonical consent store and reflects a real research_study grant.

Open questions / known gaps#

  • No telemetry on the Download link or footer navigation: asserted in profile-data-readback.spec.ts against 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.