V1 Web PWA · Surface walkthrough

Tara · sit player

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

walked
9sections14 minread

On this page

Context. surface customer · domain tara · route /tara/sit/[id] · auth signed-in · source apps/oshun/web/src/app/tara/sit/[id]/page.tsx

Last walked. 2026-06-27 targeted automated coverage — /tara/sit/when-the-mind-wanders: route-level Playwright covers idle/running/paused transport, aligned 15s keyboard scrub, reflection save/queue states, and 44px return-link targets; component tests cover completed/partial/drifted states. Earlier 2026-05-29 automated runtime walk verified populated render, HTTP 200, and 0 page/console errors. 2026-06-28 deepening adds Space/k global shortcut coverage, focused control bypass coverage, and storage-unavailable offline reflection copy. 2026-07-03 addendum adds a clock-controlled browser pass for the 90-second long-idle drift state, scrub recovery to paused, and resume telemetry. 2026-07-03 follow-up covers an unknown sitting id rendering the app-level 404 UI, no player render, usable search, and browser-back recovery to /tara. 2026-07-03 second follow-up returns the offline reflection branch online and confirms the queued UI plus tara:reflection:<id> localStorage draft remain because no reconnect drain is wired yet. Evidence: first-tara-sit-deepening.spec.ts, user-flag-to-review.spec.ts, TaraSitPlayer.test.tsx, WALKTHROUGH/results/runtime-sweep-2026-05-29.md

Purpose#

The single-sitting player. Resolves [id] against getTara() — either todaysSit or one of the sittings rows — and renders the TaraSitPlayer client component: timer, audio controls, drift detection, completion threshold, reflection capture, and cross-domain step-out. Lands here from TaraRoom "Begin sitting", from a notification deep link, or from a resumed session card.

Entry points#

  • Linked from /tara via TaraRoom "Begin sitting" button on the today-card and the sittings rail
  • Deep link from notification — Tara ritual reminder push (/tara/sit/<id>)
  • Direct URL / bookmark — yes (auth required); 404 if id resolves to neither todaysSit nor a row in sittings
  • Resumed-session card on / continuation rail (when wired)
  • Telegram mini-app share / web share target — same URL shape
  • Not in shell nav; not in PWA manifest.json shortcuts[] (verify)
  • robots: { index: false, follow: false } — never SEO-indexed

Layout regions#

page.tsx is a thin server component that resolves the sitting then renders <TaraSitPlayer> inside <LWebShell top={<LCustomerNav active="today" />}>. The player is one centered column, maxWidth: 760.

  • Header: LCustomerNav (active=today); on the player view itself a top bar with a "← Tara" back link and the TrustSafetyFlagButton
  • Hero / above-the-fold: eyebrow status (Ready when you are / In session / Paused / Paused, gently / Reflection), large display title, byline (teacher · pathLabel · session NN of M · sitting <index>)
  • Main (running): large mono timer (remaining), elapsed/total/percent eyebrow, scrub slider, transport row (play/pause, ±15s, End sitting), drift status bar (conditional), voice-speed + ambient-mix sliders, captions / transcript / large-text toggles, captions panel, transcript panel
  • Main (complete): completion blessing copy (variant by completed vs partially-completed), mood-after chips (settled / tender / restless / awake), reflection textarea, Save / saved-status row, "Done · back to Tara" link, then a "Next steps" rule + cross-domain chip row (Arete / Nisaba / Nyx)
  • Footer: none beyond LWebShell chrome

States#

