V1 Web PWA · Surface walkthrough

Metis · the schoolroom

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 · auth signed-in · source apps/oshun/web/src/app/metis/page.tsx

Last walked. 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; course-card links, first-run no-assessment state, and hub CTAs reconciled 2026-06-27 by metis-lesson-completion.spec.ts

Purpose#

The Metis domain hub. The schoolroom: active courses, this week's progress stats, the open assessment, and a tutor grounded in the sources. The recommended next action resumes a specific course at a specific section when the BFF returns an in-progress cursor; the live first-run state instead says "Begin your first lesson" and keeps the same lesson CTA route.

Entry points#

  • Home domain card grid — see ../02-home-discovery/home.md
  • Domain switcher/switcher (see ../02-home-discovery/switcher.md)
  • Library / Explore — learning-reentry collection and curated routes link here
  • Direct URL / bookmark — yes (auth required via shell middleware)
  • Server fetchgetMetis() runs on the server before render

Layout regions#

page.tsx is a thin server component: await getMetis()<MetisSchoolroom data={data} />.

Inside MetisSchoolroom (metis.tsx, line 65):

  • Web shell topLCustomerNav active="library"
  • Header row (gridTemplateColumns: '1fr auto'):
    • Left — Resume / first-run headline — Metis glyph (LDomainMark domain="metis"), eyebrow "Metis · the schoolroom", LDisplay "Resume at §
      ." when resume.weekTotal > 0; otherwise "Begin your first lesson." The meta line is either <courseLabel> · week <n> of <total> · <minutes> minutes today or "No lesson in progress yet — open a course below to start."
    • Right — LBtn kind="primary" href="/metis/lesson" — "Continue lesson →" or first-run "Start a lesson →"
  • Two-column body (gridTemplateColumns: '1.4fr 1fr'):
    • Left — Open courses · N — list of Link-wrapped LCards per data.courses, each with code, week/weekTotal, title, teacher, progress bar, and href="/metis/lesson?course=<code>"; the here course gets a 3 px accent left border and bg colour
    • Right — sidebar:
      • This weekLCard with LField rows for Lessons read, Assessments, Tutor minutes, Streak (from data.weekStats)
      • Open assessmentLCard (accent border). When data.openAssessment.open is true it shows code, title, kind, word count, due label, "Begin" primary button to /metis/assessment, and "Rubric" ghost button. When false it shows "No assessment is open." plus the honest explanatory eyebrow and hides Begin/Rubric.
      • Tutor · ✻LCard (dotted border) with a paragraph from data.tutorNudge and eyebrow "AI tutor · grounded in 4 sources · disclosed"

States#

  • LoadedgetMetis() resolves before render; covered by metis-lesson-completion.spec.ts.
  • Resume present / first-rundata.resume.sectionNumber populates the resume headline when weekTotal > 0; the live first-run BFF state renders "Begin your first lesson." instead.
  • Open courses populated — one linked card per data.courses; the E2E count and click-through are covered.
  • Active course (here === true) — highlighted with accent border in source.
  • Week stats populateddata.weekStats field rows (lessons read, assessments, tutor minutes, streak)
  • Open assessment presentdata.openAssessment.open === true populates the sidebar card and exposes Begin/Rubric.
  • No open assessment — live first-run BFF state shows "No assessment is open." and hides Begin/Rubric; covered in metis-lesson-completion.spec.ts.
  • Tutor nudge presentdata.tutorNudge italic paragraph and visible "AI tutor · grounded in 4 sources · disclosed" eyebrow.
  • Reduced motion — composes static Lilith primitives; no route-level animation on this hub.
  • Auth gate — shell middleware only.

Interactions#

  • Continue lesson → / Start a lesson → (LBtn kind="primary")
    • Function: navigates to /metis/lesson
    • Disabled when: no in-source disabled branch — always rendered
    • Covered by: metis-lesson-completion.spec.ts

Open courses lane#

  • Course card (Link wrapping LCard) — navigates to /metis/lesson?course=<code>; covered by browser click-through.
  • Progress bar — non-interactive.
  • LField rows — non-interactive (Lessons read, Assessments, Tutor minutes, Streak)
  • Begin (LBtn kind="primary" href="/metis/assessment")
    • Function: navigates to /metis/assessment
  • Rubric (LBtn kind="ghost")
    • Function: display-only in V1; no destination or modal handler is wired.
  • Tutor card — non-interactive paragraph + eyebrow; no href wired.

Data & contracts#

  • Reads: getMetis() from @/lib/lilith-data/metis — fetches the live BFF route GET /v1/metis/room via bffGet (metis.ts ~14-19) and returns MetisData with resume, courses[], weekStats, openAssessment, tutorNudge. On an unreachable feed it returns an honest metisUnavailable() room (no embedded fixture).
  • Writes: None — read-only hub.
  • Realtime: None.
  • Caching: server fetch; SW caches the rendered HTML for offline reload
  • Auth/role check: shell middleware
  • Route metadata: title "Metis · the schoolroom", canonical /metis

Cross-references#

Open questions / known gaps#

  • Course cards are real Links to /metis/lesson?course=<code>; no separate per-course detail page is mounted.
  • The Rubric ghost button is display-only in V1; the rubric preview itself lives on /metis/assessment.
  • MetisData is served live from GET /v1/metis/room; the browser spec asserts the live first-run/no-assessment shape instead of page-routing a fake server component payload.
  • Tutor nudge card integration is confirmed absent in V1; direct URL /metis/tutor is the reachable tutor entry.