Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
9thLevelSoftware marked this conversation as resolved.

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()
Expand Down Expand Up @@ -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,
Expand All @@ -8966,6 +8983,7 @@ class ActiveSessionEngine(
biomechanicsRepResults = biomechanicsSummary?.repResults.orEmpty()
.map { it.deepCopyForExitSnapshot() },
singleExerciseDefaults = captureSingleExerciseDefaultsFromWorkout(),
justLiftDefaults = capturedJustLiftDefaults,
presentationSummary = presentationSummary,
exerciseIndex = exerciseIndex,
setIndex = setIndex,
Expand Down Expand Up @@ -9015,6 +9033,20 @@ class ActiveSessionEngine(
}
}

// Issue #714: writes the Just Lift defaults captured in the immutable exit
Comment thread
9thLevelSoftware marked this conversation as resolved.
// 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)
Expand Down Expand Up @@ -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)
Comment thread
9thLevelSoftware marked this conversation as resolved.

val postSave = snapshot.postSaveInput
val hasPR = gamificationManager.processPostSaveEvents(
Expand Down Expand Up @@ -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
Comment thread
9thLevelSoftware marked this conversation as resolved.
// 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.devil.phoenixproject.domain.model.BiomechanicsRepResult
import com.devil.phoenixproject.domain.model.BiomechanicsSetSummary
import com.devil.phoenixproject.domain.model.CompletedSet
import com.devil.phoenixproject.domain.model.ForceCurveResult
import com.devil.phoenixproject.domain.model.JustLiftDefaultsDocument
import com.devil.phoenixproject.domain.model.ProgramMode
import com.devil.phoenixproject.domain.model.RepMetricData
import com.devil.phoenixproject.domain.model.RoutineExecutionIdentity
Expand Down Expand Up @@ -357,6 +358,17 @@ internal data class WorkoutExitSnapshot(
val repMetrics: List<RepMetricData>,
val biomechanicsRepResults: List<BiomechanicsRepResult>,
val singleExerciseDefaults: SingleExerciseDefaultsDocument? = null,
/**
* Just Lift defaults captured from the pre-teardown Just Lift WorkoutParameters.
* Populated only when [completion] is a Just Lift completion; persisted in the same
* `settingsManager.mutateWorkout(snapshot.lease.profileId)` block as
* [singleExerciseDefaults] so the automatic-completion path cannot fall behind the
* legacy manual `saveJustLiftDefaultsFromWorkout()` path.
*
* See issue #714 (Just Lift mode resets to TUT at end of every set instead of
* preserving the user's selected mode).
*/
val justLiftDefaults: JustLiftDefaultsDocument? = null,
val presentationSummary: WorkoutState.SetSummary,
val exerciseIndex: Int,
val setIndex: Int,
Expand Down
Loading
Loading