The player's data-session-state attribute reflects the active state — useful for E2E selectors.

  • Loading — server-rendered after await getTara(); no client-side skeleton; verify Suspense / loading.tsx boundary exists on the parent segment
  • Not started (not-started) — heading "Ready when you are"; play button shows the triangle glyph; timer shows full duration; scrub slider at 0
  • Running (started) — heading "In session"; play button shows the pause glyph; setInterval(…, 1000) advances elapsed; drift watcher armed
  • Paused (paused) — heading "Paused"; tick interval stopped; scrub still works; resuming returns to started
  • Drifted (drifted) — auto-entered after DRIFT_IDLE_SECONDS = 90s with no interaction; soft status bar "The session paused itself. Take a breath; resume when you're ready."; scrubbing returns to paused. Playwright now installs the browser clock, starts the real route player, fast-forwards past the idle threshold, asserts the drifted state and copy, scrubs forward to recover to paused, then resumes with data-telemetry-event="tara_sit_resumed".
  • Completed (completed)elapsed >= total OR finish-early with ratio ≥ COMPLETION_THRESHOLD = 0.8; reflection panel mounted; blessing copy "A sit, kept…"
  • Partially completed (partially-completed) — finish-early with ratio < 0.8; reflection panel mounted; blessing copy "You crossed part of the way…"
  • Reflection submitting (submitting) — Save button shows "Saving…"; disabled
  • Reflection saved (saved) — green eyebrow "Saved · attached to today"
  • Reflection queued (queued) — offline path: writes tara:reflection:<sitId> to localStorage; eyebrow "Saved locally, will sync when you're back online". If durable storage throws, the state becomes memory-queued and the user is told the draft is kept in this tab only instead of being told it persisted locally. Playwright now also returns the browser online after the offline save and confirms the queued UI plus tara:reflection:<sitId> localStorage entry remain because no client-side drain exists yet.
  • 404 (sitting not found)id resolves to neither todaysSit nor a sittings row → Next notFound() → app-level 404 UI. Playwright now deep-links to /tara/sit/not-a-real-sitting-id from /tara, captures the current dev-shell document status as 200, and asserts "Page not found", exact 404, no [data-tara-sit-player], the 404 search box routing to /?q=morning%20clarity, and browser-back recovery to /tara.
  • Reduce-motionprefers-reduced-motion: reduce matches → play-button transition is none. Evidence: first-tara-sit-deepening.spec.ts emulates reduced motion and polls the real route player's play button transition until it collapses to none.
  • Error (recoverable)getTara() throws → segment error.tsx boundary; retry button preserves id
  • Offline (post-load) — once the player mounted, the timer ticks regardless of network; reflection submit detects navigator.onLine === false and queues to localStorage
  • Offline (cold load)/tara/sit/<id> not in SW precache; verify SW runtime caches the rendered HTML for the today-sit so cold offline launch works
  • Gatedauth: signed-in enforced by middleware (verify); no role gating today
  • Standalone PWA — cold-launch as relaunch target renders cleanly; safe-area insets respected; LCustomerNav not clipped
  • Storage unavailable — Safari private mode / quota exceeded: catch around localStorage.setItem flips state to memory-queued (component state keeps the draft visible). E2E now forces Storage.prototype.setItem to throw for tara:reflection:*, verifies data-tara-reflection-sync-state="memory-queued", verifies the copy says "Kept in this tab only", and verifies no tara:reflection:* durable entry was written.

Interactions#

Top bar#

  • "← Tara" back link (link)
    • Function: navigates to /tara
    • Keyboard: standard <a>; in tab order before timer controls
    • Screen reader: announces "Back to Tara" (aria-label)
    • Touch target: ≥ 44×44 CSS px (minHeight: 44, verified in Playwright)
    • Mobile: stays in top-left, eyebrow caps
    • Disabled when: never
    • Offline: same (client nav)
    • Telemetry: none
  • Trust & safety flag button (<TrustSafetyFlagButton targetKind="content" targetId="tara-sit-<id>">)
    • Function: opens flag/report dialog
    • Keyboard: button; Enter / Space activates
    • Screen reader: per shared component
    • Telemetry: per shared component
    • Evidence: user-flag-to-review.spec.ts opens the Tara sitting report dialog, asserts targetKind="content", target id tara-sit-when-the-mind-wanders, target label metadata, 44px touch target, category select, rationale field, submit enablement, and cancel close.
    • See shell/01-app-shell.md and apps/oshun/web/src/components/TrustSafetyFlagButton.tsx for shared behavior

Title block#

  • Heading — non-interactive; visual variant of session state
  • Display title — non-interactive; mirrors sitting.title
  • Byline eyebrow — non-interactive; teacher · pathLabel · session XX of M · sitting <index>

Timer + scrubber (only while not complete)#

  • Timer readout (role="timer" aria-live="polite" aria-atomic="true")
    • Function: announces remaining as MM:SS; polite live region (does not interrupt)
    • SR announcement cadence remains a manual AT follow-up if users report flood.
  • Scrub slider (<input type="range">)
    • Function: sets elapsed to chosen seconds (clamped 0..total)
    • Keyboard: arrow keys move ±1s by default; verify desired step
    • Screen reader: aria-label="Scrub session position", aria-valuemin/max/now, aria-valuetext="<MM:SS> elapsed"
    • Touch target: native range; verify mobile thumb is hit-friendly
    • Mobile: full-width
    • Disabled when: completed/partial — slider not rendered
    • Offline behavior: same (local state)
    • Telemetry: none today (consider tara_sit_scrubbed)
  • Play / pause toggle (circular button)
    • Function: toggles started ↔ paused; if not-started, starts the session and tick interval
    • Keyboard: Enter / Space; also bound to Space and k globally via window keydown (ignored when focus is inside <textarea> / <input>)
    • Screen reader: aria-label flips between "Pause sitting" / "Resume sitting"
    • Touch target: 56×56
    • Mobile: large circular target
    • Disabled when: state is completed / partially-completed — early-return in togglePlay
    • Offline behavior: works (local state)
    • Telemetry: data-telemetry-event flips between tara_sit_started, tara_sit_paused, tara_sit_resumed; data-telemetry-surface="tara_player"
  • "« 15s" rewind (button)
    • Function: scrub(elapsed - 15)
    • Keyboard: also bound to ArrowLeft globally through SCRUB_STEP_SECONDS = 15
    • Screen reader: aria-label="Rewind 15 seconds"
    • Touch target: minHeight: 44
    • Telemetry: none today
  • "15s »" forward (button)
    • Function: scrub(elapsed + 15)
    • Keyboard: bound to ArrowRight through SCRUB_STEP_SECONDS = 15
    • Screen reader: aria-label="Forward 15 seconds"
  • "End sitting" (button)
    • Function: finishEarly() — completes with partially-completed if ratio < 0.8, else completed
    • Keyboard: button; Enter / Space
    • Screen reader: aria-label="Finish session early"
    • Disabled when: never (verify intent — should pressing during not-started count as partially-completed?)
    • Telemetry: tara_sit_finished_early

Drift status bar (conditional)#

  • Drift advisory (role="status" aria-live="polite") — appears only when state === 'drifted'; copy: "The session paused itself. Take a breath; resume when you're ready."

Audio sliders#

  • Voice speed slider (<input type="range">, 0.85–1.25, step 0.05)
    • Function: stores voiceSpeed; verify the value is wired into the audio element when audio backend ships
    • Screen reader: aria-label="Voice playback speed", aria-valuenow
    • Telemetry: none
  • Voice / ambient mix slider (<input type="range">, 0–1, step 0.05)
    • Function: stores ambientMix; verify wiring when audio backend ships
    • Screen reader: aria-label="Voice ambient mix"

Display toggles#

  • Captions toggle (button)
    • Function: toggles caption panel (live SR-friendly caption-by-progress)
    • aria-pressed reflects state
  • Transcript toggle (button)
    • Function: toggles transcript panel; aria-controls="tara-transcript"
    • aria-expanded={showTranscript} reflects state (line 533) — note this toggle uses aria-expanded (disclosure semantics), not aria-pressed like the captions / large-text toggles
  • Large-text toggle (button)
    • Function: bumps caption font from 17→22 px and transcript font from 14→17 px
    • aria-pressed reflects state
    • Verify the toggle does not leak into other panels (mood chips, reflection textarea)

Captions / transcript panels#

  • Captions panel (role="status" aria-live="polite") — text chosen by captionForProgress(progress); updates as session advances
  • Transcript panel (#tara-transcript) — list of TRANSCRIPT lines with LDots marker
    • Verify the transcript panel scrolls / does not break the column on long transcripts

Completion: mood + reflection#

The completion block (data-tara-completion, line 600) opens with an LEyebrow "Thank you for sitting" (line 602) above the blessing copy, and the reflection field is introduced by a <label htmlFor="tara-reflection"> "Reflection · one or two lines is plenty" (line 645).

  • Mood chips (settled / tender / restless / awake)
    • Function: toggles single-select moodAfter
    • Keyboard: each button; Enter / Space
    • aria-pressed reflects active mood
    • Telemetry: none today (consider tara_reflection_mood_set)
  • Reflection textarea (<textarea id="tara-reflection">)
    • Function: stores reflection text
    • Keyboard: tab into field; Enter inserts newline (Space/k global handler correctly bails out — verify)
    • Placeholder: "What did you notice?"
  • Save reflection (button)
    • Function: calls submitReflection() — if offline writes to localStorage and flips queued; if online simulates a 300ms POST and flips saved
    • Disabled when: reflection.trim().length === 0 OR status is submitting → cursor not-allowed, opacity 0.4
    • Screen reader: aria-label="Save reflection"
    • Telemetry: data-telemetry-event="tara_reflection_saved"
  • Saved eyebrowSaved · attached to today (green)
  • Queued eyebrowSaved locally, will sync when you're back online (accent); Playwright verifies the current reconnect behavior keeps this eyebrow visible and keeps tara:reflection:<sitId> in localStorage. A BFF drain that clears the local queue on reconnect is not wired yet.
  • "Done · back to Tara" link (link)
    • Function: navigates to /tara
    • Keyboard: standard <a>
    • Touch target: ≥ 44×44 CSS px (minHeight: 44, verified in Playwright)

Next-steps cross-domain row (post-complete only)#

  • Arete chip-link/arete — telemetry tara_cross_domain_arete; copy "Add to today's routine"
  • Nisaba chip-link/nisaba — telemetry tara_cross_domain_nisaba; copy "Read a related passage"; verify the tara-to-nisaba-handoff passage-companion bridge (buildTaraNisabaPassageCompanion / NisabaPassageCompanionCard) is reachable from this chip rather than a generic /nisaba landing
  • Nyx chip-link/nyx — telemetry tara_cross_domain_nyx; copy "Step outside, tonight"; verify the nyx-to-tara-bridge is the mirror entry

Global keyboard handlers#

  • Space / k — toggle play/pause; bypassed when focus is in <textarea> / <input> (verify the early-return covers <select> and <button> too if they ever appear in-flow). E2E now verifies k starts from page context, Space pauses from page context, focused transcript/captions buttons keep native Space activation without toggling the session, and focused range input ignores the k shortcut.
  • ArrowLeftscrub(elapsed - 15) via SCRUB_STEP_SECONDS; aligned with the visible 15s rewind button
  • ArrowRightscrub(elapsed + 15) via SCRUB_STEP_SECONDS; aligned with the visible 15s forward button

Automated coverage#

  • apps/oshun/web/e2e/first-tara-sit-deepening.spec.ts covers the real /tara/sit/when-the-mind-wanders route for not-started, running, paused, timer/scrub, aligned ArrowLeft/ArrowRight 15-second jumps, rewind/forward buttons, clock-controlled long-idle drift and scrub-to-paused recovery, unknown-id 404 routing/search/browser-back recovery, transcript/captions/ large-text toggles, reflection mood + textarea + submitting/saved states, the return links' 44px hit areas, current cross-domain chip hrefs, Space/k page shortcuts, focused-control keyboard bypass, the partially-completed early-finish branch, the durable offline queued-reflection branch, the reconnect non-drain behavior for tara:reflection:<id>, and the storage-unavailable in-tab-only reflection queue branch.
  • apps/oshun/web/e2e/user-flag-to-review.spec.ts covers the Tara sit report affordance target metadata and dialog preparation path for /tara/sit/when-the-mind-wanders.
  • apps/oshun/web/src/components/lilith/TaraSitPlayer.test.tsx covers the component's title/byline/back-link render, play/pause timer progression, completed and partially-completed branches, drift auto-pause after 90 idle seconds, scrub-from-drift back to paused, caption/transcript disclosure semantics, 15-second global arrow shortcuts, and the post-complete bridge link render.
  • apps/oshun/web/e2e/arete-streak-recovery.spec.ts covers the Arete recovery handoff into /tara/sit/when-the-mind-wanders and observes the completion POST to /v1/tara/sittings/when-the-mind-wanders/complete.
  • apps/oshun/web/e2e/tara-session-player.spec.ts covers the newer /domains/tara?...path= immersive player, including Tara-to-Nisaba, Metis, Arete, Nyx, and assistant handoffs. That spec is related Tara journey coverage, but it does not replace /tara/sit/[id] route coverage above.

Data & contracts#

  • Reads: getTara() from @/lib/lilith-data/tara (server). The page resolves the [id] param against data.todaysSit.id and data.sittings[].id. No client fetch for the session itself.
  • Writes (completion — wired): on the state transition into completed / partially-completed, a useEffect POSTs once to /v1/tara/sittings/<id>/complete (buildOshunBffUrl, method: 'POST', credentials: 'include', headers include await csrfHeaders()), body { durationSeconds: elapsed, reflectionText: reflection || null } (lines 122–161). A completionPostedRef guard makes it fire exactly once per session; the endpoint is idempotent on the sitting id + session. On a thrown fetch it falls back to a localStorage queue under key tara:completion:<id> with the payload plus queuedAtIso.
  • Writes (reflection — today): the separate reflection submit button (submitReflection) is still a 300 ms stub (await new Promise((r) => setTimeout(r, 300)), line 252) — verify the real BFF route and contract. Add to libs/contracts/ when wired. (Note: the reflection text is already carried to the BFF by the completion POST above.)
  • Writes (offline): reflection-submit offline path writes tara:reflection:<sittingId> localStorage key; the completion POST's offline fallback writes tara:completion:<id>
  • Realtime: none today; future: Psyche session envelope push (session.tick / session.completed / session.drifted) — verify event names
  • Caching: SSR per request; SW runtime cache should pin the today-sit HTML so the cold-offline launch works; verify cache key includes [id]
  • Auth/role check: middleware enforces signed-in; no role gating; Lilith tone policy applies to teacher / transcript copy upstream of this view, not at render time

Cross-references#

Open questions / known gaps#

  • Runtime walk (2026-05-29) — walked with a real id: the earlier placeholder-id 404 was expected; walking /tara/sit/when-the-mind-wanders (a real id from the /tara hub: also first-sit, the-breath-observed, the-looker, a-small-space, the-body-settled) renders the populated TaraSitPlayer with transport controls. HTTP 200, no notFound, 0 errors. id hit "Page not found" (demo id -> not found; needs a real id to walk populated state (h1="Page not found")). Re-walk with a real id from mock data to verify the populated player state.
  • Keyboard scrub step is aligned with the 15s button labels through SCRUB_STEP_SECONDS = 15; covered by TaraSitPlayer.test.tsx and first-tara-sit-deepening.spec.ts.
  • Voice-speed and ambient-mix sliders are stateful and covered for keyboard updates; voice speed is still not wired to a voice-audio backend. Ambient mix is wired to the /v1/tara/sessions/<id>/ambient.wav audio element. Evidence: first-tara-sit-deepening.spec.ts presses ArrowRight on each slider and polls aria-valuenow for the controlled value change.
  • Reflection BFF route + contract not yet defined — add to libs/contracts/ and wire submitReflection
  • Localstorage tara:reflection:<id> queue has no client-side drain on reconnect. Evidence: first-tara-sit-deepening.spec.ts returns the browser online after an offline reflection save and asserts the queued eyebrow plus localStorage draft remain. The offline-first-use "queued writes flush on reconnect" claim must not be applied to this Tara reflection path until a drain is implemented.
  • notFound() path is reachable and falls through cleanly to the app-level 404 UI. Evidence: first-tara-sit-deepening.spec.ts captures the current dev-shell document status as 200 and asserts no player render, 404 search routing, and browser-back recovery to /tara. A Tara-themed 404 remains a possible product refinement, not a coverage gap for the current shipped route.
  • SW precache scope: confirm apps/oshun/web/src/sw.ts (or equivalent) caches /tara/sit/<todaysSitId> so cold-offline launch works for the today-sit
  • End sitting while still in not-started produces partially-completed with 0% — Playwright verifies the current branch; product intent remains open (ledger entry vs no-op).
  • Cross-domain chip links go to domain landing pages, not the specific bridge targets — wire ?from=tara-sit-<id> or use buildTaraNisabaPassageCompanion directly so the receiving view knows the origin