V1 Web PWA · Surface walkthrough

Profile · Memory Controls

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

walked
8sections8 minread

On this page

Context. surface customer · domain account · route /profile/memory · auth signed-in · source apps/oshun/web/src/app/profile/memory/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; content re-verified 2026-06-03 against current source; real Iris empty + populated BFF hydration re-verified 2026-06-25 by memory-edit-pause-forget.spec.ts; persisted pause + assistant dock memory-off handoff re-verified 2026-06-27; anonymous redirect and offline cached/read write-block path re-verified 2026-06-27; transient BFF review failure + retry re-verified 2026-06-30; real Iris single-fact hard-delete through /v1/iris/adapter/memory/:memoryId plus review read-back absence re-verified 2026-06-30; cross-tab edit-conflict keep-remote and overwrite resolution re-verified 2026-06-30; synced Iris export receipt through /v1/iris/adapter/export re-verified 2026-06-30

Purpose#

OSHUN assistant memory controls — review, edit, export, pause, and delete what the assistant remembers. Thin server component that mounts the ProfileMemoryControls client composition.

Entry points#

  • /profile sub-section deep link (highlightPath = memory)
  • Profile settings panel "Memory" row → /profile/memory
  • Privacy-related links from /legal/privacy (assistant memory section) — verify
  • Direct URL / bookmark — yes (auth required)
  • Onboarding "Memory" step (/onboarding?step=memory) hand-off — verify

Layout regions#

page.tsx is <ProfileMemoryControls /> with no other chrome from the route. The component composition is in apps/oshun/web/src/app/profile/memory/ProfileMemoryControls.tsx — a 'use client' view that starts from the honest empty local state, adopts any cached local control state after hydration, then replaces it with the member's real Iris review from the BFF when /v1/iris/adapter/review returns records. The pause state is also hydrated from /v1/iris/adapter/pause?role=settings and profile pause/resume writes are persisted back through that BFF endpoint. Regions:

  • Header<h1> "What OSHUN remembers", the off-the-record pause switch, and Export JSON
  • Alert banners — off-the-record banner, full-clear 7-day grace banner, and transient Iris BFF read-unavailable retry banner (conditional)
  • Status grid — State / Active facts / Audit events / Tombstones metrics
  • Workspace — left: the paginated Memory facts table with category / scope filters and scope tabs; right: the per-fact inspector (detail, provenance, recent recalls, edit, scope-change, fact controls, step-up, hard delete, full clear)
  • Controls grid — Category controls, Scope controls, Export bundle
  • Audit chain + footer toast / reset

States#

  • Initial hydration / loading — there is no skeleton; the route renders the deterministic empty state first, then shows the sync notice once real Iris records replace it
  • Empty — no stored memories; ledger row data-profile-memory-empty-state says "No remembered facts yet." and the inspector shows data-profile-memory-empty-inspector
  • Populated — list of real Iris or cached memory entries (scope, category, origin, content, source, timestamp)
  • Paused — memory capture toggle off; existing entries still visible
  • Editing — selected entry exposes label/content edit controls
  • Exporting / export ready — synced Iris exports first request a server-side export receipt from /v1/iris/adapter/export?role=settings, then show a non-secret receipt plus the inline signed JSON preview and download link. Local-only cached exports show the same signed bundle with a local-only receipt note.
  • Deleting (single) — destructive confirmation + step-up phrase
  • Deleting (all) — full-clear confirmation + step-up + 7-day grace banner
  • Error (recoverable) — a failed Iris review/consent hydration keeps cached/local memory rows visible, exposes data-profile-memory-bff-status with retry copy, and the Retry Iris read action re-runs the real BFF hydration path
  • Anonymous user — middleware redirects direct /profile/memory visits to /welcome?redirect=/profile/memory; the memory surface does not mount
  • Offline — cached memory entries remain readable; signed-in BFF-backed pause writes fail closed with "Off-the-record mode could not sync. Try again.", the switch remains off, and no pause banner is shown

