V1 Web PWA · Surface walkthrough

Arete · the letter

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

walked
8sections3 minread

On this page

Context. surface customer · domain arete · route /arete/coaching · auth signed-in · source apps/oshun/web/src/app/arete/coaching/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

Purpose#

Lilith's letter to the practitioner — "twice a month, on a Sunday, Lilith writes you a short letter." Named patterns, two adjustments awaiting a nod, and a privacy note. Marked robots: { index: false, follow: false } in metadata. Renders <AreteCoaching data={await getAreteCoaching()} /> against the Lilith design system.

Entry points#

  • From /arete coach card — the weekly coach note links here (verify; in source AreteRoom's Approve button is not wired)
  • From /arete/review — the weekly review references "Lilith might suggest a phrase in the margin"
  • From notification / email — a "your letter has arrived" deep link
  • Direct URL / bookmark — yes (signed-in)
  • Not indexedrobots.index = false, so no search-engine surface

Layout regions#

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

  • Header: LCustomerNav active="today"
  • Masthead (ArMast): left "Arete · the letter", mid from Lilith · ${letterDate}, right "private to you · always", big "A letter, after the week.", lede about twice-a-month Sunday letters
  • Main, left (1.4fr): the letter — paper-card with letterCode watermark, recipient name, paragraphs (with optional emphasized accent and chip highlight spans), — Lilith signoff
  • Main, right (1fr):
    • "Patterns Lilith named" section — list of data.patterns rows (name + conf · 0.NN chip + evidence body)
    • "Two adjustments — your nod" section — list of data.adjustments rows with action buttons (Not now / Tell me more / Apply for one week) for non-accepted, "accepted · 11:42" code chip when accepted
    • "Privacy of the letter" dashed-border card

States#

  • Loading — server-awaited; no loading.tsx at this route
  • Populated (default) — Sunday letter with paragraphs, 2+ patterns, and 2+ adjustments (fixture provides all)
  • Adjustment acceptedadjustments[i].accept === true renders "accepted · 11:42" code chip and hides the button row
  • Adjustment pendingadjustments[i].accept !== true renders Not now / Tell me more / Apply for one week buttons
  • Empty patterns / adjustments — N/A: component renders the section headers and maps over the arrays; an empty array would yield empty space (no empty state coded)
  • Error (recoverable) — relies on parent error boundary (none here)
  • Offline — no offline handling in component
  • Gated — N/A
  • Standalone PWA — renders cleanly inside LWebShell

Interactions#

Pattern row (one per data.patterns)#

  • Pattern card — name, confidence chip (conf · 0.NN), evidence body
    • Function: presentational only
    • Keyboard / SR: no interactive role

Adjustment row (one per data.adjustments)#

  • "Not now" (button LBtn kind="ghost" size="sm")
    • Function: no onClick handler in source — visual only
    • Telemetry: none
  • "Tell me more" (button LBtn kind="ghost" size="sm")
    • Function: no onClick handler in source — visual only
  • "Apply for one week" (button LBtn kind="primary" size="sm")
    • Function: no onClick handler in source — visual only

These are hidden when a.accept === true; instead a static "accepted · 11:42" LCode chip renders. The "11:42" timestamp is hardcoded in the component, not driven by data.

Data & contracts#

  • Reads: getAreteCoaching() from @/lib/lilith-data/arete-depth — returns AreteCoachingData ({ letterDate, letterCode, recipientName, letterParagraphs, patterns, adjustments }). BFF mapping comment: getAreteCoaching ← /arete/letters/:id.
  • Writes: none wired today (no handlers on Not now / Tell me more / Apply for one week)
  • Realtime: none
  • Caching: server fetch, default Next.js cache; metadata also disables indexing
  • Auth/role check: relies on app-level middleware

Cross-references#

  • Shell: shell/01-app-shell.md
  • Domain hub: arete.md
  • Sibling Arete routes (see arete.md for full list)
  • Cross-domain: Lilith / Sophia are the AI substrate; this surface narrates pattern detection rather than wiring to a model in code
  • Component source: apps/oshun/web/src/components/lilith/arete.tsx (line 1347, AreteCoaching)
  • Data: apps/oshun/web/src/lib/lilith-data/arete-depth.ts (getAreteCoachingFixture)
  • Feature spec: V1/features.md

Open questions / known gaps#

  • All adjustment-action buttons (Not now / Tell me more / Apply for one week) are wired as static LBtn with no onClick — confirm whether this is intentional pre-V1 staging
  • The "accepted · 11:42" timestamp is hardcoded in the component instead of pulled from adjustment.acceptedAt — verify when wiring real data
  • Where does the "letter" actually originate — Sophia (AI substrate) or a domain-specific Lilith service? No imports cite Sophia in this file
  • Confirm the cadence ("every other Sunday" in metadata vs. "twice a month" in body lede) is consistent with feature spec