Context. surface customer · domain arete · route /arete/recovery · auth signed-in · source apps/oshun/web/src/app/arete/recovery/page.tsx
Last walked. 2026-06-26 automated runtime coverage added — Playwright types the one-sentence recovery note, verifies submit gating, forwards the browser POST to the live BFF /v1/arete/recovery/journal, and reads the resulting Written recovery return partial engagement back through /v1/arete/room. Live screen-reader, touch, offline, and telemetry-delivery checks still pending a manual AT pass.
Purpose#
"After three quiet days, Lilith checks in — with a small offer, not a debt. Welcome back." A humane recovery surface: Lilith names what she observed (the recital, the long workdays), then offers three returns at three sizes — none are a debt. Rendered as a phone shell inside a desktop frame.
Entry points#
- From a "we've missed you" push notification after three quiet days — inferred from the body copy ("Three days went quiet… Lilith will check in — never before.")
- From
/arete/streak— the streak surface mentions "three quiet days in a row, and Lilith will check in" but no explicit link is wired - Direct URL / bookmark — yes (signed-in)
- From a home rail "soft return" CTA — verify; not seen in source
Layout regions#
page.tsx is not a thin server component — it wraps a PhoneFrame inside
an LWebShell with a masthead and lede. Layout:
- Outer:
LWebShellwithtop={<LCustomerNav active="today" />} - Outer body:
padding: 32px 56px 80px; max-width: 720px - Masthead:
LMasthead— left "Specimen · Arete mobile", right "375 × 812", kicker "The soft return", title "Welcome back." - Lede block:
LEyebrow"How this opens" + italic body "Three days went quiet. Lilith names what she observed (the recital, the long workdays), then offers three returns at three sizes. None are a debt." - Phone frame:
PhoneFramewithlabel="Arete · recovery"andcaption="A 6-min sit, the walk only, or a written one-sentence." - Phone contents:
<AreteRecovery data={data} />—apps/oshun/web/src/components/lilith/arete.tsxline 355- Status bar (
LStatusBar) - Top row: "Arete · the soft return" eyebrow +
data.quietDaysLabelaccent eyebrow ("three quiet days") - Welcome headline (italic) + body lead/emphasized/rest spans
- "What Lilith noticed" callout card with accent left bar — uses
data.noticed("Sunday was your daughter's recital. Monday and Tuesday were long workdays…") - "An offer · not a debt" stack — three offer links (first one styled as
primary picked). Sourced from
data.offers(title + sub + href). - "One sentence — what was it like?"
<AreteRecoveryJournal />client island: textarea, character count, disabled/enabled "Save note" button, and role=status saved/error readback - Bottom action row (absolute-positioned): "Begin · 6 min" primary
LBtn size="lg"(flex 1), linked to the first offer href with a Tara sit fallback - Home indicator (
LHomeIndicator)
- Status bar (
States#
- Loading — server-awaited; no
loading.tsxat this route - Populated (default fixture) — quiet-day label, welcome copy, noticed callout, 3 offers, writable one-sentence recovery note composer
- Different quiet-day count —
data.quietDaysLabelis a plain string ("three quiet days"); no plural-aware handling visible - Empty offers — empty
data.offerswould render no offer cards (no empty-state coded) - Recovery copy tone (V1 feature requirement) — body says "does not break", offers framed as "not a debt"; no shaming copy detected ✓ matches V1/features.md humane-recovery requirement
- Destination engagement write — when a linked Tara sitting is completed
from
?origin=arete-recovery, the player posts the origin + completion state to/v1/tara/sittings/:id/complete; the BFF writes a member-scoped AreteTara recovery returncheck-in (partialordone) so/v1/arete/roomcan read the streak engagement back - Written recovery note engagement write — "Save note" posts
{ note }to/v1/arete/recovery/journal; the BFF writes a member-scopedWritten recovery returnAretepartialcheck-in so/v1/arete/roomcan read the streak engagement back - Error (recoverable) — relies on parent error boundary
- Offline — no explicit handling
- Gated — N/A
- Standalone PWA — page is a desktop frame around a phone shell; verify
on actual mobile viewport (the page hardcodes
max-width: 720pxand the phone is a fixed 375×812 specimen)
Interactions#
Offer cards (per data.offers)#
- Offer card — title + sub
- Function: navigates to
o.href - Style: first card (
i === 0) is highlighted (accent border, paper background); others are neutral border
- Function: navigates to
One-sentence journal#
- Textarea (
data-arete-recovery-journal-input) — labelled "One-sentence recovery note"; placeholder usesjournalPlaceholder;maxLength=220- Function: controlled input; updates
data-arete-recovery-journal-count - Empty/short state: "Save note" disabled until trimmed note length is at least eight characters
- Function: controlled input; updates
- Save note (
data-arete-recovery-journal-submit)- Function: POSTs
/v1/arete/recovery/journalwith{ note }, bearer auth, and CSRF headers; success rendersdata-arete-recovery-journal-outcome="saved"withdata-arete-recovery-journal-habit="Written recovery return"anddata-arete-recovery-journal-status="partial" - Failure: renders
data-arete-recovery-journal-outcome="error"with the BFF message or network failure copy; no optimistic "saved" state is shown
- Function: POSTs
"Begin · 6 min"#
- "Begin · 6 min" (link
LBtn kind="primary" size="lg")- Function: navigates to
data.offers[0]?.href, falling back to/tara/sit/when-the-mind-wanders - Completion contract: the Tara player carries
origin=arete-recoveryinto/v1/tara/sittings/:id/complete; partial completion counts as Arete engagement without making quiet days a debt - Touch target:
lgsize; absolute-positioned at bottom of phone canvas
- Function: navigates to
Data & contracts#
- Reads:
getAreteRecovery()from@/lib/lilith-data/arete-depth— returnsAreteRecoveryData({ quietDaysLabel, welcomeHeadline, welcomeBody {lead, emphasized, rest}, noticed, offers {title, sub, href}, journalPlaceholder }). BFF mapping comment:getAreteRecovery ← /arete/recovery. - Writes:
/v1/arete/recovery/journal— records the written one-sentence return as a member-scopedWritten recovery returnAretepartialcheck-in; this is read back through/v1/arete/room/v1/tara/sittings/:id/completewithorigin=arete-recovery— records the Tara completion and creates/reuses the member'sTara recovery returnArete habit, then writes apartialordonecheck-in for/v1/arete/roomread-back
- Realtime: none
- Caching: server fetch, default Next.js cache
- Auth/role check: app-level middleware
Cross-references#
- Domain hub:
arete.md - Streak partner:
arete-streak.md— the promise surface that defines when recovery triggers - Component source:
apps/oshun/web/src/components/lilith/arete.tsx(line 355,AreteRecovery) - Data:
apps/oshun/web/src/lib/lilith-data/arete-depth.ts(getAreteRecoveryFixture) - Phone frame:
apps/oshun/web/src/components/lilith/mobile.tsx - Feature spec:
V1/features.md— humane streak / recovery is a V1 requirement
E2E coverage#
apps/oshun/web/e2e/arete-streak-recovery.spec.ts— covers humane recovery copy, offer/Begin navigation, Tara-origin completion -> Arete read-back, written note submit gating, browser POST payload to/v1/arete/recovery/journal,Written recovery returnpartial engagement read-back through/v1/arete/room, streak heatmap a11y, and coach decision persistence.apps/oshun/bff/src/__tests__/domain-stubs-route.test.ts— covers the written recovery note route's auth-required, too-short, and missing-Postgres fail-closed branches.
Open questions / known gaps#
- Offer links and "Begin · 6 min" navigate to return surfaces; the recovery
page itself stays navigation-only, and the Tara destination now writes the
partial/doneArete engagement record for read-back on/arete— verified 2026-06-24. - The written one-sentence option is no longer a static placeholder: it is a
real textarea + BFF write, and the resulting
Written recovery returnpartialengagement reads back through/v1/arete/room— verified 2026-06-26. - The page wraps a phone shell inside a desktop frame (
Specimenlabel,375 × 812right meta); this looks like a design specimen surface, not a production responsive view. Verify whether the actual mobile customer experience is this page or a different responsive layout -
quietDaysLabelis a plain string in fixture — no pluralisation logic (1 quiet dayvs3 quiet days) visible - Empty
data.offerswould produce a section with header "An offer · not a debt" and no offer cards — needs an empty-state design