---
path: /metis/lessons
surface: customer
domain: metis
auth: signed-in
source: apps/oshun/web/src/app/metis/lessons/page.tsx
status: walked
last_walked:
  '2026-06-29 real-infra Playwright chromium — live guarded BFF multi-course
  fixture, signed-in customer shell, Explore nav, course grouping, lesson rows,
  assessment-ready state, hrefs, 44px lesson/course tap targets, 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'
---

# Metis · lessons index

## Purpose

The schoolroom backlog: every active lesson across the user's courses, grouped
by course, each row showing section number, week N/M, minutes estimated, and
whether its assessment is ready. Read-only index page — server-rendered from
`/v1/metis/lessons`. See
[`V1/features.md#metis`](../../../V1/features.md#metis).

## Entry points

- **Metis schoolroom** — verify whether [`metis.md`](./metis.md) links to the
  lessons backlog
- **Metis today's session** — "All lessons" ghost button on
  [`metis-session.md`](./metis-session.md) links here
- **Direct URL / bookmark** — yes (auth required); `canonical` `/metis/lessons`
- **Search result** — page metadata title "Metis · lessons"

## Layout regions

`page.tsx` is an async server component:
`await bffGet<LessonsResponse>('/v1/metis/lessons')` (falls back to
`{ lessons: [] }` on null), groups lessons by `courseId`, renders inside
`<LWebShell top={<LCustomerNav active="explore" />}>` in an 880px-max centered
column.

- **Header**: web shell top — `LCustomerNav` with `active="explore"`
- **Masthead**: `LMasthead` — left "Metis · lessons", right "`<count>` active",
  kicker "The schoolroom backlog", title "The _lessons_."
- **Intro**: `LEyebrow` "How this opens" + serif-italic explainer
- **Main**: one `<section>` per course — an `<h2>` with `courseLabel` plus an
  "Open course →" link, then an `<ol>` of lesson `<li>` rows
- **Footer**: a button row — "← Back to Metis", "Today's session", "Propose a
  course"
- **Automation anchors**: root exposes `data-metis-lessons-page`, `data-route`,
  `data-generated-at`, lesson/course/assessment-ready counts, and `data-empty`;
  course sections expose `data-metis-lessons-course` plus course/count
  attributes; lesson rows expose `data-metis-lessons-row` plus
  lesson/course/week/minute/assessment attributes.

## States

- [x] **Populated (short)** — 3 lessons across 2 course sections from the live
      guarded BFF fixture; grouped layout
- [ ] **Populated (long)** — 50+ lessons / many courses; verify scroll and that
      grouping stays performant (no pagination present)
- [ ] **Empty** — `byCourse.length === 0` → dashed-border card "No active
      lessons. Start a course to populate the backlog."
- [x] **Per-lesson assessment ready** — row link reads "open · quiz ready" in
      `L.accent`; when not ready it reads "open →" in `L.muted`
- [ ] **Error / null fetch** — `bffGet` returns `null` on non-2xx/timeout →
      falls back to empty (renders the Empty card, not an error banner)
- [x] **Loading** — server-rendered; no route-level `loading.tsx` skeleton in
      the route folder
- [ ] **Offline** — first load needs the BFF; on failure renders Empty; verify
      SW caches the rendered HTML for offline reload
- [x] **Signed-in shell context** — walked through an authenticated customer
      shell with the Explore nav active
- [x] **Standalone PWA** — mobile standalone launch renders within `LWebShell`
      without horizontal overflow

## Interactions

### Per-course section

- [x] **Course heading** (`<h2>`) — non-interactive; shows
      `courseLabel ?? courseId`
- [x] **Open course →** (`Link`)
  - Function: navigates to
    `/metis/lesson?courseId=<encodeURIComponent(courseId)>`
  - Screen reader: announces "Open course `<courseLabel>`"
  - Styling: mono, uppercase, `L.accent`
  - Touch target: verified ≥ 44×44 px

### Per-lesson row (`<li>`)

- [x] **Lesson meta line** — non-interactive: "§`<sectionNumber>` · week
      `<week>`/`<weekTotal>` · `<minutesEstimated>` min" (mono uppercase)
- [x] **Lesson title** — non-interactive serif text
- [x] **Open lesson link** (`Link`)
  - Function: navigates to `/metis/lesson?id=<encodeURIComponent(lesson.id)>`
  - Label: "open · quiz ready" when `assessmentReady`, else "open →"
  - Screen reader: announces "Open lesson `<title>`" and appends "(quiz ready)"
    when relevant; `whiteSpace: nowrap`
  - Touch target: verified ≥ 44×44 px

### Footer buttons

- [x] **← Back to Metis** (`LBtn kind="ghost" size="md"`, `href="/metis"`)
- [x] **Today's session** (`LBtn kind="ghost" size="md"`,
      `href="/metis/session"`)
- [x] **Propose a course** (`LBtn kind="primary" size="md"`,
      `href="/metis/courses/new"`)

## Data & contracts

- **Reads**: `GET /v1/metis/lessons` via `bffGet<LessonsResponse>` (server-side,
  forwards `oshun-session`/`__session` cookie; `revalidate: 60s` default;
  `timeout 5000ms`); returns `null` on failure → `{ generatedAt, lessons: [] }`
  - `LessonsResponse = { generatedAt: string; lessons: Lesson[] }`
  - `Lesson = { id, courseId, courseLabel, title, week, weekTotal, sectionNumber, minutesEstimated, assessmentReady }`
  - BFF serves the production-guarded `METIS_LESSONS_FIXTURE` with 3 lessons
    across 2 courses (`phl-102`, `ast-110`), exactly one `assessmentReady=true`,
    and 58 total planned minutes for session consumers
    (`apps/oshun/bff/src/routes/domain-stubs.ts`)
- **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 in
`page.tsx`.

## Cross-references

- Feature spec: [`V1/features.md#metis`](../../../V1/features.md#metis)
- Architecture: [`V1/ARCHITECTURE.md#metis`](../../../V1/ARCHITECTURE.md#metis)
- Sibling Metis routes: [`metis.md`](./metis.md),
  [`metis-session.md`](./metis-session.md),
  [`metis-lesson.md`](./metis-lesson.md),
  [`metis-courses-new.md`](./metis-courses-new.md),
  [`metis-assessment.md`](./metis-assessment.md)
- Journeys:
  [`../../journeys/metis-lesson-completion.md`](../../journeys/metis-lesson-completion.md)
- Component sources:
  - `apps/oshun/web/src/app/metis/lessons/page.tsx`
  - BFF: `apps/oshun/bff/src/routes/domain-stubs.ts`

## Open questions / known gaps

- [ ] No pagination/virtualization — confirm expected behavior for a large
      backlog (the page renders all lessons inline)
- [ ] Rows link to `/metis/lesson` (singular) with `id`/`courseId` query params;
      the hrefs are proven, but confirm the destination page's query-param
      behavior against the real lesson source (see
      [`metis-lesson.md`](./metis-lesson.md))
- [ ] `/v1/metis/lessons` is still a production-guarded fixture
      (`METIS_LESSONS_FIXTURE`); confirm the real backlog source and per-user
      filtering
- [ ] A null fetch silently renders Empty rather than a distinct error state —
      confirm whether an error/retry affordance is desired
