V1 Web PWA · Surface walkthrough

Arete · the soft return

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

walked
9sections6 minread

On this page

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: 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 countdata.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
  • 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 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#

  • 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.
  • 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