Skip to content

Milestone 2: Core Application

Status: Tracking. Phases 4–7.5 are implemented in tree. The CMS contract has been adapted in the LMS so that a CMS project maps to an LMS course, top-level page-hierarchy nodes map to modules, and leaf nodes map to entries; entry types are resolved by widget-sniff (src/lib/course/entry-classifier.ts). Enrollment, entry routes, progress writes, the toggleable course sidebar, a basic quiz player, the assignment placeholder, admin manual-enrol, and the CMS revalidate webhook (/api/cms/revalidate) all ship in this milestone. See code as authoritative when this milestone diverges.

Reading guide: Detailed phase prose lives in reference/implementation.md. When this milestone and the implementation plan disagree, the implementation plan and the code itself win.

See also:

  • Part 1: Foundational — Project scaffold, design system, persistence, Auth.js, email.
  • Part 3: Operationalization — Quiz, dashboards, errors/security, well-known files.
  • Part 4: Hand-off and Roadmap — Testing/CI, documentation, creator/marketplace proposals.

Overview

This milestone delivers the core learner experience: a typed boundary into the headless CMS, the public marketing site, the enrollment state machine, sequential entry playback with progress tracking, and an assignment placeholder that keeps assignment-bearing courses routable.

Dependency order

Schemas + CMS Client → Public Website → Enrollment → Entry Pagination → Assignment Placeholder

Phase Checklist

Phase Title Status Source
4 Schemas, CMS Client, OpenAPI [x] Phase 4
5 Public Website (no auth) [x] Phase 5
6 Enrollment Flow [x] Phase 6
7 Entry Pagination & Progress [x] Phase 7
7.5 Assignment Entry Placeholder [x] Phase 7.5