Interactions#

All interactions live inside ProfileMemoryControls (a single 'use client' view). Heading: <h1> "What OSHUN remembers". The real affordances (with their data-profile-memory-* test ids):

  • Off the record / Resume memory (role="switch", data-profile-memory-pause) — toggles capture-pause; raises an "Off the record" role="alert" banner (data-profile-memory-pause-banner) with its own Resume button. For signed-in real Iris users this writes PATCH /v1/iris/adapter/pause?role=settings, exposes data-profile-memory-pause-sync, and notifies the shell assistant dock so its memory disclosure switches to off.
  • Export JSON (data-profile-memory-export) — see Export panel below

Status grid#

  • Status metrics (non-interactive) — State, Active facts, Audit events, Tombstones

Memory facts table#

  • Category filter / Scope filter (<select>s) — filter the fact rows by category / scope
  • Scope tabs (role="tablist", data-profile-memory-scope-tabs) — "All scopes" + one tab per scope present, each with a live count
  • Fact rows (data-profile-memory-fact-row) — paginated table (Fact / Category / Scope / Origin / Last used / Status / Actions); selecting a row drives the inspector; per-row Edit / Clear / Opt out icon buttons
  • Empty ledger (data-profile-memory-empty-state) — rendered when pagedFacts.length === 0; no fabricated member names or demo facts
  • Pagination (data-profile-memory-pagination) — Previous / Next over FORGET_PAGE_SIZE (10) rows per page

Inspector (selected fact)#

  • Empty inspector (data-profile-memory-empty-inspector) — rendered when no selected fact exists
  • Detail — body, scope / category / origin / suppression / source / downstream-use metadata, and a "View audit chain →" jump link
  • Provenance chain (data-profile-memory-provenance-chain) — created + each revision + "Edited from" predecessor
  • Recent recalls (data-profile-memory-recent-recalls) — last surfaces this fact was recalled on
  • Edit fact (data-profile-memory-save-edit) — label + content inputs, optimistic-concurrency check; on conflict an edit-conflict panel (data-profile-memory-edit-conflict) offers Keep remote version / Save my edit as the next revision
  • Move to a different scope (data-profile-memory-scope-change-panel) — target-scope <select> + Move scope; rejects tenant→consumer promotion
  • Fact controlsOpt out this fact / Clear this fact. When the selected row came from a synced Iris review, Clear this fact first calls DELETE /v1/iris/adapter/memory/:memoryId?role=settings&mode=soft; local cached rows still use the local control-state reducer.
  • Step-up authentication (data-profile-memory-step-up-panel) — challenge-phrase input gating destructive flows; shows active/missing token status
  • Hard delete (data-profile-memory-hard-delete) — irreversible delete, requires the hard-delete confirmation phrase + a valid step-up token. For synced Iris rows, the page first calls DELETE /v1/iris/adapter/memory/:memoryId?role=settings&mode=hard; the real BFF review read-back no longer returns that memory.
  • Forget everything (7-day grace) (data-profile-memory-full-clear-panel) — full-clear confirmation phrase + step-up; schedules a clear with a 7-day grace banner (data-profile-memory-grace-banner) carrying a Cancel clear action

Category / scope controls#

  • Category controls (data-profile-memory-category-controls) — per category: Opt out / Clear (clear requires step-up)
  • Scope controls (data-profile-memory-scope-controls) — per scope: Opt out / Clear (clear requires step-up)

