Students using In-Browser Compile mode report losing progress: they complete a lesson (graded locally, shown green / next lesson unlocks), then take an action that should be harmless — most commonly an immediate hard refresh — and the lesson re-locks as if it was never completed.
Root cause
A locally-graded pass is written to the offline outbox (pendingProgress in IndexedDB) and then flushed to POST /Lessons/progress/batch. The flush deletes the outbox row as soon as the server accepts the batch — even when the server only accepts it as unverified and hasn't yet surfaced it through GET /progress (the server has an async unverified → verified lifecycle).
On reload, LanguageRunner seeds completed lessons from server progress ∪ outbox-pending. In the window between "flush accepted" and "server reports the pass", both sources miss it:
the outbox row was already deleted on accept, and
the server doesn't yet return it from GET /progress.
A hard refresh during that window re-locks the lesson → perceived progress loss.
Expected behavior
Once a lesson is completed locally, completion must never regress on a mere refresh on the same device. The server stays authoritative for cross-device progress, but local completion should be a durable floor.
Fix
Add a durable local completion ledger (a new IndexedDB store, completedLessons) that is:
written at pass time, independent of the network flush lifecycle;
never deleted when the outbox row is flushed;
user- + language-scoped (mirrors the #221 scoping so a shared browser never shows another student's passes);
unioned into the reload seeding (getPendingPassedLessonNumbers) alongside the outbox and server progress.
This closes the flush/verify race so a completed lesson survives an immediate hard refresh.
Notes / trade-offs
The ledger is local and additive (never subtracts), so a server-side admin reset won't be reflected on a device that has the local floor. Acceptable for the reported UX; can be reconciled later if needed.
### Classification
**Type:** Bug
**Priority:** High
**Component:** Playgrounds / Progress sync (WASM "In-Browser Compile" path)
### Summary
Students using **In-Browser Compile** mode report losing progress: they complete a lesson (graded locally, shown green / next lesson unlocks), then take an action that should be harmless — most commonly an **immediate hard refresh** — and the lesson re-locks as if it was never completed.
### Root cause
A locally-graded pass is written to the offline outbox (`pendingProgress` in IndexedDB) and then flushed to `POST /Lessons/progress/batch`. The flush **deletes the outbox row as soon as the server *accepts* the batch — even when the server only accepts it as _unverified_** and hasn't yet surfaced it through `GET /progress` (the server has an async unverified → verified lifecycle).
On reload, `LanguageRunner` seeds completed lessons from `server progress ∪ outbox-pending`. In the window between *"flush accepted"* and *"server reports the pass"*, **both** sources miss it:
- the outbox row was already deleted on accept, and
- the server doesn't yet return it from `GET /progress`.
A hard refresh during that window re-locks the lesson → perceived progress loss.
### Expected behavior
Once a lesson is completed locally, completion must **never regress on a mere refresh** on the same device. The server stays authoritative for cross-device progress, but local completion should be a durable floor.
### Fix
Add a **durable local completion ledger** (a new IndexedDB store, `completedLessons`) that is:
- written at pass time, independent of the network flush lifecycle;
- **never deleted** when the outbox row is flushed;
- user- + language-scoped (mirrors the #221 scoping so a shared browser never shows another student's passes);
- unioned into the reload seeding (`getPendingPassedLessonNumbers`) alongside the outbox and server progress.
This closes the flush/verify race so a completed lesson survives an immediate hard refresh.
### Notes / trade-offs
- The ledger is local and additive (never subtracts), so a server-side admin reset won't be reflected on a device that has the local floor. Acceptable for the reported UX; can be reconciled later if needed.
- Follow-up to #221 (session/progress durability).
Resolved in spikersoft-angular PR #42 (merged to master). Added a durable completedLessons IndexedDB store (DB v2), written at pass time and never removed by the flush lifecycle, unioned into reload seeding — so a locally-graded pass survives an immediate hard refresh. Closing.
(Fix is in the spikersoft-angular repo; cross-repo #223 doesn't auto-close — closing manually.)
Resolved in spikersoft-angular PR #42 (merged to `master`). Added a durable `completedLessons` IndexedDB store (DB v2), written at pass time and never removed by the flush lifecycle, unioned into reload seeding — so a locally-graded pass survives an immediate hard refresh. Closing.
(Fix is in the `spikersoft-angular` repo; cross-repo `#223` doesn't auto-close — closing manually.)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Classification
Type: Bug
Priority: High
Component: Playgrounds / Progress sync (WASM "In-Browser Compile" path)
Summary
Students using In-Browser Compile mode report losing progress: they complete a lesson (graded locally, shown green / next lesson unlocks), then take an action that should be harmless — most commonly an immediate hard refresh — and the lesson re-locks as if it was never completed.
Root cause
A locally-graded pass is written to the offline outbox (
pendingProgressin IndexedDB) and then flushed toPOST /Lessons/progress/batch. The flush deletes the outbox row as soon as the server accepts the batch — even when the server only accepts it as unverified and hasn't yet surfaced it throughGET /progress(the server has an async unverified → verified lifecycle).On reload,
LanguageRunnerseeds completed lessons fromserver progress ∪ outbox-pending. In the window between "flush accepted" and "server reports the pass", both sources miss it:GET /progress.A hard refresh during that window re-locks the lesson → perceived progress loss.
Expected behavior
Once a lesson is completed locally, completion must never regress on a mere refresh on the same device. The server stays authoritative for cross-device progress, but local completion should be a durable floor.
Fix
Add a durable local completion ledger (a new IndexedDB store,
completedLessons) that is:getPendingPassedLessonNumbers) alongside the outbox and server progress.This closes the flush/verify race so a completed lesson survives an immediate hard refresh.
Notes / trade-offs
Resolved in spikersoft-angular PR #42 (merged to
master). Added a durablecompletedLessonsIndexedDB store (DB v2), written at pass time and never removed by the flush lifecycle, unioned into reload seeding — so a locally-graded pass survives an immediate hard refresh. Closing.(Fix is in the
spikersoft-angularrepo; cross-repo#223doesn't auto-close — closing manually.)