Skip to content

"Continue Today's Routine" dead-ends when the next recommendation deep-links to the page you're already on #3428

Description

@atomantic

Problem / Goal

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:

const result = await getPostRecommendations(1).catch(() => null);
const recommendation = result?.recommendations?.[0];
if (!recommendation) { navigate('/post/launcher'); return; }
if (recommendation.deepLink && recommendation.deepLink !== '/post/launcher') {
  navigate(recommendation.deepLink);   // <-- no-op when deepLink === current URL
  return;
}

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:

if (itemId === ELEMENTS_SONG_ID) return '/post/memory/elements/element-flash';

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%:

if (quality < 3) intervalDays = 0; // relearn — resurface immediately

nextReview then lands at "now", and isMemoryItemDue (:287-296) returns true for t <= now. So the exact repro is:

  1. Go to /post/memory/elements/element-flash.
  2. Finish the quiz scoring under 50% (skipping every question works).
  3. Click Continue Today's Routine.
  4. 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.

  1. 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.

  2. Resolve the target against the current location. Compare each recommendation's deepLink pathname to location.pathname:

    • Different pathnavigate(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.

    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.

  3. 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).
  4. 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 PostSessionResultshandleSaved 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.

Metadata

Metadata

Assignees

Labels

area:postMeatSpace POST training platform (drills, morse, memory, wordplay)bugSomething isn't workingseverity:medium

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions