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
/taraviaTaraRoom"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
idresolves to neithertodaysSitnor a row insittings - 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.jsonshortcuts[](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 theTrustSafetyFlagButton - 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
completedvspartially-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
LWebShellchrome
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.tsxboundary 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)advanceselapsed; drift watcher armed - Paused (
paused) — heading "Paused"; tick interval stopped; scrub still works; resuming returns tostarted - Drifted (
drifted) — auto-entered afterDRIFT_IDLE_SECONDS = 90swith no interaction; soft status bar "The session paused itself. Take a breath; resume when you're ready."; scrubbing returns topaused. Playwright now installs the browser clock, starts the real route player, fast-forwards past the idle threshold, asserts thedriftedstate and copy, scrubs forward to recover topaused, then resumes withdata-telemetry-event="tara_sit_resumed". - Completed (
completed) —elapsed >= totalOR 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: writestara:reflection:<sitId>tolocalStorage; eyebrow "Saved locally, will sync when you're back online". If durable storage throws, the state becomesmemory-queuedand 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 plustara:reflection:<sitId>localStorage entry remain because no client-side drain exists yet. - 404 (sitting not found) —
idresolves to neithertodaysSitnor asittingsrow → NextnotFound()→ app-level 404 UI. Playwright now deep-links to/tara/sit/not-a-real-sitting-idfrom/tara, captures the current dev-shell document status as200, and asserts "Page not found", exact404, no[data-tara-sit-player], the 404 search box routing to/?q=morning%20clarity, and browser-back recovery to/tara. - Reduce-motion —
prefers-reduced-motion: reducematches → play-button transition isnone. Evidence:first-tara-sit-deepening.spec.tsemulates reduced motion and polls the real route player's play button transition until it collapses tonone. - Error (recoverable) —
getTara()throws → segmenterror.tsxboundary; retry button preservesid - Offline (post-load) — once the player mounted, the timer ticks
regardless of network; reflection submit detects
navigator.onLine === falseand queues tolocalStorage - 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 - Gated —
auth: signed-inenforced by middleware (verify); no role gating today - Standalone PWA — cold-launch as relaunch target renders cleanly;
safe-area insets respected;
LCustomerNavnot clipped - Storage unavailable — Safari private mode / quota exceeded: catch
around
localStorage.setItemflips state tomemory-queued(component state keeps the draft visible). E2E now forcesStorage.prototype.setItemto throw fortara:reflection:*, verifiesdata-tara-reflection-sync-state="memory-queued", verifies the copy says "Kept in this tab only", and verifies notara: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
- Function: navigates to
- 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.tsopens the Tara sitting report dialog, assertstargetKind="content", target idtara-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.mdandapps/oshun/web/src/components/TrustSafetyFlagButton.tsxfor 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.
- Function: announces remaining as
- Scrub slider (
<input type="range">)- Function: sets
elapsedto 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)
- Function: sets
- Play / pause toggle (circular button)
- Function: toggles
started ↔ paused; ifnot-started, starts the session and tick interval - Keyboard: Enter / Space; also bound to
Spaceandkglobally viawindowkeydown (ignored when focus is inside<textarea>/<input>) - Screen reader:
aria-labelflips between "Pause sitting" / "Resume sitting" - Touch target: 56×56
- Mobile: large circular target
- Disabled when: state is
completed/partially-completed— early-return intogglePlay - Offline behavior: works (local state)
- Telemetry:
data-telemetry-eventflips betweentara_sit_started,tara_sit_paused,tara_sit_resumed;data-telemetry-surface="tara_player"
- Function: toggles
- "« 15s" rewind (button)
- Function:
scrub(elapsed - 15) - Keyboard: also bound to
ArrowLeftglobally throughSCRUB_STEP_SECONDS = 15 - Screen reader:
aria-label="Rewind 15 seconds" - Touch target:
minHeight: 44 - Telemetry: none today
- Function:
- "15s »" forward (button)
- Function:
scrub(elapsed + 15) - Keyboard: bound to
ArrowRightthroughSCRUB_STEP_SECONDS = 15 - Screen reader:
aria-label="Forward 15 seconds"
- Function:
- "End sitting" (button)
- Function:
finishEarly()— completes withpartially-completedif ratio < 0.8, elsecompleted - Keyboard: button; Enter / Space
- Screen reader:
aria-label="Finish session early" - Disabled when: never (verify intent — should pressing during
not-startedcount aspartially-completed?) - Telemetry:
tara_sit_finished_early
- Function:
Drift status bar (conditional)#
- Drift advisory (
role="status" aria-live="polite") — appears only whenstate === '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
- Function: stores
- 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"
- Function: stores
Display toggles#
- Captions toggle (button)
- Function: toggles caption panel (live SR-friendly caption-by-progress)
aria-pressedreflects state
- Transcript toggle (button)
- Function: toggles transcript panel;
aria-controls="tara-transcript" aria-expanded={showTranscript}reflects state (line 533) — note this toggle usesaria-expanded(disclosure semantics), notaria-pressedlike the captions / large-text toggles
- Function: toggles transcript panel;
- Large-text toggle (button)
- Function: bumps caption font from 17→22 px and transcript font from 14→17 px
aria-pressedreflects 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 bycaptionForProgress(progress); updates as session advances - Transcript panel (
#tara-transcript) — list ofTRANSCRIPTlines withLDotsmarker- 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-pressedreflects active mood- Telemetry: none today (consider
tara_reflection_mood_set)
- Function: toggles single-select
- Reflection textarea (
<textarea id="tara-reflection">)- Function: stores
reflectiontext - Keyboard: tab into field; Enter inserts newline (Space/k global handler correctly bails out — verify)
- Placeholder: "What did you notice?"
- Function: stores
- Save reflection (button)
- Function: calls
submitReflection()— if offline writes to localStorage and flipsqueued; if online simulates a 300ms POST and flipssaved - Disabled when:
reflection.trim().length === 0OR status issubmitting→ cursornot-allowed, opacity0.4 - Screen reader:
aria-label="Save reflection" - Telemetry:
data-telemetry-event="tara_reflection_saved"
- Function: calls
- Saved eyebrow —
Saved · attached to today(green) - Queued eyebrow —
Saved locally, will sync when you're back online(accent); Playwright verifies the current reconnect behavior keeps this eyebrow visible and keepstara: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)
- Function: navigates to
Next-steps cross-domain row (post-complete only)#
- Arete chip-link →
/arete— telemetrytara_cross_domain_arete; copy "Add to today's routine" - Nisaba chip-link →
/nisaba— telemetrytara_cross_domain_nisaba; copy "Read a related passage"; verify thetara-to-nisaba-handoffpassage-companion bridge (buildTaraNisabaPassageCompanion/NisabaPassageCompanionCard) is reachable from this chip rather than a generic/nisabalanding - Nyx chip-link →
/nyx— telemetrytara_cross_domain_nyx; copy "Step outside, tonight"; verify thenyx-to-tara-bridgeis 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 verifieskstarts from page context,Spacepauses from page context, focused transcript/captions buttons keep native Space activation without toggling the session, and focused range input ignores thekshortcut. - ArrowLeft —
scrub(elapsed - 15)viaSCRUB_STEP_SECONDS; aligned with the visible 15s rewind button - ArrowRight —
scrub(elapsed + 15)viaSCRUB_STEP_SECONDS; aligned with the visible 15s forward button
Automated coverage#
apps/oshun/web/e2e/first-tara-sit-deepening.spec.tscovers the real/tara/sit/when-the-mind-wandersroute 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 fortara:reflection:<id>, and the storage-unavailable in-tab-only reflection queue branch.apps/oshun/web/e2e/user-flag-to-review.spec.tscovers 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.tsxcovers 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.tscovers the Arete recovery handoff into/tara/sit/when-the-mind-wandersand observes the completion POST to/v1/tara/sittings/when-the-mind-wanders/complete.apps/oshun/web/e2e/tara-session-player.spec.tscovers 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 againstdata.todaysSit.idanddata.sittings[].id. No client fetch for the session itself. - Writes (completion — wired): on the state transition into
completed/partially-completed, auseEffectPOSTs once to/v1/tara/sittings/<id>/complete(buildOshunBffUrl,method: 'POST',credentials: 'include', headers includeawait csrfHeaders()), body{ durationSeconds: elapsed, reflectionText: reflection || null }(lines 122–161). AcompletionPostedRefguard 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 keytara:completion:<id>with the payload plusqueuedAtIso. - 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 tolibs/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 writestara: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#
- Shell:
shell/01-app-shell.md,shell/03-pwa-behavior.md,shell/06-keyboard-a11y.md - Sibling Tara routes:
tara.md,domains-tara-programs.md,domains-tara-teachers.md,domains-tara-courses-id.md,domains-tara-collections.md,domains-tara-sounds.md,domains-tara-search.md,domains-tara-analytics.md - Component sources:
apps/oshun/web/src/components/lilith/TaraSitPlayer.tsx,apps/oshun/web/src/components/lilith/TaraSitPlayer.test.tsx,apps/oshun/web/src/components/TrustSafetyFlagButton.tsx,apps/oshun/web/src/lib/lilith-data/tara.ts - Feature spec:
V1/features.md - Architecture:
V1/ARCHITECTURE.md - Journeys traversing this view:
journeys/first-tara-sit.md— end-to-end first sit; cites this view as the player landingjourneys/tara-daily-ritual.md— daily ritual lands herejourneys/tara-to-nisaba-handoff.md— post-complete Nisaba chip is the bridge entryjourneys/offline-first-time-use.md— reflection queue path exercised here
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/tarahub: also first-sit, the-breath-observed, the-looker, a-small-space, the-body-settled) renders the populatedTaraSitPlayerwith 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 byTaraSitPlayer.test.tsxandfirst-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.wavaudio element. Evidence:first-tara-sit-deepening.spec.tspresses ArrowRight on each slider and pollsaria-valuenowfor the controlled value change. - Reflection BFF route + contract not yet defined — add to
libs/contracts/and wiresubmitReflection - Localstorage
tara:reflection:<id>queue has no client-side drain on reconnect. Evidence:first-tara-sit-deepening.spec.tsreturns 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.tscaptures the current dev-shell document status as200and 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 sittingwhile still innot-startedproducespartially-completedwith 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 usebuildTaraNisabaPassageCompaniondirectly so the receiving view knows the origin