V1 Web PWA · Surface walkthrough

Activity

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

walked
9sections10 minread

On this page

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

Last walked. 2026-06-27 automated coverage reconciliation by Codex — real-dev Activity Playwright expanded for scoped empty live timeline, live loading, real Nisaba notebook write/read through BFF activity aggregation, support-lane absence notes, focal shortcuts, desktop/laptop support layouts; component, telemetry, shell-state, outage, PWA/offline, accessibility, and BFF contract coverage cross-checked.

Purpose#

A cross-domain continuity surface. Activity surfaces the real threads worth returning to — saved continuations, notebooks, tutoring/session artifacts, assistant excerpts, and backed achievement progress — grouped into "continuity threads" so the next move is easy to pick up. Milestones, streak calendar, and weekly digest have UI components but no live V1 backing projection yet; the route renders explicit unavailable notes instead of fixture data.

Entry points#

  • Shell nav: Activity tabWEB_SHELL_ROUTE_PATHS.activity; primary entry from any shell route
  • Home rail ("See all" on the activity feed on /) — see home.md
  • Home focal-stage shortcutBack to Home and Open Library rail shortcuts inside the Activity focal stage cross-link from sibling shell surfaces (so users return here from there)
  • Re-entry from messages, library, or domain surfaces when an action card deep-links to /activity
  • Direct URL / bookmark — yes, auth required (no anonymous redirect logic in page.tsx; relies on shell middleware)
  • Shell state preview?shellState=<mode> swaps the content for ShellSurfaceStatePreview (loading / empty / error / offline / etc.) via resolveShellRouteSurfaceContent({ surface: 'activity' })

Layout regions#

page.tsx mounts ShellLayout with active="activity" and breadcrumbs Home → Activity, then renders <ActivityDashboard /> (or the shell-state preview).

Inside ActivityDashboard:

  • Focal stage (ShellRouteAnchor + ShellLandingFocalStage):
    • Focal canvas headline ("N threads are ready to reopen right now."), summary, and up to two "lead thread" cards (data-activity-focal-thread)
    • Focal rail with DataSourceIndicator (Live data / Offline data, refresh) plus metric pills (Threads, Cross-domain, Domains, Ready now)
    • Useful next surfaces card (Back to Home, Open Library)
  • Primary laneEnhancedTimeline:
    • Section header "Continuity timeline" with summary line N threads · N cross-domain · N ready to resume and "Mark all read" affordance
    • Continuity summary tile grid (data-activity-continuity-stat)
    • WorkspaceToolbar ("Activity controls") with domain WorkspaceSegmentRail and thread-type chip pills, plus a Reset filters chip when filters are on
    • Timeline list with a vertical guideline; time-grouped continuity thread cards
  • Support lane (WorkspaceSupportLane):
    • AchievementsSection — achievement card grid with rarity borders + detail overlay when /v1/achievements/user + /definitions return records
    • MilestonesSection — milestone cards with confetti celebration when data is supplied; live route currently shows an unavailable note
    • StreakCalendarSection — per-day streak calendar with domain dots when data is supplied; live route currently shows an unavailable note
    • WeeklyDigestSection — glass-style weekly review panel with expandable highlights when data is supplied; live route currently shows an unavailable note
    • Docked aside on wide desktop (supportDocked); collapses into a single lane below the timeline on smaller breakpoints

States#

  • Initial live fetch / loadinguseActivityData() starts loading=true; DataSourceIndicator intentionally returns null while the shell, focal stage, and timeline stay composed. Covered by activity-cross-domain-continuity.spec.ts with a delayed real /v1/activity request.
  • Live data, populatedisLive=true; live BFF activity timeline renders a real seeded Nisaba notebook in the focal lead card and the timeline. Covered by activity-cross-domain-continuity.spec.ts and BFF route tests for real Nisaba/Metis records.
  • Honest empty live timeline — scoped real BFF read with no Nisaba/Metis records shows "Nothing is waiting to reopen right now.", no invented continuity threads, no retired thread-grounding-evidence, and data-activity-empty. Covered by activity-cross-domain-continuity.spec.ts.
  • Offline / fallback / endpoint failure — live endpoint failure renders CustomerShellOutageBanner ("Live activity is unavailable") and keeps activity sections honest-empty instead of showing fixture fallback data. Covered by shell-partial-outage-banners.spec.ts and ActivityDashboard.test.tsx.
  • Error (recoverable)error text is surfaced in the data-source indicator and outage banner reason; Refresh retries useActivityData().refresh(). Covered by component tests and the route-level outage Playwright spec.
  • Empty filter result — timeline shows "No continuity threads match the current filters." when active domain/kind filters remove all threads. Covered by activity-cross-domain-continuity.spec.ts and component filter tests.
  • No unread items — "Mark all read" is hidden when unreadCount=0; read cards have no unread dot. Covered by the empty Activity E2E and ActivityDashboard.test.tsx.
  • Active filters — domain !== 'all' or kind !== 'all' reveals Reset filters chip. Covered by ActivityDashboard.test.tsx.
  • Achievement detail overlay — clicking an achievement opens the modal-style AchievementDetailOverlay; close button hides it. Covered by ActivityDashboard.test.tsx.
  • Unavailable live support projections — milestone, streak-calendar, and weekly-digest backing projections are absent in V1; the live route shows unavailable notes for each. Covered by activity-cross-domain-continuity.spec.ts.
  • Milestone celebrationMilestonesSection triggers a confetti burst via setCelebrating(id) for ~2.5 s when milestone data is supplied. Covered by ActivityDashboard.test.tsx fixture coverage.
  • Reduced motiondisableAnimation suppresses entrance animations, confetti, pulses, and spin in component coverage; shell PWA reduced-motion spinner behavior is covered in pwa-lifecycle-deepening.spec.ts.
  • Shell state preview?shellState=loading|empty|error|offline|... replaces the workspace with ShellSurfaceStatePreview (handled in page.tsx). Covered by shell-surface-states.spec.ts.
  • Standalone PWA — Activity relies on shell PWA behavior; standalone cold start and offline cached shell restore /activity without a route-specific branch. Covered by pwa-lifecycle-deepening.spec.ts and offline-shell-routes.spec.ts.

