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 fromsceneTimeline[0],[data-arete-offering-scene-preview-line]derived from the current intention prose, and hardcoded play button glyph "▶" - Scene timeline strip —
data.sceneTimelinerows (numbered scenes, with labels), waveform SVG, scrub head indicator at 12% - III · Score — 3-column grid of
data.scoreOptionsbutton cards (title + sub + selected chip); client state is seeded from the fixture's selected option and click updates the active score
- I · The intention — accent-bordered paper card with a real textarea
(
- 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", thendata.sharingRulesrows (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
- IV · Materials — 3x3 grid of color-swatch buttons driven by
States#
- Loading — server-awaited; no
loading.tsxat this route - Populated (default fixture) — scene timeline, 3 score options, 9 swatch cells, sharing rules, action footer all render
- Scene timeline empty — empty
sceneTimelinewould render no scene blocks (only waveform); no empty-state coded - Score selected — score buttons expose
aria-pressedand[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 tostoic / 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/keepPOST body,/sentreadback, and the/arete/offeringsgallery 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) —
AreteOfferingActionscatches a non-2xx/v1/arete/offerings/keepresponse, renders[data-arete-offering-error][role="alert"], leaves the action controls enabled, and can retry the same browser-entered intention into a live BFFdraft-keptordeliveredresponse - Crisis pre-screen blocked — before keep/send,
AreteOfferingActionscalls/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/safetylink, with no/v1/arete/offerings/keepwrite - 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/keepwrite - 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-labelon the glyph
- Function: no
- 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-pressedreflects the active score
- Function: click sets the active score; root grid exposes
Coach preview panel#
- Framework selector — five segmented buttons expose
[data-arete-offering-coach-framework-option]andaria-pressed- Function: click updates
[data-arete-offering-coach-panel][data-arete-offering-coach-framework]and clears stale preview/error state
- Function: click updates
- Preview coach score — button calls the real BFF
/v1/arete/coach/responseswith 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
- Function: successful responses render
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
- Function: click selects a tile; drag/drop reorders a dragged tile; Move
earlier / Move later moves the currently selected tile and updates
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'sdata-arete-offering-action-recipient
- Function: updates
- 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'sdata-arete-offering-action-occasion - Current scope: natural-language occasion text, not an ISO date picker
- Function: updates
Privacy / footer actions#
- "Read the full promise" (ghost button)
- Function: toggles
[data-arete-offering-promise-detail]with the explicit private-intention, no-training, source-cue redaction promise
- Function: toggles
- "See the C2PA receipt" (ghost button)
- Function: POSTs
/v1/arete/offerings/receiptwith 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
- Function: POSTs
- "Save draft" (button inside
AreteOfferingActions)- Function: POSTs
/v1/lilith/adapter/safety-assessment?role=safetywith the current intention first; clean assessments then POST/v1/arete/offerings/keepwith{ action: 'draft', recipient, occasion, intention }; UI rendersdata-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
- Function: POSTs
- "Send to {recipient} · {occasion}" (button inside
AreteOfferingActions)- Function: POSTs
/v1/lilith/adapter/safety-assessment?role=safetywith the current intention first; clean assessments then POST/v1/arete/offerings/keepwith{ action: 'send', recipient, occasion, intention }; UI rendersdata-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
- Function: POSTs
Data & contracts#
- Reads:
getAreteOffering()from@/lib/lilith-data/arete-depth— returnsAreteOfferingData({ sceneTimeline, scoreOptions, toneSwatch, sharingRules }). BFF mapping comment:getAreteOffering ← /arete/offerings/:id. - Writes:
AreteOfferingWorkspacecallsPOST /v1/arete/coach/responsesfrom the coach-preview panel with{ framework, userMessage }, and renders only the returned framework-scoped copy/suggestions or an honest recoverable error.AreteOfferingActionscallsPOST /v1/lilith/adapter/safety-assessment?role=safetybefore keep/send; a blocking response halts in the action-region safety interruption. Clean assessments continue toPOST /v1/arete/offerings/keep; the BFF stores the keep/send decision and a trimmedintentionPreviewunder the verified member id. Member-scoped read back is available throughGET /v1/arete/offerings/sent.AreteOfferingWorkspacealso callsPOST /v1/arete/offerings/receiptfor 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/*— seecustomer/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 fromsceneTimeline[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
scoreOptionsfield 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/offeringnow has a direct coach framework preview into the real/v1/arete/coach/responsesBFF 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/receiptnow backs the receipt envelope, butAreteOfferingstill 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