Current implementation status

  • Shipped (Phases 4–7.5): Zod schemas, CMS client + asset proxies, published-only filter, OpenAPI generator + admin Swagger UI, all marketing routes under app/\[locale\]/, and the MainNav driven by getPageHierarchy(). The CMS contract is adapted in the LMS: lowercase project.acronym is the course slug, top-level page-hierarchy nodes become modules, and leaf nodes become entries. Entry types are inferred via widget-sniff (src/lib/course/entry-classifier.ts).
  • Enrollment + entry playback: /learn/course/\[slug\]/enroll/ calls enrollInCourse (rate-limited, audit-logged, idempotent). The enrolled course home renders a Resume CTA via listEntrySlugsInOrder + per-entry progress. /learn/course/\[slug\]/entry/\[entrySlug\]/ ships with generateStaticParams (published-only), revalidate = 60, the CourseSidebar (zustand-persisted, [ toggle), EntryNav prev/next, Breadcrumbs, and dispatchers to LessonPlayer, QuizPlayer, or AssignmentPlaceholder. Server actions: markEntryViewedAction, markEntryCompleteAction, submitQuizAttemptAction (each rate-limited, audit-logged on first completion / submission).
  • Admin manual enrol: /admin/courses/\[slug\]/students/ lists the roster (tenant-scoped) with adminEnrollStudent and adminCancelEnrollment server actions; both are audit-logged.
  • Cache invalidation: POST /api/cms/revalidate accepts a Bearer secret + Zod-validated tag prefixes (course:, entry:, site:, cms:page-hierarchy, progress:) and is rate-limited at 60 req/min/IP.

Phase 4 — Schemas, CMS Client, OpenAPI

Goal: typed boundary into the headless CMS and a generated OpenAPI surface for our own API routes.

Reference detail: reference/implementation.md — Phase 4, reference/hierarchy-structure.md, reference/quiz-system.md.

Phase 5 — Public Website (no auth)

Goal: statically generated marketing surface and course catalogue.

  • Routes under app/\[locale\]/: home, generic CMS pages (/\[slug\]), blog index, blog post, public course landing.
  • MainNav is built from getPageHierarchy() so the rendered menu mirrors the CMS structure.
  • Marketing chrome copy uses translation keys; CMS body copy is rendered as authored.
  • ISR revalidate = 60 on every page; generateStaticParams includes published content only.
  • Per-tenant well-known files (favicon, manifest, robots, sitemap, OG/Twitter cards, .well-known/*) ship in Phase 10.5 (Milestone 3).

Reference detail: reference/implementation.md — Phase 5, reference/hierarchy-structure.md, guides/learner-experience.md, guides/content-rendering.md.

Phase 6 — Enrollment Flow

Status: Not implemented. Outstanding work tracked in roadmap.md under Implementation Plan Follow-Ups. The bullets below describe the target contract for the build, not what is currently in tree. Today the public landing's EnrollCTA links to /learn/course/{slug}/enroll, which 404s until this phase lands.

Goal: an authenticated learner can enrol in a published course.

  • Route: app/\[locale\]/learn/course/\[slug\]/enroll/page.tsx.
  • Server action enrollInCourse(courseSlug) validates the slug, verifies the course is published, upserts the enrollments row scoped by tenant_id + user_id, writes an audit event, and revalidates progress:<userId>.
  • Free courses become active immediately. Paid courses keep the provider-neutral seam under src/server/payments/ and currently return 501 Not Implemented with user-safe copy until a payment phase ships.
  • Re-enrollment from cancelled updates the same row, bumps enrolled_at, clears completed_at, and writes an audit event.
  • UI: enroll CTA (today: EnrollCTA / EnrollCTAView), CourseModuleList, EntryRow (completed / locked / available). All status copy is translation-keyed.

Reference detail: reference/implementation.md — Phase 6 and reference/course-enrollment-system.md.

Phase 7 — Entry Pagination & Progress

Status: Not implemented. No src/lib/course/ helpers, no entry route under learn/course/\[slug\]/, and no markEntryComplete / recordEntryView server actions exist in tree. Partial infrastructure only: entry_progress migration and the resumeTarget query helper. Tracked in roadmap.md.

Goal: sequential lesson playback with prev/next, completion tracking, and a toggleable course sidebar.

  • Pure pagination helper: src/lib/course/pagination.ts (flattens modules → entries; module boundaries are visual).
  • Central gating helper: src/lib/course/gating.ts exposes getEntryAccess({ user, course, entry }) returning the canonical access modes (enrolled, gated, previewReadOnly, notEnrolled, notFound). Callers must not reimplement gating.
  • markEntryComplete and recordEntryView server actions; revalidation tags course:<slug> and progress:<userId>.
  • Admin preview-as-student mode (?preview=1) for assigned course_admin, tenant_admin, and super_admin. Read-only — no entry_progress or quiz_attempts writes.
  • CourseSidebar follows the Toggleable Navigation contract: non-persisted Zustand store, CSS flexbox layout, [ keyboard shortcut, locked-row toast.

Reference detail: reference/implementation.md — Phase 7, reference/entry-pagination.md, reference/toggleable-navigation.md.

Phase 7.5 — Assignment Entry Placeholder

Status: Not implemented. _type: 'assignment' is already accepted by the Zod entry schema (src/schemas/cms/entry.ts), but the entry route, placeholder UI, and MarkCompleteButton are not built yet. Depends on Phase 7. Tracked in roadmap.md.

Goal: assignment entries are routable and clearly marked as placeholders without implying full submission/grading support.

  • _type: 'assignment' is supported in schemas and routing so courses with assignments do not crash.
  • Body/instructions render via the same Portable Text serialiser used for lessons.
  • Manual MarkCompleteButton path; no upload, grading queue, or submission persistence in V1.
  • Translation-keyed placeholder copy explicitly states submissions are not yet implemented.

Real assignment submission and grading is tracked as F9 in roadmap.md.

Reference detail: reference/implementation.md — Phase 7.5, reference/hierarchy-structure.md.

Core Application Verification

Phases 4–5 (runnable today)

  1. npm run check, npm run build, npm run test:components — clean.
  2. npm run test:unit — schema fixtures, published filter, and CMS client tests pass.
  3. Manual smoke: home → generic CMS page → blog index → blog post → course catalogue → public course landing (CTA visible; enroll click 404s until Phase 6).
  4. Locale switch on at least one route in this milestone, asserting the menu and chrome both translate.

Phases 6–7.5 (blocked until implemented)

The following can only be verified once the outstanding phases ship:

  • Enrol → course home → first entry → prev/next → mark complete smoke.
  • Pagination and gating unit tests (src/lib/course/* does not exist yet).
  • Assignment-entry placeholder rendering and manual mark-complete.

Outstanding items roll into roadmap.md.