Interactions#

Focal stage#

  • Lead thread card (link, data-activity-focal-thread)
    • Function: navigates to thread.actionHref or WEB_SHELL_ROUTE_PATHS.activity
    • Mobile: wraps; domains chip-bar reflows
    • Telemetry: none in this branch.
    • Coverage: real Nisaba notebook focal lead href asserted in activity-cross-domain-continuity.spec.ts; component layout covered in ActivityDashboard.test.tsx.
  • Refresh (button inside DataSourceIndicator)
    • Function: calls useActivityData().refresh(); re-fetches the activity payload
    • Disabled when: none — always clickable; loading branch returns null entirely
    • Coverage: ActivityDashboard.test.tsx.
  • Back to Home / Open Library shortcut cards
    • Function: navigate to WEB_SHELL_ROUTE_PATHS.home / WEB_SHELL_ROUTE_PATHS.library
    • Coverage: route hrefs asserted in activity-cross-domain-continuity.spec.ts.

Continuity timeline header#

  • Mark all read (button)
    • Function: optimistically marks every timeline item read after a 400 ms delay; spinner shows during the delay
    • Disabled when: markingAllRead === true (cursor wait, 0.6 opacity)
    • Hidden when: unreadCount === 0
    • Coverage: spinner + all-read transition covered in ActivityDashboard.test.tsx; hidden state covered in activity-cross-domain-continuity.spec.ts.

Activity controls toolbar#

  • Domain segments (DomainFilterTabs, WorkspaceSegmentRail)
    • Function: filters threads by WebNavigableDomainId | 'all'
    • Aria-label: "Activity domains"
    • Options: All, Tara, Veritas, Nyx, Arete, Nisaba, Metis
    • Coverage: route empty filters in activity-cross-domain-continuity.spec.ts; component Tara/Metis filtering in ActivityDashboard.test.tsx.
  • Thread type pills (KindFilterPills, WorkspaceChoiceChip)
    • Function: filters by ActivityKind | 'all'
    • Options: All types, update, reminder, event, insight
    • Coverage: route kind-empty filter in activity-cross-domain-continuity.spec.ts; component kind filtering in ActivityDashboard.test.tsx.
  • Reset filters (button) — appears only when filters are active; resets to All / All
  • <N> threads status badge (WorkspaceStatusBadge) — non-interactive

Continuity thread card (one per thread; nested step rows)#

  • Card hover/focus — translates 2 px upward, soft drop-shadow
  • Unread dot — pulses; suppressed when disableAnimation or unreadCount === 0
  • Domain path label, step count, status label, priority badge, timestamp — non-interactive eyebrows
  • Thread reason and metrics row — non-interactive
  • Domain chips — non-interactive (one per thread.domains)
  • Step row · action link (Link, data-activity-thread-step-action)
    • Function: navigates to step.actionHref
    • Telemetry: trackActivityReentryOpened with trigger: 'step'
  • Step row · Mark read (button) — visible only on unread steps; calls handleMarkRead(step.id)
  • Step assistant highlight link — Telemetry: trackActivityReentryOpened with trigger: 'assistant-highlight'
  • Step study-continuation link (notebook / course / assessment / tutoring) — Telemetry: trackActivityReentryOpened with trigger: 'study-continuation'
  • Thread action link (e.g., "Resume") — Telemetry: trackActivityReentryOpened with trigger: 'thread'
    • Coverage: thread/card rendering and mark-read interactions in ActivityDashboard.test.tsx; thread/step/assistant/study telemetry in ActivityDashboardTelemetry.test.tsx and activity-reentry-analytics.spec.ts; real notebook action href and click in activity-cross-domain-continuity.spec.ts.

Achievements section#

  • Achievement card (button) — opens AchievementDetailOverlay
  • Achievement detail overlay
    • Close (×) — closes overlay
    • Backdrop click — closes overlay
    • Lists Requirements and Tips when present
    • Shows progress bar when locked; "Unlocked on …" when unlocked
    • Coverage: ActivityDashboard.test.tsx.

