[Bug] Lessons passed offline appear locked again after page refresh #72

Closed
opened 2026-05-06 04:21:21 +00:00 by spikerj · 0 comments
Owner

Summary

When a student passes a lesson while offline / in-browser compile is on, the lesson is correctly marked as complete locally for the rest of the session. After a page refresh, however, the lesson appears locked again — its prerequisite isn't satisfied because the local completedLessonsSignal was rebuilt from the server's /Lessons/progress only, ignoring the offline pass that's still sitting in the IndexedDB outbox waiting to flush.

Reported by reproduction: passed C# lesson 105 offline, refreshed, lesson 105 became locked.

Root cause

  1. LanguageRunner.loadCatalogAndProgress() populates completedLessonsSignal from lessonCatalog.getProgress() only. The IDB pending queue (pendingProgress store) is never consulted.
  2. ProgressSyncService.flush() is only triggered by online and visibilitychange→visible events. Neither fires on a normal page load (already online, already visible), so a refresh while online doesn't drain the queue immediately. The student has to switch tabs and back, or trigger another submit, before the queue is sent.
  3. QueuedProgress carries no language field, so even if we did read the queue, we'd need an additional defensive filter against the active catalog to avoid leaking pending Python lesson 105 into the C# completed set (the IDB is shared across all three playgrounds despite its spikersoft-csharp-playground name).

Expected behavior

An offline pass should survive a page refresh:

  • The lesson should remain marked as completed (locally provisional) so prerequisites for downstream lessons stay satisfied.
  • On the next online opportunity (initial page load if already online, or the existing online / visibilitychange triggers), the queue should flush; the server's /Lessons/progress/batch response promotes the lesson from provisional to verified, or rejects it (in which case the local mark is removed by the existing lastFlushItemsSignal handler).

Acceptance criteria

  • Passing lesson N offline and refreshing the page keeps lesson N marked as complete and its successors unlocked.
  • On page load with navigator.onLine === true, ProgressSyncService.flush() is invoked once so any queued items reach the server without requiring tab focus changes.
  • Pending queue items are scoped to the active language so a Python pass can't unlock the same number in C#.
  • Server-side rejection (server_grading_failed / state_expired) still removes the provisional mark via the existing lastFlushItemsSignal handler.
  • Unit tests cover the merge (server progress + pending passed), the opportunistic flush on init, and the language scoping.

Scope hints

  • libraries/tools/src/services/csharp-runner/progress-sync.service.ts — add a getPendingPassedLessonNumbers(language) helper, expose flush() for explicit init-time call.
  • libraries/tools/src/services/csharp-runner/csharp-playground.idb.ts — add optional language?: 'csharp'|'python'|'javascript' to QueuedProgress.
  • libraries/tools/src/services/{csharp,python,javascript}-runner/*.service.ts — pass language when enqueueing.
  • libraries/tools/src/components/language-runner/language-runner.ts — union the pending-passed numbers into completedLessonsSignal inside loadCatalogAndProgress, and call progressSync.flush() opportunistically from ngOnInit.
## Summary When a student passes a lesson while **offline / in-browser compile** is on, the lesson is correctly marked as complete locally for the rest of the session. After a **page refresh**, however, the lesson appears **locked** again — its prerequisite isn't satisfied because the local `completedLessonsSignal` was rebuilt from the server's `/Lessons/progress` only, ignoring the offline pass that's still sitting in the IndexedDB outbox waiting to flush. Reported by reproduction: passed C# lesson 105 offline, refreshed, lesson 105 became locked. ## Root cause 1. `LanguageRunner.loadCatalogAndProgress()` populates `completedLessonsSignal` from `lessonCatalog.getProgress()` only. The IDB pending queue (`pendingProgress` store) is never consulted. 2. `ProgressSyncService.flush()` is only triggered by `online` and `visibilitychange→visible` events. Neither fires on a normal page load (already online, already visible), so a refresh while online doesn't drain the queue immediately. The student has to switch tabs and back, or trigger another submit, before the queue is sent. 3. `QueuedProgress` carries no `language` field, so even if we did read the queue, we'd need an additional defensive filter against the active catalog to avoid leaking pending Python lesson 105 into the C# completed set (the IDB is shared across all three playgrounds despite its `spikersoft-csharp-playground` name). ## Expected behavior An offline pass should survive a page refresh: * The lesson should remain marked as **completed (locally provisional)** so prerequisites for downstream lessons stay satisfied. * On the next online opportunity (initial page load if already online, or the existing `online` / `visibilitychange` triggers), the queue should flush; the server's `/Lessons/progress/batch` response promotes the lesson from provisional to verified, or rejects it (in which case the local mark is removed by the existing `lastFlushItemsSignal` handler). ## Acceptance criteria * [ ] Passing lesson N offline and refreshing the page keeps lesson N marked as complete and its successors unlocked. * [ ] On page load with `navigator.onLine === true`, `ProgressSyncService.flush()` is invoked once so any queued items reach the server without requiring tab focus changes. * [ ] Pending queue items are scoped to the active language so a Python pass can't unlock the same number in C#. * [ ] Server-side rejection (`server_grading_failed` / `state_expired`) still removes the provisional mark via the existing `lastFlushItemsSignal` handler. * [ ] Unit tests cover the merge (server progress + pending passed), the opportunistic flush on init, and the language scoping. ## Scope hints * `libraries/tools/src/services/csharp-runner/progress-sync.service.ts` — add a `getPendingPassedLessonNumbers(language)` helper, expose `flush()` for explicit init-time call. * `libraries/tools/src/services/csharp-runner/csharp-playground.idb.ts` — add optional `language?: 'csharp'|'python'|'javascript'` to `QueuedProgress`. * `libraries/tools/src/services/{csharp,python,javascript}-runner/*.service.ts` — pass `language` when enqueueing. * `libraries/tools/src/components/language-runner/language-runner.ts` — union the pending-passed numbers into `completedLessonsSignal` inside `loadCatalogAndProgress`, and call `progressSync.flush()` opportunistically from `ngOnInit`.
Sign in to join this conversation.