Description
DSA-sheet progress never persists to the backend: saveProgress rejects the correct completedTopics object/map shape with a 400, because a hand-rolled check requires an array — contradicting the Zod validator, the Mongoose schema, the import util, and the frontend.
backend/controllers/userSheetProgressController.js (~L63-68):
if (completedTopics !== undefined && !Array.isArray(completedTopics)) {
return res.status(400).json({ success:false, error:"Invalid completedTopics field, must be an array" });
}
The frontend POSTs an object, e.g. { sheetId, followed:true, completedTopics:{ "0-0-0":true }, percentage:12 } (frontend/src/components/SheetDetailsPage.jsx:198, ProgressTrackerDashboard.jsx:153). The Zod validator permits it (completedTopics: z.record(z.string(), z.boolean())), and the schema is completedTopics: { type: Object }, resetProgress sets {}, and the import util returns an object. Only this controller check disagrees, and !Array.isArray({...}) is true → every save returns 400. The frontend .catch swallows it, so progress lives only in localStorage and is silently lost across devices / on cache clear.
(Distinct from the existing race-condition retry issue — this is the payload shape being rejected outright.)
Fix
Accept a plain object:
if (completedTopics !== undefined &&
(typeof completedTopics !== "object" || completedTopics === null || Array.isArray(completedTopics))) {
return res.status(400).json({ success:false, error:"Invalid completedTopics field, must be an object" });
}
(or delete the redundant manual checks and rely on the wired validateSaveProgress).
Happy to fix if assigned.
Contributing as part of Elite Coders Summer of Code (ECSoC 2026).
Description
DSA-sheet progress never persists to the backend:
saveProgressrejects the correctcompletedTopicsobject/map shape with a 400, because a hand-rolled check requires an array — contradicting the Zod validator, the Mongoose schema, the import util, and the frontend.backend/controllers/userSheetProgressController.js(~L63-68):The frontend POSTs an object, e.g.
{ sheetId, followed:true, completedTopics:{ "0-0-0":true }, percentage:12 }(frontend/src/components/SheetDetailsPage.jsx:198,ProgressTrackerDashboard.jsx:153). The Zod validator permits it (completedTopics: z.record(z.string(), z.boolean())), and the schema iscompletedTopics: { type: Object },resetProgresssets{}, and the import util returns an object. Only this controller check disagrees, and!Array.isArray({...})istrue→ every save returns 400. The frontend.catchswallows it, so progress lives only in localStorage and is silently lost across devices / on cache clear.(Distinct from the existing race-condition retry issue — this is the payload shape being rejected outright.)
Fix
Accept a plain object:
(or delete the redundant manual checks and rely on the wired
validateSaveProgress).Happy to fix if assigned.
Contributing as part of Elite Coders Summer of Code (ECSoC 2026).