My learning path

The Learning app does not just list courses. It leads a signed-in learner into a complete learning path: a few questions about the project they want to build, then the courses, then a schedule. The route has eight steps, LearningSchedule.TotalSteps:

Steps Stage (LearningSchedule.Stage) Where it happens
1–6 Questions: the LearningRoadmap questions (project, outcome, experience, confidence, pace, language) the learner's plan, {viewer}/LearningRoadmap/Plan
7 PickCourses: the questions are done but the route holds no course the plan page lists every course on the instance with Add to my path
8 Schedule: courses are planned, and some lack a start or finish day each stop on the plan page carries its date inputs
✓ Complete: every planned course is dated or kept for later the front door names what is due next

NoPlan is the state before step 1.

The front door (Edu/Courses)

The hero card (MyLearning.Project) always shows one button that resumes exactly where the learner stands, the step ("Step 7 of 8") and the platform's progress control:

A saved legacy plan (from before the guided roadmap) just opens. The roadmap page itself offers to rebuild it.

Every course card carries the path action (MyLearning.PathActions):

The plan page

The roadmap page's questionnaire progress now counts against the whole route ("Step 2 of 8"). After the questions, a guide panel under the hero shows the step, the progress and the one line of what to do next (GuideStep / GuideLine). Two computed slots join the stops:

They are computed slots for the same reason the stops already were: each button or input addresses its own course key, which a bound row cannot carry.

The data — the learner's own typed node

Everything is stored on the learner's own plan node (Edu/LearningJourney, LearningJourneyContent). Nothing is copied into a layout-area replica:

Every date input is a DateTimeControl bound directly to the plan node at courses/{key}/targetStart, …/targetFinish and …/milestones/{m}/due, the same way the stop's notes already were. There is no save button and no /data copy.

🚨 Dates are calendar facts. A target is the day the learner picked. It is shown as that day (LearningSchedule.DateText), compared as a day, and never converted to a time zone; a test pins that 23:30 UTC on the 12th still reads "12 March". "Today" for the overdue check is UTC's date: a target is a day, and a boundary a few hours off for one viewer only changes whether that day already counts as overdue.

A course the learner adds is marked Picked, and a rebuilt recommendation keeps picked courses on the route; only recommended courses outside the new route move to an earlier plan. Completion for the next due item reads the plan's own courses as well as the catalog's, so a finished course the catalog no longer lists is never advertised as due.

Adding never duplicates (LearningJourney.AddFromCatalog). A course already on the route is left exactly as it is, and an entry kept from an earlier plan comes back with its own dates. Rebuilding the route after new answers keeps the learner's schedule: the course dates, and each module's date matched by its page wherever it now sits.

LearningCourse has value equality including the module map. A record compares an ImmutableDictionary by reference, which made two identical recommendations read as different.

Tests

Edu/LearningJourney → LearningScheduleTests (every route state, next due item, calendar dates, adding, rebuild, the guide, the add confirmation) and Edu/CourseCatalog → MyLearningTests.Hero_LeadsThroughEveryState (no plan, questions, no match, partial, complete, German) and Cards_OfferAddOrSchedule.