Record per-set actuals and drive the chart and volume from them

Every completed set now writes a SetEntry (reps/weight or seconds),
pre-filled from the plan by transition(to:) so the list checkbox, both
run flows, and One More all capture for free; reset clears, skip keeps
partials. The rest and finish pages show the just-done set as a pill
that opens a stepper sheet for correcting reps and weight (2.5 lb /
1.25 kg steps). The Weight Progression chart plots the top-set actual
weight and workout volume sums recorded sets, both falling back to the
plan for legacy logs via effectiveSetEntries.

Storage side of UX #3 rides along: plan weights are Double now.
Schema bumps: SplitDocument 2→3, WorkoutDocument 3→4 (a fractional
weight fails an older Int decode, and a rewrite would strip the
irreplaceable actuals), SwiftData cache 4→5. A per-log updatedAt is
reserved for the future cross-device log merge.

Claude-Session: https://claude.ai/code/session_01HJDQQDA9QdP8zByg43H5v3
This commit is contained in:
2026-07-08 12:48:37 -04:00
parent c05e83cff7
commit 394ec0989e
24 changed files with 1193 additions and 77 deletions
+18 -6
View File
@@ -54,7 +54,7 @@ enum ScreenshotSeed {
for (eIndex, e) in s.3.enumerated() {
let isDuration = e.0 == "Plank"
let ex = Exercise(id: ULID.make(), name: e.0, order: eIndex, sets: e.1, reps: e.2,
weight: e.3, loadType: (isDuration ? LoadType.duration : .weight).rawValue,
weight: Double(e.3), loadType: (isDuration ? LoadType.duration : .weight).rawValue,
durationTotalSeconds: isDuration ? 45 : 0)
ex.split = split
context.insert(ex)
@@ -64,7 +64,7 @@ enum ScreenshotSeed {
let upper = splits[0]
// ---- Past finished sessions (Lat Pull Down trend for the chart) --------
let liftTrend = [95, 100, 105, 110]
let liftTrend: [Double] = [95, 100, 105, 110]
for (i, w) in liftTrend.enumerated() {
let date = daysAgo((liftTrend.count - i) * 4)
let workout = Workout(id: ULID.make(), splitID: nil, splitName: upper.0, start: date,
@@ -72,11 +72,12 @@ enum ScreenshotSeed {
createdAt: date, updatedAt: date, jsonRelativePath: "")
context.insert(workout)
for (eIndex, e) in upper.3.enumerated() {
let weight = e.0 == "Lat Pull Down" ? w : e.3
let weight = e.0 == "Lat Pull Down" ? w : Double(e.3)
let log = WorkoutLog(id: ULID.make(), exerciseName: e.0, order: eIndex, sets: e.1,
reps: e.2, weight: weight, loadType: LoadType.weight.rawValue,
durationTotalSeconds: 0, currentStateIndex: e.1,
statusRaw: WorkoutStatus.completed.rawValue, notes: nil, date: date)
statusRaw: WorkoutStatus.completed.rawValue, notes: nil, date: date,
setEntries: actualEntries(sets: e.1, reps: e.2, weight: weight, at: date))
log.workout = workout
context.insert(log)
}
@@ -91,10 +92,12 @@ enum ScreenshotSeed {
let progress = [(4, WorkoutStatus.completed), (2, .inProgress), (0, .notStarted), (0, .notStarted)]
for (eIndex, e) in upper.3.enumerated() {
let (idx, status) = progress[eIndex]
let weight = e.0 == "Lat Pull Down" ? 110 : Double(e.3)
let log = WorkoutLog(id: ULID.make(), exerciseName: e.0, order: eIndex, sets: e.1, reps: e.2,
weight: e.0 == "Lat Pull Down" ? 110 : e.3, loadType: LoadType.weight.rawValue,
weight: weight, loadType: LoadType.weight.rawValue,
durationTotalSeconds: 0, currentStateIndex: idx,
statusRaw: status.rawValue, notes: nil, date: today)
statusRaw: status.rawValue, notes: nil, date: today,
setEntries: idx > 0 ? actualEntries(sets: idx, reps: e.2, weight: weight, at: today) : nil)
log.workout = workout
context.insert(log)
}
@@ -102,5 +105,14 @@ enum ScreenshotSeed {
try? context.save()
return workout
}
/// Plausible per-set actuals for a weighted log: plan values, with the last
/// set a couple of reps short so screenshots show real-looking variation.
private static func actualEntries(sets: Int, reps: Int, weight: Double, at date: Date) -> [SetEntry] {
(0..<sets).map { i in
SetEntry(reps: i == sets - 1 ? max(1, reps - 2) : reps, weight: weight,
completedAt: date.addingTimeInterval(Double(i) * 150))
}
}
}
#endif