Symptom
Started a workout in the app. After the app got killed/restarted mid-workout, there was no way to stop/finish it from the UI — the only reachable action on it was delete.
Also reported independently in #102's feedback dump: "deleting the manual workout does not leave the app UI in the same state as if I didn't do it. It shows run live (but I already finished and deleted it, so something is bad here)" — same root, different angle (a stale live-looking state surviving past its actual lifetime).
Root cause
AppState.activeWorkout (the in-memory live-session state that drives the mini-player banner and the live session screen's "hold to finish" control) is purely in-memory and was never rehydrated from the persisted sessions row on cold start.
startWorkout() writes a sessions row with status: 'live' immediately (app_state.dart, putSession), and only stopWorkout() (or the live-activity/GPS teardown paths) ever flips it to status: 'done'. If the app is killed before stopWorkout() runs, the row is permanently stuck at status: 'live' and activeWorkout is lost — there was no code anywhere that restored it on the next launch.
Downstream:
- The Workouts list explicitly skips the swipe-to-delete
Dismissible wrapper for status == 'live' rows (workouts_screen.dart).
WorkoutDetailScreen's only action, for any status, was an unconditional trash button — no "finish"/"end" affordance existed there at all.
So once the in-memory state was gone, delete really was the only reachable action — the workout (and whatever was legitimately recorded) could only be discarded, never properly closed out.
Fix
LocalDb.liveSessions() (new) — queries any session row still status='live'.
AppState._reconcileOrphanedLiveWorkout() (new, called once from _init()):
- Recent (≤ 6h old): rehydrates
activeWorkout from the row so the normal mini-player banner / live session screen / "hold to finish" flow becomes reachable again. Live per-second tallies (calories/strain/zone-minutes) can't be reconstructed from a single DB row, so they resume accruing from zero going forward rather than being fabricated.
- Stale (> 6h, or a malformed row): silently finalized to
status: 'done' instead of resurfacing a days-old "still live" banner — duration_min/calories are left unset rather than guessed.
Fixed in the same pass as #129/#130/#123.
Symptom
Started a workout in the app. After the app got killed/restarted mid-workout, there was no way to stop/finish it from the UI — the only reachable action on it was delete.
Also reported independently in #102's feedback dump: "deleting the manual workout does not leave the app UI in the same state as if I didn't do it. It shows run live (but I already finished and deleted it, so something is bad here)" — same root, different angle (a stale live-looking state surviving past its actual lifetime).
Root cause
AppState.activeWorkout(the in-memory live-session state that drives the mini-player banner and the live session screen's "hold to finish" control) is purely in-memory and was never rehydrated from the persistedsessionsrow on cold start.startWorkout()writes asessionsrow withstatus: 'live'immediately (app_state.dart,putSession), and onlystopWorkout()(or the live-activity/GPS teardown paths) ever flips it tostatus: 'done'. If the app is killed beforestopWorkout()runs, the row is permanently stuck atstatus: 'live'andactiveWorkoutis lost — there was no code anywhere that restored it on the next launch.Downstream:
Dismissiblewrapper forstatus == 'live'rows (workouts_screen.dart).WorkoutDetailScreen's only action, for any status, was an unconditional trash button — no "finish"/"end" affordance existed there at all.So once the in-memory state was gone, delete really was the only reachable action — the workout (and whatever was legitimately recorded) could only be discarded, never properly closed out.
Fix
LocalDb.liveSessions()(new) — queries any session row stillstatus='live'.AppState._reconcileOrphanedLiveWorkout()(new, called once from_init()):activeWorkoutfrom the row so the normal mini-player banner / live session screen / "hold to finish" flow becomes reachable again. Live per-second tallies (calories/strain/zone-minutes) can't be reconstructed from a single DB row, so they resume accruing from zero going forward rather than being fabricated.status: 'done'instead of resurfacing a days-old "still live" banner —duration_min/caloriesare left unset rather than guessed.Fixed in the same pass as #129/#130/#123.