-
-
Notifications
You must be signed in to change notification settings - Fork 26
fix: persist Just Lift mode through automatic completion snapshot (#714) #716
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e4b79e7
52a8078
7095ee3
54a5868
33f859c
ca0b9b3
73f28bf
6b74c6a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6678,29 +6678,41 @@ class ActiveSessionEngine( | |||||||||
|
|
||||||||||
| private suspend fun saveJustLiftDefaultsFromWorkout() { | ||||||||||
| val params = coordinator._workoutParameters.value | ||||||||||
| if (!params.isJustLift) return | ||||||||||
|
|
||||||||||
| val eccentricLoadPct = if (params.isEchoMode) params.eccentricLoad.percentage else 100 | ||||||||||
| val echoLevelVal = if (params.isEchoMode) params.echoLevel.levelValue else 0 | ||||||||||
|
|
||||||||||
| val defaults = toJustLiftDefaultsDocumentOrNull(params) ?: return | ||||||||||
| try { | ||||||||||
| val defaults = JustLiftDefaultsDocument( | ||||||||||
| workoutModeId = params.programMode.modeValue, | ||||||||||
| weightPerCableKg = params.weightPerCableKg.coerceAtLeast(0.1f), | ||||||||||
| weightChangePerRep = params.progressionRegressionKg, | ||||||||||
| eccentricLoadPercentage = eccentricLoadPct, | ||||||||||
| echoLevelValue = echoLevelVal, | ||||||||||
| stallDetectionEnabled = params.stallDetectionEnabled, | ||||||||||
| repCountTimingName = params.repCountTiming.name, | ||||||||||
| restSeconds = params.justLiftRestSeconds, | ||||||||||
| ) | ||||||||||
| settingsManager.saveJustLiftDefaultsDocument(defaults) | ||||||||||
| Logger.d { "Saved Just Lift defaults: mode=${params.programMode.modeValue}, weight=${params.weightPerCableKg}kg, restSeconds=${params.justLiftRestSeconds}" } | ||||||||||
| } catch (e: Exception) { | ||||||||||
| Logger.e(e) { "Failed to save Just Lift defaults: ${e.message}" } | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * Shared Just Lift defaults conversion used by both the legacy manual | ||||||||||
| * `saveJustLiftDefaultsFromWorkout()` path and the automatic-completion | ||||||||||
| * snapshot persistence path. Centralising the conversion guarantees the | ||||||||||
| * two paths cannot drift. | ||||||||||
| * | ||||||||||
| * Returns null when [params] is not a Just Lift workout. See issue #714. | ||||||||||
| */ | ||||||||||
| internal fun toJustLiftDefaultsDocumentOrNull(params: com.devil.phoenixproject.domain.model.WorkoutParameters): JustLiftDefaultsDocument? { | ||||||||||
| if (!params.isJustLift) return null | ||||||||||
|
|
||||||||||
| val eccentricLoadPct = if (params.isEchoMode) params.eccentricLoad.percentage else 100 | ||||||||||
| val echoLevelVal = if (params.isEchoMode) params.echoLevel.levelValue else 0 | ||||||||||
|
|
||||||||||
| return JustLiftDefaultsDocument( | ||||||||||
| workoutModeId = params.programMode.modeValue, | ||||||||||
| weightPerCableKg = params.weightPerCableKg.coerceAtLeast(0.1f), | ||||||||||
| weightChangePerRep = params.progressionRegressionKg, | ||||||||||
| eccentricLoadPercentage = eccentricLoadPct, | ||||||||||
| echoLevelValue = echoLevelVal, | ||||||||||
| stallDetectionEnabled = params.stallDetectionEnabled, | ||||||||||
| repCountTimingName = params.repCountTiming.name, | ||||||||||
| restSeconds = params.justLiftRestSeconds, | ||||||||||
| ) | ||||||||||
| } | ||||||||||
|
|
||||||||||
| suspend fun getSingleExerciseDefaults( | ||||||||||
| exerciseId: String, | ||||||||||
| ): com.devil.phoenixproject.data.preferences.SingleExerciseDefaults? = settingsManager.getSingleExerciseDefaultsDocument(exerciseId)?.toLegacySingleExerciseDefaults() | ||||||||||
|
|
@@ -8955,6 +8967,11 @@ class ActiveSessionEngine( | |||||||||
| cycleId = context.cycleId, | ||||||||||
| dayNumber = context.cycleDayNumber, | ||||||||||
| ) | ||||||||||
| // Issue #714: capture Just Lift defaults from the pre-teardown params | ||||||||||
| // so automatic completion persists the user's confirmed Just Lift mode. | ||||||||||
| // Read here (before reset) and freeze into the immutable snapshot so the | ||||||||||
| // persistSnapshot write cannot read mutable live state after teardown. | ||||||||||
| val capturedJustLiftDefaults = toJustLiftDefaultsDocumentOrNull(params) | ||||||||||
| return WorkoutExitSnapshot( | ||||||||||
| lease = lease, | ||||||||||
| completion = completion, | ||||||||||
|
|
@@ -8966,6 +8983,7 @@ class ActiveSessionEngine( | |||||||||
| biomechanicsRepResults = biomechanicsSummary?.repResults.orEmpty() | ||||||||||
| .map { it.deepCopyForExitSnapshot() }, | ||||||||||
| singleExerciseDefaults = captureSingleExerciseDefaultsFromWorkout(), | ||||||||||
| justLiftDefaults = capturedJustLiftDefaults, | ||||||||||
| presentationSummary = presentationSummary, | ||||||||||
| exerciseIndex = exerciseIndex, | ||||||||||
| setIndex = setIndex, | ||||||||||
|
|
@@ -9015,6 +9033,20 @@ class ActiveSessionEngine( | |||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // Issue #714: writes the Just Lift defaults captured in the immutable exit | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔥 The Roast: You wrote a 9-line essay on a 6-line helper. The function name 🩹 The Fix: Collapse the comment to the one load-bearing sentence. Something like:
Suggested change
📏 Severity: nitpick Reply with |
||||||||||
| // snapshot. Split out from `persistSnapshot` so the Just Lift completion | ||||||||||
| // job can call it synchronously before publishing `SetSummary` / flipping | ||||||||||
| // to `Idle`, and the async path can still call it for retained-snapshot | ||||||||||
| // retry and process recovability. The async re-write is idempotent | ||||||||||
| // (same captured value). | ||||||||||
| internal suspend fun persistCapturedJustLiftDefaultsSnapshot(snapshot: WorkoutExitSnapshot) { | ||||||||||
| snapshot.justLiftDefaults?.let { justLiftDefaults -> | ||||||||||
| settingsManager.mutateWorkout(snapshot.lease.profileId) { workoutPreferences -> | ||||||||||
| workoutPreferences.copy(justLiftDefaults = justLiftDefaults) | ||||||||||
| } | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| private fun retryRetainedWorkoutExitPersistence() { | ||||||||||
| scope.launch { | ||||||||||
| exitSnapshotStore.retainedSnapshots().forEach(::launchSnapshotPersistence) | ||||||||||
|
|
@@ -9072,6 +9104,12 @@ class ActiveSessionEngine( | |||||||||
| ) | ||||||||||
| } | ||||||||||
| } | ||||||||||
| // Issue #714: persist Just Lift defaults captured in the immutable | ||||||||||
| // exit snapshot so the user's confirmed Just Lift mode survives the | ||||||||||
| // return-to-setup reload. Uses the same profile-scoped mutateWorkout | ||||||||||
| // and the snapshot's lease profile id, so it cannot read mutable | ||||||||||
| // coordinator state after teardown. | ||||||||||
| persistCapturedJustLiftDefaultsSnapshot(snapshot) | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When two Just Lift sets complete before the first Useful? React with 👍 / 👎. |
||||||||||
|
|
||||||||||
| val postSave = snapshot.postSaveInput | ||||||||||
| val hasPR = gamificationManager.processPostSaveEvents( | ||||||||||
|
|
@@ -11223,6 +11261,38 @@ class ActiveSessionEngine( | |||||||||
|
|
||||||||||
| Logger.d("handleSetCompletion: summaryCountdownSeconds=$summaryCountdownSeconds, skipSummary=$skipSummary, wasBodyweight=$wasBodyweight, effectiveSkipSummary=$effectiveSkipSummary, isJustLift=$isJustLift, isAMRAP=${params.isAMRAP}") | ||||||||||
|
|
||||||||||
| // Issue #714 (Codex P1 follow-up): write synchronously so | ||||||||||
| // JustLiftScreen's reload sees fresh defaults before any navigation | ||||||||||
| // fires; try/catch so a transient prefs failure doesn't strand the | ||||||||||
| // user mid-teardown. | ||||||||||
| // reads persisted defaults on `LaunchedEffect(readyProfileId)` — once per | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔥 The Roast: You applied the suggestion from the previous review by adding the 4-line summary on top of the existing 16-line block instead of replacing the block. The result is 19 lines of comment saying roughly the same thing twice — same over-explained-comment pattern, but now wearing the same costume in two consecutive scenes. The 4-line summary at L11264-11267 is the entire story; this 15-line block is now a redundant director's commentary playing on mute. 🩹 The Fix: Delete the entire old verbose block (lines 11268-11282). The 4-line summary above already carries the sync-before-flip rationale and the try/catch resilience note; the helper's KDoc at � Severity: nitpick Reply with |
||||||||||
| // profile-id change — and the async `persistSnapshot` coroutine does not | ||||||||||
| // finish writing the captured Just Lift defaults until AFTER this | ||||||||||
| // completion job has published `SetSummary` or flipped `WorkoutState` to | ||||||||||
| // `Idle`. The ActiveWorkoutScreen observer then pops back to | ||||||||||
| // JustLiftScreen and JustLiftScreen recomposes against the stale TUT value. | ||||||||||
| // Write the captured defaults synchronously here, BEFORE any state flip or | ||||||||||
| // summary publish that could trigger the navigation observer, so the | ||||||||||
| // JustLiftScreen reload sees the persisted Old School (or whatever the user | ||||||||||
| // picked). The write is wrapped in try/catch so a transient | ||||||||||
| // preferences-store failure cannot leave the user stuck after machine | ||||||||||
| // teardown — the retained-snapshot retry path is the durable backstop. | ||||||||||
| // The async `persistSnapshot` path still calls the same helper for retained | ||||||||||
| // snapshot recovery and process recovability (idempotent re-write of the | ||||||||||
| // same value). | ||||||||||
| if (isJustLift && terminalSnapshot?.justLiftDefaults != null) { | ||||||||||
| try { | ||||||||||
| persistCapturedJustLiftDefaultsSnapshot(terminalSnapshot) | ||||||||||
| } catch (e: CancellationException) { | ||||||||||
| throw e | ||||||||||
| } catch (e: Throwable) { | ||||||||||
| Logger.w(e) { | ||||||||||
| "Issue #714: synchronous Just Lift defaults write failed; " + | ||||||||||
| "retained-snapshot retry will recover" | ||||||||||
| } | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if (!effectiveSkipSummary && !preservePlanOwnedResting) { | ||||||||||
| Logger.d("handleSetCompletion: Setting state to SetSummary (effectiveSkipSummary=false)") | ||||||||||
| val summaryPublished = executionGuard.commitIfCurrent(lease) { | ||||||||||
|
|
||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔥 The Roast: You imported
WorkoutParametersat line 78 like a responsible engineer, then immediately wrotecom.devil.phoenixproject.domain.model.WorkoutParameterson line 6699 like you didn't notice. The import is sitting right there, patient, waiting to be used. It's giving "I copied this from a different file and never looked back."🩹 The Fix:
📏 Severity: nitpick
Reply with
@kilocode-bot fix itto have Kilo Code address this issue.