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
projectmaps to an LMS course, top-levelpage-hierarchynodes 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
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 theMainNavdriven bygetPageHierarchy(). The CMS contract is adapted in the LMS: lowercaseproject.acronymis the course slug, top-levelpage-hierarchynodes 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/callsenrollInCourse(rate-limited, audit-logged, idempotent). The enrolled course home renders a Resume CTA vialistEntrySlugsInOrder+ per-entry progress./learn/course/\[slug\]/entry/\[entrySlug\]/ships withgenerateStaticParams(published-only),revalidate = 60, theCourseSidebar(zustand-persisted,[toggle),EntryNavprev/next,Breadcrumbs, and dispatchers toLessonPlayer,QuizPlayer, orAssignmentPlaceholder. 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) withadminEnrollStudentandadminCancelEnrollmentserver actions; both are audit-logged. - Cache invalidation:
POST /api/cms/revalidateaccepts 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.
- Zod schemas in
src/schemas/coveringCourse,Module,Entry,Quiz/QuizPublic,EnrollmentRequest,QuizAttemptSubmission,PageHierarchyNode, and a genericpaginated<T>envelope. - Typed CMS fetch wrapper in
src/lib/cms/client.ts— sendsx-api-key: ${CMS_API_UUID}, scopes byCMS_PROJECT_UUID, threadslocale, appliesnext: { revalidate: 60, tags }, and throws a typedCmsIntegrationErroron failure. - Published-only filter in
src/lib/cms/published.tsconsumed bygenerateStaticParamsand runtime fetches. - Project-asset proxies for CMS-hosted binaries:
src/lib/cms/project-asset-proxy.tsandsrc/lib/cms/rewrite-asset-urls.ts. - OpenAPI generator:
scripts/openapi-generate.tsemitspublic/openapi.yaml. Swagger UI gated totenant_admin/super_adminunder the admin route segment (src/app/\[locale\]/admin).
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. MainNavis built fromgetPageHierarchy()so the rendered menu mirrors the CMS structure.- Marketing chrome copy uses translation keys; CMS body copy is rendered as authored.
- ISR
revalidate = 60on every page;generateStaticParamsincludes 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.mdunder Implementation Plan Follow-Ups. The bullets below describe the target contract for the build, not what is currently in tree. Today the public landing'sEnrollCTAlinks 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 theenrollmentsrow scoped bytenant_id+user_id, writes an audit event, and revalidatesprogress:<userId>. - Free courses become
activeimmediately. Paid courses keep the provider-neutral seam undersrc/server/payments/and currently return501 Not Implementedwith user-safe copy until a payment phase ships. - Re-enrollment from
cancelledupdates the same row, bumpsenrolled_at, clearscompleted_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 underlearn/course/\[slug\]/, and nomarkEntryComplete/recordEntryViewserver actions exist in tree. Partial infrastructure only:entry_progressmigration and theresumeTargetquery helper. Tracked inroadmap.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.tsexposesgetEntryAccess({ user, course, entry })returning the canonical access modes (enrolled,gated,previewReadOnly,notEnrolled,notFound). Callers must not reimplement gating. markEntryCompleteandrecordEntryViewserver actions; revalidation tagscourse:<slug>andprogress:<userId>.- Admin preview-as-student mode (
?preview=1) for assignedcourse_admin,tenant_admin, andsuper_admin. Read-only — noentry_progressorquiz_attemptswrites. CourseSidebarfollows 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, andMarkCompleteButtonare not built yet. Depends on Phase 7. Tracked inroadmap.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
MarkCompleteButtonpath; 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)¶
npm run check,npm run build,npm run test:components— clean.npm run test:unit— schema fixtures, published filter, and CMS client tests pass.- Manual smoke: home → generic CMS page → blog index → blog post → course catalogue → public course landing (CTA visible; enroll click 404s until Phase 6).
- 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.