You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Finishing the Element Flash quiz at /post/memory/elements/element-flash and clicking Continue Today's Routine does nothing — the completion screen stays exactly as it was, with no navigation, no toast, and no error. The button is the primary CTA on every POST completion screen, so a silent no-op reads as a broken app.
The goal: clicking Continue always produces a visible next step — either a different practice surface, a fresh run of the same drill when that genuinely is the top recommendation, or the launcher.
Context
PostCompletionActions (client/src/components/meatspace/post/PostCompletionActions.jsx:16-23) runs the handler inside Promise.resolve().then(action).catch(() => {}), so any failure or no-op is invisible by design — the button just un-disables itself.
The handler is continueDailyRoutine in client/src/components/meatspace/tabs/PostTab.jsx:88-100:
constresult=awaitgetPostRecommendations(1).catch(()=>null);constrecommendation=result?.recommendations?.[0];if(!recommendation){navigate('/post/launcher');return;}if(recommendation.deepLink&&recommendation.deepLink!=='/post/launcher'){navigate(recommendation.deepLink);// <-- no-op when deepLink === current URLreturn;}
It requests exactly one recommendation and navigates to its deepLink with no check that the link points somewhere other than the current page.
For the Elements Song, that deep link is this page. memoryPracticeDeepLink (server/services/meatspacePost.js:1038-1042) maps the elements item to the flash quiz:
and composePostRecommendations (server/services/meatspacePost.js:1070-1078) emits memory-due entries first, ahead of every other kind.
The item is still due at that moment because the quiz just made it due. submitPractice (server/services/meatspacePostMemory.js:684-691) advances the schedule from the run's accuracy, and advanceSchedule (:142-164) sets intervalDays = 0 when quality < 3 — i.e. accuracy below ~50%:
nextReview then lands at "now", and isMemoryItemDue (:287-296) returns true for t <= now. So the exact repro is:
Go to /post/memory/elements/element-flash.
Finish the quiz scoring under 50% (skipping every question works).
Click Continue Today's Routine.
Progress saves (the POST request fires), then navigate('/post/memory/elements/element-flash') runs against the URL already in the address bar.
React Router re-renders the same route, PostTab returns the same <ElementsSong> element — and unlike MemoryPractice at PostTab.jsx:312-322, ElementsSong is rendered with no key (:296-305), so nothing remounts. ElementFlashMode keeps its internal idx >= questions.length state and the user is left staring at the same "Element Flash Complete" panel.
Same exposure elsewhere.continueDailyRoutine is shared by four in-page surfaces — WordplayTrainer (:251), MorseTrainer (:268), ElementsSong (:302), and MemoryPractice (:321) — and other recommendation kinds emit deep links that can equally match the page in view (/post/morse/copy at server/services/meatspacePost.js:1006, /post/memory/<id>/spaced via memoryPracticeDeepLink). MemoryPractice's key={${subtab}:${practiceMode || 'picker'}} doesn't help either — the key is unchanged by a same-URL navigation.
Existing coverage misses this.client/src/components/meatspace/tabs/PostTab.test.jsx:211-234 ("continues from a saved Elements lesson to the next cross-domain recommendation") only asserts the case where the recommendation deep-links away (/post/morse/copy). There is no test for a self-referential deep link.
Proposed approach
Fix it once in continueDailyRoutine, plus a remount key on the surfaces it drives.
Ask for the full list, not one. In PostTab.jsx, call getPostRecommendations(5) (the server's RECOMMENDATION_LIMIT, server/services/meatspacePost.js:912) instead of getPostRecommendations(1). No server change needed — the route already honors ?limit.
Resolve the target against the current location. Compare each recommendation's deepLink pathname to location.pathname:
Different path → navigate(deepLink), exactly as today.
Same path (the top recommendation is "practice this again", which is what a sub-50% run legitimately produces) → restart the drill in place rather than no-op: bump a run nonce in the query string via URLSearchParams (preserving any existing params — MorseTrainer already threads ?ref= through location.search at PostTab.jsx:265-266) and navigate to ${pathname}?${params}.
No recommendations at all → /post/launcher, unchanged.
Make the restart actually remount. Read the run param in PostTab and fold it into each surface's key:
Add key={${elementsMode || 'picker'}:${runNonce}} to <ElementsSong> (PostTab.jsx:296) — it currently has none.
Append :${runNonce} to MemoryPractice's existing key (:314).
Add the same key to <MorseTrainer> (:262) and <WordplayTrainer> (:245).
Tests. Add to PostTab.test.jsx alongside the existing cross-domain case: mock getPostRecommendations to return a rec whose deepLink equals the rendered route, complete the drill, click Continue, and assert the drill restarts (question 1 / N visible, completion panel gone) rather than the screen freezing. Cover at least the Elements Song route; a second case for /post/morse/copy guards the shared path.
Acceptance criteria
Completing Element Flash with a sub-50% score and clicking Continue Today's Routine starts a fresh Element Flash run (progress back at 1 / N) instead of leaving the completion screen untouched.
When the top recommendation deep-links to a different surface, behavior is unchanged — it navigates there (existing PostTab.test.jsx:211-234 still passes).
When there are no recommendations, Continue still lands on /post/launcher.
The restart preserves unrelated query params already in the URL (e.g. Morse's ?ref=).
The same fix covers MorseTrainer, WordplayTrainer, and MemoryPractice, which share continueDailyRoutine.
New PostTab.test.jsx cases cover the self-referential deep link for at least the Elements and Morse routes; cd client && npm test passes.
Out of scope
Changing the server-side recommendation ranking (e.g. suppressing a just-practiced item from composePostRecommendations) — the ranking is correct; the client's handling of it is not.
The PostSessionResults → handleSaved path (PostTab.jsx:102-108). It calls session.reset() and no deep link targets /post/session/run, so it can't self-navigate.
Reworking PostCompletionActions' swallow-all .catch(() => {}) error handling.
Any change to advanceSchedule / spaced-repetition intervals.
Problem / Goal
Finishing the Element Flash quiz at
/post/memory/elements/element-flashand clicking Continue Today's Routine does nothing — the completion screen stays exactly as it was, with no navigation, no toast, and no error. The button is the primary CTA on every POST completion screen, so a silent no-op reads as a broken app.The goal: clicking Continue always produces a visible next step — either a different practice surface, a fresh run of the same drill when that genuinely is the top recommendation, or the launcher.
Context
PostCompletionActions(client/src/components/meatspace/post/PostCompletionActions.jsx:16-23) runs the handler insidePromise.resolve().then(action).catch(() => {}), so any failure or no-op is invisible by design — the button just un-disables itself.The handler is
continueDailyRoutineinclient/src/components/meatspace/tabs/PostTab.jsx:88-100:It requests exactly one recommendation and navigates to its
deepLinkwith no check that the link points somewhere other than the current page.For the Elements Song, that deep link is this page.
memoryPracticeDeepLink(server/services/meatspacePost.js:1038-1042) maps the elements item to the flash quiz:and
composePostRecommendations(server/services/meatspacePost.js:1070-1078) emitsmemory-dueentries first, ahead of every other kind.The item is still due at that moment because the quiz just made it due.
submitPractice(server/services/meatspacePostMemory.js:684-691) advances the schedule from the run's accuracy, andadvanceSchedule(:142-164) setsintervalDays = 0whenquality < 3— i.e. accuracy below ~50%:nextReviewthen lands at "now", andisMemoryItemDue(:287-296) returns true fort <= now. So the exact repro is:/post/memory/elements/element-flash.navigate('/post/memory/elements/element-flash')runs against the URL already in the address bar.React Router re-renders the same route,
PostTabreturns the same<ElementsSong>element — and unlikeMemoryPracticeatPostTab.jsx:312-322,ElementsSongis rendered with nokey(:296-305), so nothing remounts.ElementFlashModekeeps its internalidx >= questions.lengthstate and the user is left staring at the same "Element Flash Complete" panel.Same exposure elsewhere.
continueDailyRoutineis shared by four in-page surfaces —WordplayTrainer(:251),MorseTrainer(:268),ElementsSong(:302), andMemoryPractice(:321) — and other recommendation kinds emit deep links that can equally match the page in view (/post/morse/copyatserver/services/meatspacePost.js:1006,/post/memory/<id>/spacedviamemoryPracticeDeepLink).MemoryPractice'skey={${subtab}:${practiceMode || 'picker'}}doesn't help either — the key is unchanged by a same-URL navigation.Existing coverage misses this.
client/src/components/meatspace/tabs/PostTab.test.jsx:211-234("continues from a saved Elements lesson to the next cross-domain recommendation") only asserts the case where the recommendation deep-links away (/post/morse/copy). There is no test for a self-referential deep link.Proposed approach
Fix it once in
continueDailyRoutine, plus a remount key on the surfaces it drives.Ask for the full list, not one. In
PostTab.jsx, callgetPostRecommendations(5)(the server'sRECOMMENDATION_LIMIT,server/services/meatspacePost.js:912) instead ofgetPostRecommendations(1). No server change needed — the route already honors?limit.Resolve the target against the current location. Compare each recommendation's
deepLinkpathname tolocation.pathname:navigate(deepLink), exactly as today.runnonce in the query string viaURLSearchParams(preserving any existing params —MorseTraineralready threads?ref=throughlocation.searchatPostTab.jsx:265-266) andnavigateto${pathname}?${params}./post/launcher, unchanged.Prefer restart-in-place over "skip to the next differing recommendation": the server's Release v0.2.0 - CI/CD, Documentation & DevTools #1 recommendation is the item the user just missed, and silently demoting it to feat: Enhanced DevTools, Apps page, and detection improvements #2 contradicts the ranking.
Make the restart actually remount. Read the
runparam inPostTaband fold it into each surface's key:key={${elementsMode || 'picker'}:${runNonce}}to<ElementsSong>(PostTab.jsx:296) — it currently has none.:${runNonce}toMemoryPractice's existing key (:314).<MorseTrainer>(:262) and<WordplayTrainer>(:245).Tests. Add to
PostTab.test.jsxalongside the existing cross-domain case: mockgetPostRecommendationsto return a rec whosedeepLinkequals the rendered route, complete the drill, click Continue, and assert the drill restarts (question1 / Nvisible, completion panel gone) rather than the screen freezing. Cover at least the Elements Song route; a second case for/post/morse/copyguards the shared path.Acceptance criteria
1 / N) instead of leaving the completion screen untouched.PostTab.test.jsx:211-234still passes)./post/launcher.?ref=).MorseTrainer,WordplayTrainer, andMemoryPractice, which sharecontinueDailyRoutine.PostTab.test.jsxcases cover the self-referential deep link for at least the Elements and Morse routes;cd client && npm testpasses.Out of scope
composePostRecommendations) — the ranking is correct; the client's handling of it is not.PostSessionResults→handleSavedpath (PostTab.jsx:102-108). It callssession.reset()and no deep link targets/post/session/run, so it can't self-navigate.PostCompletionActions' swallow-all.catch(() => {})error handling.advanceSchedule/ spaced-repetition intervals.