From 9ddad828436133fd43b97c2d71a64bfcf6bbdd96 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 19 Mar 2026 23:40:45 +0000 Subject: [PATCH] fix: clear stale timer state on new workout start When navigating away from a workout while a rest timer was running (e.g. pressing Back), the timer state was left in IndexedDB. On the next workout load, the recovery code would find the stale timer and fire the notification and/or show "Time's Up!" immediately. Two fixes: 1. renderWorkout now clears any stale timer state when starting fresh (not resuming an active workout), preventing the recovery code from misinterpreting a leftover timer from a previous session. 2. The Back button handler now calls putTimerState(null) so the timer is explicitly cleaned up when the user navigates away mid-rest. https://claude.ai/code/session_01W41HrMh4pZma1AiT7fHRxd --- workout-tracker/e2e/workout-reload.spec.ts | 52 ++++++++++++++++++++++ workout-tracker/src/ui/workout.ts | 13 +++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/workout-tracker/e2e/workout-reload.spec.ts b/workout-tracker/e2e/workout-reload.spec.ts index 1b15ff9..443c143 100644 --- a/workout-tracker/e2e/workout-reload.spec.ts +++ b/workout-tracker/e2e/workout-reload.spec.ts @@ -117,6 +117,58 @@ test.describe('Workout Reload Persistence', () => { }); }); +test.describe('Back Button Timer Cleanup', () => { + /** + * Regression: pressing Back during a rest timer left the timer state in + * IndexedDB. On the next workout load, the recovery code would fire the + * notification and/or show "Time's Up!" immediately. + */ + test('stale timer from previous session does not fire on new workout start', async ({ page }) => { + await page.addInitScript(() => { + (window as any).__vibrateCount = 0; + Object.defineProperty(navigator, 'vibrate', { + value: () => { (window as any).__vibrateCount++; return true; }, + writable: true, + configurable: true, + }); + }); + + await page.goto('/'); + await page.waitForSelector('#app'); + await page.click('#start-workout-btn'); + await page.waitForSelector('.workout-screen'); + + // Seed a stale timer (still has ~2 seconds remaining) — simulating what + // the Back button leaves behind because it doesn't clear IndexedDB timer state. + await page.evaluate(async () => { + const { putTimerState } = await import('/src/db/database.ts'); + await putTimerState({ + expectedEndTime: Date.now() + 2000, // expires in 2 seconds + durationMs: 90000, + }); + }); + + // Navigate to home (simulating Back button navigation) + await page.evaluate(() => { window.location.hash = 'home'; }); + await page.waitForSelector('#start-workout-btn'); + await page.evaluate(() => { (window as any).__vibrateCount = 0; }); + + // Start a new workout — stale timer should be cleared, not re-used + await page.click('#start-workout-btn'); + await page.waitForSelector('.workout-screen'); + + // Wait long enough for the stale timer to expire (>2s) + await page.waitForTimeout(2500); + + // No notification should have fired from the stale timer + const vibrateCount = await page.evaluate(() => (window as any).__vibrateCount); + expect(vibrateCount).toBe(0); + + // No "Time's Up!" UI should have appeared + await expect(page.locator('[data-testid="timer-expired"]')).not.toBeAttached(); + }); +}); + test.describe('Cancel/Abandon Workout', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); diff --git a/workout-tracker/src/ui/workout.ts b/workout-tracker/src/ui/workout.ts index 53ec5ba..74a0dd1 100644 --- a/workout-tracker/src/ui/workout.ts +++ b/workout-tracker/src/ui/workout.ts @@ -61,6 +61,7 @@ export async function renderWorkout(container: HTMLElement): Promise { // Restore in-progress workout if one exists for this same day const activeWorkout = await getActiveWorkout(); + let resumingActiveWorkout = false; if ( activeWorkout && activeWorkout.templateId === state.templateId && @@ -71,6 +72,15 @@ export async function renderWorkout(container: HTMLElement): Promise { completedSets.push(...activeWorkout.completedSets); currentSetIndex = activeWorkout.currentSetIndex; workoutStartTime = activeWorkout.startedAt; + resumingActiveWorkout = true; + } + + // If starting fresh (not resuming), clear any stale timer state that may + // have been left over from a previous session (e.g. user pressed Back + // while a rest timer was running). + if (!resumingActiveWorkout) { + await putTimerState(null); + cancelBackgroundTimerNotification(); } container.innerHTML = ''; @@ -498,10 +508,11 @@ export async function renderWorkout(container: HTMLElement): Promise { } // Event listeners - document.getElementById('back-btn')?.addEventListener('click', () => { + document.getElementById('back-btn')?.addEventListener('click', async () => { releaseWakeLock(); if (timerInterval) clearInterval(timerInterval); cancelBackgroundTimerNotification(); + await putTimerState(null); navigate('home'); });