V1 Web PWA · Surface walkthrough

Metis · today's session

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

walked
8sections4 minread

On this page

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

Last walked. 2026-06-29 real-infra Playwright chromium — live guarded BFF multi-course fixture, signed-in customer shell, assessment-ready start selection, queue rows, total minutes, hrefs, mobile standalone shape, no horizontal overflow, and shared axe scan. Evidence: WALKTHROUGH/results/v1-real-infra-run-2026-06-22.md#199-2026-06-29-metis-lessons-session-live-bff-coverage

Purpose#

A "pick it up here" framing over the lesson backlog: server-side it reads /v1/metis/lessons, picks the first lesson with assessmentReady=true (else the first lesson) as the one to study now, queues the next two, and shows an estimated total session length. Links into the existing lesson and tutor pages. See V1/features.md#metis.

Entry points#

  • Metis lessons index — "Today's session" ghost button on metis-lessons.md links here
  • Metis schoolroom — verify whether metis.md surfaces a "today's session" CTA
  • Direct URL / bookmark — yes (auth required); canonical /metis/session
  • Search result — page metadata title "Metis · today's session"

Layout regions#

page.tsx is an async server component: await bffGet<LessonsResponse>('/v1/metis/lessons') (falls back to empty), computes next, a 2-item queue, and totalMin, rendered inside <LWebShell top={<LCustomerNav active="explore" />}> in a 720px-max centered column.

  • Header: web shell top — LCustomerNav with active="explore"
  • Masthead: LMasthead — left "Metis · today", right "<totalMin> min planned" (or "nothing queued"), kicker "Pick it up here", title "Today's session."
  • Main (Start here): an accent-bordered <section> for the next lesson — eyebrow "Start here", course/week/minutes meta, <h2> title, action buttons
  • Next in line: a <section> listing the up-to-two queue lessons (only when queue.length > 0)
  • Footer: button row — "← Back to Metis", "All lessons", "Live tutor"
  • Automation anchors: root exposes data-metis-session-page, data-route, data-generated-at, lesson/queue/total-minute/assessment counts, chosen next lesson id, and data-empty; the start card exposes data-metis-session-start; queued rows expose data-metis-session-queue-row plus lesson/week/minute/assessment attributes.

States#

  • Has next lesson — accent-bordered "Start here" card with the chosen lesson; "Open the lesson" primary button
  • Next is assessment-ready — additional ghost button "Skip to the assessment" appears alongside "Open the lesson"
  • No active lessons — dashed-border card "No active lessons. Start a course in /metis to begin a session."; masthead right reads "nothing queued"
  • Queue populated — "Next in line" section with 2 lesson rows from the live guarded BFF fixture
  • Queue empty — "Next in line" section omitted (e.g. only one lesson)
  • Error / null fetchbffGet null → empty → renders the "No active lessons" card (no distinct error banner)
  • Loading — server-rendered; no route-level loading.tsx in folder
  • Offline — first load needs the BFF; failure renders empty; verify SW HTML cache for offline reload
  • Signed-in shell context — walked through an authenticated customer shell with the Explore nav active
  • Standalone PWA — mobile standalone launch renders within LWebShell without horizontal overflow

Interactions#

Start-here card#

  • Start-here meta — non-interactive: "<courseLabel> · week <week>/<weekTotal> · <minutesEstimated> min" (mono uppercase)
  • Lesson title (<h2>) — non-interactive serif heading
  • Open the lesson (LBtn kind="primary" size="lg")
    • Function: navigates to /metis/lesson?id=<encodeURIComponent(next.id)>
    • Keyboard: Enter activates; primary action on the page
    • Touch target: ≥ 44×44 px via LBtn size="lg"
  • Skip to the assessment (LBtn kind="ghost" size="lg", only when next.assessmentReady)
    • Function: navigates to /metis/assessment (no lesson id param — see gaps)

Next-in-line rows#

  • Queue row (<li>) — non-interactive: course/week/minutes meta line + serif title; no link to open these directly from here
  • ← Back to Metis (LBtn kind="ghost" size="md", href="/metis")
  • All lessons (LBtn kind="ghost" size="md", href="/metis/lessons")
  • Live tutor (LBtn kind="primary" size="md", href="/metis/tutor")

Data & contracts#

  • Reads: GET /v1/metis/lessons via bffGet<LessonsResponse> (same fixture as metis-lessons.md); revalidate: 60s, 5000ms timeout; returns null on failure → { generatedAt, lessons: [] }
    • Local Lesson interface here is a subset: { id, courseLabel, title, week, weekTotal, minutesEstimated, assessmentReady } (no courseId/sectionNumber)
    • Selection: next = lessons.find(l => l.assessmentReady) ?? lessons[0]; queue = lessons.filter(l => l.id !== next?.id).slice(0, 2); totalMin = next.minutesEstimated + sum(queue.minutesEstimated)
    • Current guarded fixture yields phl-102-week-2 as next, two queued rows (phl-102-week-3, ast-110-week-1), and 58 planned minutes.
  • Writes: None.
  • Realtime: None.
  • Caching: SSR with Next revalidate: 60; SW HTML cache for offline reload
  • Auth/role check: page is walked through a signed-in customer shell; bffGet forwards the session cookie. The current domain-stubs BFF endpoint is production-guarded fixture data, not an auth-scoped per-user route.

No typed contract in libs/contracts//libs/openapi/; shapes are inline.

Cross-references#

Open questions / known gaps#

  • "Skip to the assessment" links to /metis/assessment without passing the lesson id — confirm whether the assessment page needs to know which lesson it is assessing
  • "Next in line" rows are non-interactive (no per-lesson open link) unlike the lessons index — confirm whether they should be clickable
  • Selection logic prefers any assessment-ready lesson over true recency; confirm this matches the intended "study now" heuristic
  • /v1/metis/lessons is a production-guarded fixture; confirm the real per-user backlog source
  • A null fetch silently renders the empty state rather than an error/retry — confirm desired behavior