V1 Web PWA · Surface walkthrough

Arete · the goal, on one page

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

walked + e2e-covered
8sections5 minread

On this page

Context. surface customer · domain arete · route /arete/goal · auth signed-in · source apps/oshun/web/src/app/arete/goal/page.tsx

Last walked. 2026-07-01 by Codex (arete-goal-portrait.spec.ts covers the signed-in one-page goal portrait, fixture-backed criteria/habit/journal/adjustment read contract, disabled placeholder action semantics for all unwired controls, screen-reader summary for the aria-hidden 90-day SVG arc, mobile standalone no-horizontal-overflow rendering, exact /arete/goal service-worker shell caching, and real offline document replay), against commit HEAD' '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#

"A goal sits on one page." Why · done-is · habits that feed it · journal excerpts · 90-day arc · the next thing. Renders <AreteGoal data={await getAreteGoal()} /> against the Lilith design system.

Entry points#

  • From /arete / /arete/plan — a goal card or "Open the chapter goal ›" ghost button in AretePlan (no href wired; verify routing)
  • From notification — "your next thing is due" deep link
  • Direct URL / bookmark — yes (signed-in)
  • From /domains/arete/goals — internal goal-system dashboard may link here (no explicit link found in source; verify)

Layout regions#

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

  • Header: LCustomerNav active="today"
  • Masthead (ArMast): left "Arete · goal · one true page", mid "goal of 90 days · day 47", right "committed · private to you and your coach", big "Write the chapter.", lede about one-page goal anatomy
  • Main, left (1.4fr):
    • I · Why — long prose paragraph ("The notebook on the ruling part has waited a long time…") with hardcoded body text and emphasized "One thing, well."
    • II · Done is — two-column grid of data.criteria (criterion + accent value; current fixture: four criteria)
    • III · Habits that feed it — list of data.habits rows (label, frequency LCode, progress text, percent in mono accent; current fixture: three habits)
    • IV · Journal · selected — list of data.journal entries (date eyebrow + italic quote)
  • Aside, right (1fr):
    • V · The arc — 90-day SVG chart (today marker at day 47, projected line to day 90) plus sr-only text summary for screen readers
    • "elapsed · 47 days, remaining · 43 days, pace · 39 wds/day, needed · 50 wds/day" 2x2 grid (values hardcoded in component)
    • VI · The next thing — accent-bordered card with "tomorrow · 90 min" eyebrow, hardcoded "Finish §III. Walls vs. estate. ~600 words.", two buttons (Block on calendar / Open notebook)
    • VII · Adjustments · coach — list of data.adjustments (suggestion + sub) each with Not now / Apply buttons

States#

  • Loading — N/A: server-awaited; no loading.tsx at this route
  • Populated (default fixture) — fixture renders chapter-goal "day 47 of 90", four criteria, three habits, three journal entries, and two adjustments
  • Empty criteria / habits / journal / adjustments — empty arrays would render empty grids; no empty-state copy coded
  • Past-goal / completed — N/A: component is parameterized by AreteGoalData but renders the 90-day arc unconditionally; "completed" visual variant is not present in source
  • Error (recoverable) — relies on parent error boundary
  • Offline — real sw.js treats /arete/goal as an exact shell route; after warmup, the signed-in portrait replays while the browser context is offline with no generic fallback header
  • Gated — N/A within the component; route remains signed-in via app-level middleware/session seeding
  • Standalone PWA — iPhone standalone rendering is readable and has no horizontal overflow after responsive-page and habit-row grid fixes

Interactions#

Habit row (one per data.habits)#

  • Habit row — label, frequency LCode, progress note, percent
    • Function: presentational only — no per-row click target
    • Screen reader: announces as static text

"The next thing" card#

  • "Block on calendar" (button LBtn kind="ghost" size="sm")
    • Function: no onClick handler in source; LBtn renders it as a disabled placeholder with aria-disabled="true", data-placeholder="true", and the placeholder tooltip
    • Telemetry: none
  • "Open notebook" (button LBtn kind="primary" size="sm")
    • Function: no onClick handler in source; same disabled placeholder contract as above
    • Telemetry: none

Adjustment rows (per data.adjustments)#

  • "Not now" (button LBtn kind="ghost" size="sm")
    • Function: no onClick handler in source; disabled placeholder
  • "Apply" (button LBtn kind="primary" size="sm")
    • Function: no onClick handler in source; disabled placeholder

Arc chart (SVG)#

  • 90-day chartaria-hidden="true" SVG with grid, target dashed line, history path, today marker (day 47, "1,840 wds"), projected dashed line to day 90, "PROJECTED" label
    • Function: presentational — no zoom / hover
    • Screen reader: adjacent sr-only summary states day 47, 1,840 finished words, elapsed/remaining days, current pace, and words-per-day needed

Data & contracts#

  • Reads: getAreteGoal() from @/lib/lilith-data/arete-depth — returns AreteGoalData ({ criteria, habits, journal, adjustments }). BFF mapping comment: getAreteGoal ← /arete/goals/:id.
  • Writes: none wired (no handlers on any button)
  • Realtime: none
  • Caching: server fetch, default Next.js cache; public/sw.js exact shell route allowlist includes /arete/goal so warmed documents replay offline
  • Auth/role check: app-level middleware

Cross-references#

  • Domain hub: arete.md
  • Plan / Review siblings: arete-plan.md, arete-review.md
  • Domains-namespace alternate: domains-arete-goals.md — the AreteGoalSystem internal workspace (SMART / OKR / WOOP / 12-week wizards)
  • Component source: apps/oshun/web/src/components/lilith/arete.tsx (line 760, AreteGoal)
  • Data: apps/oshun/web/src/lib/lilith-data/arete-depth.ts (getAreteGoalFixture)
  • Feature spec: V1/features.md
  • E2E: apps/oshun/web/e2e/arete-goal-portrait.spec.ts covers the route-level read contract, placeholder controls, accessible arc summary, mobile standalone layout, and real service-worker offline replay

Open questions / known gaps#

  • None of the action buttons (Block on calendar / Open notebook / Not now / Apply) have onClick handlers in AreteGoal; they now render and test as disabled LBtn placeholders rather than live affordances.
  • Product decision still needed: should Block on calendar, Open notebook, Not now, and Apply become real persisted flows, and if so which BFF routes own them?
  • The "Why" prose, "elapsed/remaining/pace/needed" values, and "The next thing" card body are hardcoded in the component, not driven by AreteGoalData — confirm whether this is fixture leakage or intentional editorial framing
  • The SVG arc is aria-hidden; a screen-reader text summary now supplies the current/projection values.
  • Relationship to /domains/arete/goals (multi-framework goal system) — that surface has SMART / OKR / WOOP / 12-week-year wizards while this one is a single-goal portrait; document V1-shipping vs internal