Milestones section#

  • Milestone celebrate trigger — fires ConfettiBurst for ~2.5 s when milestone data is supplied. Covered by ActivityDashboard.test.tsx.

Streak calendar#

  • Per-day cell — entrance staggered; reduced motion suppresses animations
  • Domain dots inside cells — non-interactive indicators
    • Coverage: ActivityDashboard.test.tsx.

Weekly digest#

  • Highlights expand/collapse — expanding panel inside glass-styled section
    • Coverage: ActivityDashboard.test.tsx.

Data & contracts#

  • Reads: useActivityData() (see apps/oshun/web/src/components/activity/data/useActivityData.ts) fans out to live backends:
    • Timeline: GET /v1/activity (BFF alias for /activity), returning only real member activity from authorized domains.
    • Achievements: GET /v1/achievements/user plus GET /v1/achievements/definitions.
    • Milestones, streak calendar, and weekly digest: no live V1 backing projection yet; the hook leaves these empty/null.
  • Writes: none from page.tsx; Mark read / Mark all read are local state mutations on the items list
  • Realtime: None observed in this surface — refresh is manual.
  • Caching: useActivityData stores client state; BFF activity responses use authenticated cache keys partitioned by user, scopes, tenant/residency, route, pagination, tier, and suspended-domain headers.
  • Auth/role check: page.tsx has no auth guard; depends on shell middleware and the BFF bearer scopes (domain:* or domain:<id>) for Activity aggregation
  • Telemetry: trackActivityReentryOpened from @/analytics/activityReentryTelemetry (surface timeline, triggers thread | step | study-continuation | assistant-highlight)

Automated coverage#

  • apps/oshun/web/e2e/activity-cross-domain-continuity.spec.ts — real-dev Activity route coverage for scoped empty live timeline, live loading, real Nisaba notebook write/read through BFF aggregation, focal lead card, thread action navigation, shortcut hrefs, filter-empty states, Metis tab, desktop docked support lane, unavailable live support projections, and laptop collapsed support lane.
  • apps/oshun/web/e2e/activity-reentry-analytics.spec.ts — route-level re-entry analytics for thread, step, assistant-highlight, and study-continuation triggers with mocked contract-shaped timeline data.
  • apps/oshun/bff/src/__tests__/activity-route.test.ts — BFF activity contract: real authorized-domain timelines, no fabricated rows for empty domains, degraded adapter handling, domain-scope filtering, real Nisaba notebook, real Metis ingest/tutoring activity, legacy /v1/activity alias, pagination, cache hits, auth required, and missing-domain-scope rejection.
  • apps/oshun/web/src/components/activity/__tests__/ActivityDashboard.test.tsx — component coverage for dashboard composition, fallback banner, refresh, timeline cards, filtering, mark-read, mark-all-read, achievements overlay, milestone celebration, streak calendar, weekly digest, reduced animation, and support-lane responsive behavior.
  • apps/oshun/web/src/components/activity/__tests__/ActivityDashboardTelemetry.test.tsx — telemetry payload coverage for thread, step, assistant-highlight, and study-continuation re-entry with active filters.
  • Shared shell/PWA/viewport/a11y coverage: shell-partial-outage-banners.spec.ts, shell-surface-states.spec.ts, pwa-lifecycle-deepening.spec.ts, offline-shell-routes.spec.ts, viewport-fit.spec.ts, redesigned-surface-breakpoints.spec.ts, and shell-route-accessibility.spec.ts.

Cross-references#

Open questions / known gaps#

  • Runtime walk (2026-05-29) — defect FIXED & verified: React 418 hydration mismatch resolved (time-derived values now deferred to a post-mount effect via useClientNow/useIsMounted from @/hooks/useClientTime; for /coordinates the missed RiseSetCalculator site was gated). Re-walked against a fresh build: 0 page errors, 0 console errors, HTTP 200. React error #418; visit https://react.dev/errors/418?a. React error 418 is a hydration mismatch — server-rendered HTML differs from the client (this view renders time/position-dependent content, e.g. new Date()/toLocale*, without suppressHydrationWarning). Fix: compute time-dependent values in an effect or gate with suppressHydrationWarning. Status kept drafted until fixed.
  • Confirm the real endpoint(s) feeding useActivityData() and whether the "Live data" badge corresponds to a specific BFF route: timeline is /v1/activity; achievements fan out to /v1/achievements/user and /v1/achievements/definitions; isLive is true when either live section resolves.
  • Document write-side semantics: Mark read / Mark all read are currently client-only state transitions, not BFF writes.
  • Confirm whether achievement, milestone, streak, and weekly-digest sources are part of the same useActivityData() payload or fan-out separately: achievements fan out separately; milestone, streak, and weekly digest have no live V1 backing projection and remain empty/null.
  • Verify that the focal-stage Back to Home / Open Library cards meet touch-target rules on mobile. Covered by activity-cross-domain-continuity.spec.ts at 390 px width.