Export panel#

  • Export bundle (data-profile-memory-export-panel) — optional DSAR request ID input flags the bundle as DSAR fulfilment; Build export / Sign DSAR bundle produces a signed JSON (facts, consents, opt-outs, tombstones, downstream uses, audit chain) with a download link + preview. When the page is synced from real Iris, the same action first posts to /v1/iris/adapter/export?role=settings and renders data-profile-memory-export-receipt with the export id, memory count, and checksum.
  • Audit chain (#audit-chain, data-profile-memory-audit-panel) — last memory mutations, appended and signed in the export bundle
  • Reset local memory controls (footer) — re-seeds the local state

Data & contracts#

  • Reads: hybrid client cache + real BFF hydration. ProfileMemoryControls starts with buildInitialProfileMemoryState() from @oshun/memory-iris, adopts localStorage[PROFILE_MEMORY_STORAGE_KEY] after hydration, then calls fetchRealProfileMemoryStateResult(realUserId) once useOshunProfileStore() has a user id. That helper reads /v1/iris/adapter/review?role=settings&pageSize=100 and /v1/iris/adapter/consents?role=settings through the shared api client. Non-empty BFF reviews replace the local state and show "Memory controls synced with your Iris record"; empty BFF reviews return { status: 'empty' } so a member's cached local facts are not erased by an empty read. Unavailable BFF reads return { status: 'unavailable' }, leave cached controls visible, and render the retry banner instead of silently falling back.
  • Pause read/write: fetchRealProfileMemoryPauseState('settings') hydrates the persisted pause record even when review is empty. The signed-in pause toggle and banner Resume action call PATCH /v1/iris/adapter/pause?role=settings; the response is mirrored into local control state and dispatches oshun:iris-memory-pause-changed for the shell assistant dock.
  • Other writes: single-fact clear and hard-delete now call the real Iris delete endpoint first when the page is synced from BFF review, then mirror the existing local control-state mutation for the current view. Synced exports call the real Iris export endpoint and render the returned receipt before the local signed bundle is exposed. Edit / opt-out / scope change / category clear / scope clear / full-clear still return a new IrisUserMemoryControlState from a @oshun/memory-iris pure function and persist to localStorage; those mutations do not yet POST back to Iris.
  • Realtime: same-shell pause changes update the assistant dock via the pause-changed event; focus/visibility refreshes read the persisted BFF pause. Other server-persisted memory edits do not stream in real time, but open /profile/memory tabs listen for same-origin localStorage changes so a local edit in one tab can refresh another tab and trigger the optimistic edit-conflict panel instead of silently resetting a dirty draft.
  • Caching: localStorage (PROFILE_MEMORY_STORAGE_KEY) for the control state; no SWR, no server cache. The offline route-level E2E keeps seeded cached rows visible while navigator.onLine === false and asserts that the signed-in pause mutation is blocked rather than queued locally when the BFF cannot be reached.
  • Auth/role check: signed-in; middleware enforces
  • Metadata: title: 'Memory Controls', description: 'Review, edit, export, pause, and delete OSHUN assistant memory.', robots: { index: false, follow: false }, alternates.canonical: '/profile/memory'

Cross-references#

Open questions / known gaps#

  • Single-fact clear / hard-delete are wired to the real Iris delete endpoint when the page is synced from BFF review; memory-edit-pause-forget.spec.ts hard-deletes a remembered profile fact from /profile/memory and proves /v1/iris/adapter/review no longer returns the deleted content.
  • Writes remain local-only for edit, opt-out, scope change, category/scope clear, and full-clear; pause/resume, single-fact delete, and synced export receipt creation are the controls currently wired to BFF Iris mutation endpoints.
  • Failed BFF hydration keeps cached/local state, exposes a visible retry/status banner, and retries through the real BFF hydration path in memory-edit-pause-forget.spec.ts.
  • Confirm whether memory pause affects ongoing assistant sessions in real time: same-shell pause/resume dispatches update the assistant dock to memory scope off, and focus/visibility refreshes re-read the persisted BFF pause endpoint.
  • Confirm the DSAR-backed export/delete path for this exact UI; synced export now records an Iris export receipt, but the optional DSAR request id is still only signed into the local JSON bundle and is not yet persisted as an IrisDataRightsRequest.
  • Anonymous redirect and offline cached/write-blocked behavior are covered at route level in memory-edit-pause-forget.spec.ts.