---
path: /arete/recovery
surface: customer
domain: arete
auth: signed-in
source: apps/oshun/web/src/app/arete/recovery/page.tsx
status: walked
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.'
---

# Arete · the soft return

## 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**: `LWebShell` with `top={<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**: `PhoneFrame` with `label="Arete · recovery"` and
  `caption="A 6-min sit, the walk only, or a written one-sentence."`
- **Phone contents**: `<AreteRecovery data={data} />` —
  `apps/oshun/web/src/components/lilith/arete.tsx` line 355
  - Status bar (`LStatusBar`)
  - Top row: "Arete · the soft return" eyebrow + `data.quietDaysLabel` accent
    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`)

## States

- [ ] **Loading** — server-awaited; no `loading.tsx` at 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.quietDaysLabel` is a plain string
      ("three quiet days"); no plural-aware handling visible
- [ ] **Empty offers** — empty `data.offers` would 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
      Arete `Tara recovery return` check-in (`partial` or `done`) so
      `/v1/arete/room` can read the streak engagement back
- [ ] **Written recovery note engagement write** — "Save note" posts `{ note }`
      to `/v1/arete/recovery/journal`; the BFF writes a member-scoped
      `Written recovery return` Arete `partial` check-in so `/v1/arete/room` can
      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:     720px` and
      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

### One-sentence journal

- [ ] **Textarea** (`data-arete-recovery-journal-input`) — labelled
      "One-sentence recovery note"; placeholder uses `journalPlaceholder`;
      `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
- [ ] **Save note** (`data-arete-recovery-journal-submit`)
  - Function: POSTs `/v1/arete/recovery/journal` with `{ note }`, bearer auth,
    and CSRF headers; success renders
    `data-arete-recovery-journal-outcome="saved"` with
    `data-arete-recovery-journal-habit="Written recovery return"` and
    `data-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

### "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-recovery` into
    `/v1/tara/sittings/:id/complete`; partial completion counts as Arete
    engagement without making quiet days a debt
  - Touch target: `lg` size; absolute-positioned at bottom of phone canvas

## Data & contracts

- **Reads**: `getAreteRecovery()` from `@/lib/lilith-data/arete-depth` — returns
  `AreteRecoveryData` ({ 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-scoped `Written recovery return` Arete `partial` check-in; this is
    read back through `/v1/arete/room`
  - `/v1/tara/sittings/:id/complete` with `origin=arete-recovery` — records the
    Tara completion and creates/reuses the member's `Tara recovery return` Arete
    habit, then writes a `partial` or `done` check-in for `/v1/arete/room`
    read-back
- **Realtime**: none
- **Caching**: server fetch, default Next.js cache
- **Auth/role check**: app-level middleware

## Cross-references

- Domain hub: [`arete.md`](./arete.md)
- Streak partner: [`arete-streak.md`](./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`](../../../V1/features.md#arete) — 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 return` partial 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

- [x] 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` / `done` Arete engagement record for read-back on `/arete` —
      verified 2026-06-24.
- [x] The written one-sentence option is no longer a static placeholder: it is a
      real textarea + BFF write, and the resulting `Written recovery return`
      `partial` engagement reads back through `/v1/arete/room` — verified
      2026-06-26.
- [ ] The page wraps a phone shell inside a desktop frame (`Specimen` label,
      `375 × 812` right 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
- [ ] `quietDaysLabel` is a plain string in fixture — no pluralisation logic
      (`1 quiet day` vs `3 quiet days`) visible
- [ ] Empty `data.offers` would produce a section with header "An offer · not a
      debt" and no offer cards — needs an empty-state design
