V1 Web PWA · Surface walkthrough

Arete · the Living Offering

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

walked
8sections9 minread

On this page

Context. surface customer · domain arete · route /arete/offering · auth signed-in · source apps/oshun/web/src/app/arete/offering/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; save/send recoverable 5xx failure and retry re-walked against live BFF 2026-06-27; score selection, material reorder, recipient/occasion selection, dynamic masthead summary, and scene preview copy re-walked in browser 2026-06-27; cue-privacy promise reveal and BFF-backed C2PA receipt envelope re-walked 2026-06-27; offering-surface coach framework preview to real BFF plus forced 503/retry re-walked 2026-06-27

Purpose#

"A 4–8 minute composition for one person — intention, scene, score, sharing, cue-privacy promise." A composition surface for assembling photos, voice notes, and a quiet score into a short personal gift, with an explicit cue-privacy promise about working-copy destruction. Renders <AreteOffering data={await getAreteOffering()} />.

Entry points#

  • Direct URL / bookmark — yes (signed-in)
  • From /atelier/* — atelier composition tools likely reference offering (no explicit link verified in source)
  • From notification — "your offering is ready to send" deep link
  • From a chest / library surface — described in the privacy section as "all from your chest" (no explicit link in this page)

Layout regions#

page.tsx is a thin server component: await getAreteOffering()<AreteOffering>. Component lives at apps/oshun/web/src/components/lilith/arete.tsx line 1794 — an LWebShell with LCustomerNav active="today".

  • Header: LCustomerNav active="today"
  • Masthead (AreteOfferingWorkspace): left "Arete · the living offering", mid [data-arete-offering-summary] seeded as "renata's offering · for J · 14 may" and updated from the live recipient/occasion inputs, right "6 min · private until you choose", big "An intention, composed into a small scene.", lede about the 4–8 minute composition concept
  • Main, left (1.4fr):
    • I · The intention — accent-bordered paper card with a real textarea ([data-arete-offering-intention-input]) seeded with the current intention prose
    • I.B · Coach lens — compact framework selector (general / cbt / act / stoic / reflective) with [data-arete-offering-coach-framework-option], plus "Preview coach score" calling the real coach BFF for the current intention
    • II · The scene · preview — aspectRatio: '16/9' simulated video card with gradient backdrop, [data-arete-offering-scene-label] derived from sceneTimeline[0], [data-arete-offering-scene-preview-line] derived from the current intention prose, and hardcoded play button glyph "▶"
    • Scene timeline strip — data.sceneTimeline rows (numbered scenes, with labels), waveform SVG, scrub head indicator at 12%
    • III · Score — 3-column grid of data.scoreOptions button cards (title + sub + selected chip); client state is seeded from the fixture's selected option and click updates the active score
  • Aside, right (1fr):
    • IV · Materials — 3x3 grid of color-swatch buttons driven by data.toneSwatch (9 cells, cell 5 initially selected with paper-color inset border); click selects a swatch, drag/drop or Move earlier / Move later reorders the selected swatch locally; caption "9 of 47 photos and 3 of 12 voice notes selected. Drag or move to reorder."
    • V · Sharing — editable recipient and occasion inputs seeded from data.recipient / data.occasionDate, "watch once · on a single device", then data.sharingRules rows (k/v pairs)
    • Cue-privacy promise card — accent-bordered paper card; "Read the full promise" reveals the full cue-privacy detail, and "See the C2PA receipt" POSTs the current recipient/occasion/intention/score to the BFF-backed receipt envelope without echoing raw intention prose
    • Footer action row: "Save draft" (ghost lg) + "Send to {recipient} · {occasion}" (primary lg), fed by the current sharing controls and guarded by the Lilith safety-assessment preflight

States#

  • Loading — server-awaited; no loading.tsx at this route
  • Populated (default fixture) — scene timeline, 3 score options, 9 swatch cells, sharing rules, action footer all render
  • Scene timeline empty — empty sceneTimeline would render no scene blocks (only waveform); no empty-state coded
  • Score selected — score buttons expose aria-pressed and [data-arete-offering-score-selected]; initial state is seeded from the selected fixture option with a first-option fallback, and click updates the active chip locally
  • Material reorder — swatch tiles expose selected state and color attrs; Move earlier / Move later and drag/drop reorder the selected material locally
  • Editable intention — the default prose renders in a textarea; typed prose updates the scene preview line, is sent with Save draft / Send, and is returned as a private member-scoped intentionPreview
  • Coach score preview — the framework selector starts on reflective, can switch to stoic / cbt / act / general, and "Preview coach score" POSTs { framework, userMessage } to /v1/arete/coach/responses; a 2xx response renders [data-arete-offering-coach-response] plus suggestions, while a 5xx renders [data-arete-offering-coach-error] with retry enabled
  • Different recipient / occasion — Section V exposes real recipient and occasion inputs; changes update [data-arete-offering-summary], [data-arete-offering-actions], the send button label, the /v1/arete/offerings/keep POST body, /sent readback, and the /arete/offerings gallery row
  • Cue-privacy receipt — "See the C2PA receipt" POSTs to /v1/arete/offerings/receipt, renders [data-arete-offering-receipt-state="receipt-issued"], C2PA manifest id, cue-privacy hash, and private-intent hash, and does not render raw intention prose in the receipt panel
  • Route render error (recoverable) — server-awaited route failures rely on the parent error boundary
  • Save/send write error (recoverable)AreteOfferingActions catches a non-2xx /v1/arete/offerings/keep response, renders [data-arete-offering-error][role="alert"], leaves the action controls enabled, and can retry the same browser-entered intention into a live BFF draft-kept or delivered response
  • Crisis pre-screen blocked — before keep/send, AreteOfferingActions calls /v1/lilith/adapter/safety-assessment?role=safety; a blocking response replaces the action buttons with [data-arete-offering-safety-interrupt], 988 crisis-resource copy, and a /profile/safety link, with no /v1/arete/offerings/keep write
  • Safety preflight error (fail closed) — a non-2xx safety-assessment response renders [data-arete-offering-error][role="alert"], leaves the action controls enabled, and still makes no /v1/arete/offerings/keep write
  • Offline — no explicit handling
  • Gated — N/A; metadata does not mark robots no-index
  • Standalone PWA — renders cleanly within LWebShell

Interactions#

Scene preview card#

  • Play glyph "▶" — static centered circular glyph
    • Function: no onClick — purely visual placeholder
    • Screen reader: no aria-label on the glyph
  • Preview label / line[data-arete-offering-scene-label] is derived from the first scene timeline label and [data-arete-offering-scene-preview-line] updates from the current intention textarea

Scene timeline#

  • Scene block (one per data.sceneTimeline)
    • Function: numbered+labelled timeline block; static
    • Function: scrub indicator at 12% is hardcoded, not interactive

Score option button (one per data.scoreOptions)#

  • Score button — title + sub + visible "● selected" chip on the active option
    • Function: click sets the active score; root grid exposes [data-arete-offering-score-selected]
    • Screen reader: aria-pressed reflects the active score

Coach preview panel#

  • Framework selector — five segmented buttons expose [data-arete-offering-coach-framework-option] and aria-pressed
    • Function: click updates [data-arete-offering-coach-panel][data-arete-offering-coach-framework] and clears stale preview/error state
  • Preview coach score — button calls the real BFF /v1/arete/coach/responses with the selected framework and current intention textarea value
    • Function: successful responses render [data-arete-offering-coach-response] with [data-arete-offering-coach-response-framework] and one chip per [data-arete-offering-coach-suggestion]
    • Recoverable error: non-2xx clears stale coach copy, renders [data-arete-offering-coach-error], and keeps the preview button enabled for retry

Materials swatches (3x3)#

  • Swatch tile — colored button with index "01"…"09" label; the selected tile has a paper-color inset border and aria-pressed
    • Function: click selects a tile; drag/drop reorders a dragged tile; Move earlier / Move later moves the currently selected tile and updates [data-arete-offering-material-selected]
    • Current scope: local composition state only; no persisted material order

Sharing controls#

  • Recipient input — controlled text field seeded from data.recipient
    • Function: updates [data-arete-offering-summary], [data-arete-offering-recipient-value], and the BFF-backed action island's data-arete-offering-action-recipient
  • Occasion input — controlled text field seeded from data.occasionDate
    • Function: updates [data-arete-offering-summary], [data-arete-offering-occasion-value], and the BFF-backed action island's data-arete-offering-action-occasion
    • Current scope: natural-language occasion text, not an ISO date picker
  • "Read the full promise" (ghost button)
    • Function: toggles [data-arete-offering-promise-detail] with the explicit private-intention, no-training, source-cue redaction promise
  • "See the C2PA receipt" (ghost button)
    • Function: POSTs /v1/arete/offerings/receipt with the current { recipient, occasion, intention, score }; UI renders receipt id, C2PA manifest id, cue-privacy hash, private-intent hash, and selected score
    • Privacy: BFF hashes the private intention and the UI does not render the raw intention prose in the receipt panel
  • "Save draft" (button inside AreteOfferingActions)
    • Function: POSTs /v1/lilith/adapter/safety-assessment?role=safety with the current intention first; clean assessments then POST /v1/arete/offerings/keep with { action: 'draft', recipient, occasion, intention }; UI renders data-arete-offering-state="draft-kept" and the private intention preview
    • Crisis path: renders [data-arete-offering-safety-interrupt] plus support resources and no keep write
    • Recoverable error: a non-2xx keep response renders [data-arete-offering-error] and leaves the button enabled for a real BFF retry
  • "Send to {recipient} · {occasion}" (button inside AreteOfferingActions)
    • Function: POSTs /v1/lilith/adapter/safety-assessment?role=safety with the current intention first; clean assessments then POST /v1/arete/offerings/keep with { action: 'send', recipient, occasion, intention }; UI renders data-arete-offering-state="delivered" and the private intention preview
    • Crisis path: renders [data-arete-offering-safety-interrupt] plus support resources and no keep write
    • Recoverable error: a non-2xx keep response renders [data-arete-offering-error] and leaves the button enabled for a real BFF retry

Data & contracts#

  • Reads: getAreteOffering() from @/lib/lilith-data/arete-depth — returns AreteOfferingData ({ sceneTimeline, scoreOptions, toneSwatch, sharingRules }). BFF mapping comment: getAreteOffering ← /arete/offerings/:id.
  • Writes: AreteOfferingWorkspace calls POST /v1/arete/coach/responses from the coach-preview panel with { framework, userMessage }, and renders only the returned framework-scoped copy/suggestions or an honest recoverable error. AreteOfferingActions calls POST /v1/lilith/adapter/safety-assessment?role=safety before keep/send; a blocking response halts in the action-region safety interruption. Clean assessments continue to POST /v1/arete/offerings/keep; the BFF stores the keep/send decision and a trimmed intentionPreview under the verified member id. Member-scoped read back is available through GET /v1/arete/offerings/sent. AreteOfferingWorkspace also calls POST /v1/arete/offerings/receipt for the cue-privacy/C2PA receipt envelope; it is member-authenticated and returns hashes/ids rather than raw intent.
  • Realtime: none
  • Caching: server fetch, default Next.js cache
  • Auth/role check: app-level middleware

Cross-references#

  • Domain hub: arete.md
  • Atelier (composition tools): apps/oshun/web/src/app/atelier/* — see customer/11-atelier/
  • Related: C2PA provenance receipts; this surface now has a member-scoped receipt envelope, while Arete-specific binary media signing still belongs to the render pipeline
  • Component source: apps/oshun/web/src/components/lilith/arete.tsx (line 1794, AreteOffering)
  • Data: apps/oshun/web/src/lib/lilith-data/arete-depth.ts (getAreteOfferingFixture)
  • Feature spec: V1/features.md

Open questions / known gaps#

  • Save draft and Send now have BFF-backed handlers through AreteOfferingActions
  • Read the full promise and See the C2PA receipt now have handlers
  • Section V recipient and occasion are editable, drive the send label, and persist through the live BFF keep/send path
  • Masthead summary and scene preview copy are now dynamic: [data-arete-offering-summary] follows edited recipient/occasion, [data-arete-offering-scene-label] is derived from sceneTimeline[0], and [data-arete-offering-scene-preview-line] follows the intention textarea
  • Score selection is no longer hardcoded by index; the client control is seeded from the selected scoreOptions field and updates on click
  • Material swatches now have tile selection plus drag/drop and Move earlier / Move later reorder controls
  • Keep/send now pre-screen the intention through Lilith and block a crisis-like intention before the Arete offering write
  • /arete/offering now has a direct coach framework preview into the real /v1/arete/coach/responses BFF route; the user no longer needs to leave the offering surface to score the current intention through Stoic, CBT, ACT, Reflective, or General lenses
  • How does this surface tie into Atelier / Iris (rendering pipeline) and Aja (cue-privacy promise / C2PA)? /v1/arete/offerings/receipt now backs the receipt envelope, but AreteOffering still does not invoke the binary render/signing pipeline
  • The cue-privacy promise text claims "the working copy is destroyed"; the receipt now records this policy, but the actual renderer still needs an Arete-specific destruction/signing audit