Add machine comfort settings and tighten the document schemas

Machine-based exercises now carry ordered name/value comfort settings
(seat height, back-rest position, ...) on both ExerciseDocument and, as a
plan-time snapshot, WorkoutLogDocument — the optional array doubles as the
machine flag. Editable from the exercise editor's new Machine section and
from the workout row's settings sheet, whose mid-workout edits write back
to the originating split (workout saved first, so seed clone-on-edit
repointing can't clobber the log edit).

Schema tightening rides the same rev: splits bump to v2 (weight-reminder
fields and the unused exercise category removed), workouts to v3 (the
derived `completed` flag removed; status is the single source). Starter
seeds regenerated at v2 with unchanged ULIDs; SwiftData cache schema
bumped to rebuild. SCHEMA.md documents the shapes.

Claude-Session: https://claude.ai/code/session_01LEoff8bXGBS83tK1c55Mf7
This commit is contained in:
2026-07-06 16:29:44 -04:00
parent fce8fa4c17
commit 2c1e4759ae
33 changed files with 1132 additions and 586 deletions
@@ -189,25 +189,38 @@ struct ExerciseProgressView: View {
var body: some View {
if startsCompleted {
CompletedPhaseView()
// Same top/bottom split as the run flow, so the form-guide figure
// stays on screen (matching the iPhone counterpart).
VStack(spacing: 0) {
CompletedPhaseView()
ExerciseFigureSlot(exerciseName: log?.exerciseName ?? "")
}
} else {
flowBody
}
}
private var flowBody: some View {
TabView(selection: $currentPage) {
ForEach(0..<totalPages, id: \.self) { index in
page(for: index)
.tag(index)
VStack(spacing: 0) {
// Paged flow top half.
TabView(selection: $currentPage) {
ForEach(0..<totalPages, id: \.self) { index in
page(for: index)
.tag(index)
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
.overlay(alignment: .bottom) {
if let dots = workDots {
WorkPhaseDots(model: dots)
.padding(.bottom, 2)
.tabViewStyle(.page(indexDisplayMode: .never))
.frame(maxWidth: .infinity, maxHeight: .infinity)
.overlay(alignment: .bottom) {
if let dots = workDots {
WorkPhaseDots(model: dots)
.padding(.bottom, 2)
}
}
// Bottom half: the looping form-guide figure when a bundled motion
// matches this exercise; empty space otherwise.
ExerciseFigureSlot(exerciseName: log?.exerciseName ?? "")
}
.toolbar {
ToolbarItem(placement: .cancellationAction) {
@@ -464,7 +477,7 @@ struct ExerciseProgressView: View {
private func beginExercise() {
guard let i = doc.logs.firstIndex(where: { $0.id == logID }) else { return }
guard doc.logs[i].status == WorkoutStatus.notStarted.rawValue else { return }
doc.logs[i].status = WorkoutStatus.inProgress.rawValue
doc.logs[i].transition(to: .inProgress)
recomputeWorkoutStatus()
doc.updatedAt = Date()
onChange()
@@ -478,8 +491,7 @@ struct ExerciseProgressView: View {
if let i = doc.logs.firstIndex(where: { $0.id == logID }) {
doc.logs[i].sets = newCount
doc.logs[i].currentStateIndex = newCount - 1 // every prior set is now complete
doc.logs[i].status = WorkoutStatus.inProgress.rawValue
doc.logs[i].completed = false
doc.logs[i].transition(to: .inProgress)
recomputeWorkoutStatus()
doc.updatedAt = Date()
onChange()
@@ -506,8 +518,7 @@ struct ExerciseProgressView: View {
guard reached > doc.logs[i].currentStateIndex else { return }
doc.logs[i].currentStateIndex = reached
doc.logs[i].status = WorkoutStatus.inProgress.rawValue
doc.logs[i].completed = false
doc.logs[i].transition(to: .inProgress)
recomputeWorkoutStatus()
doc.updatedAt = Date()
@@ -520,11 +531,9 @@ struct ExerciseProgressView: View {
let log = doc.logs[i]
// Skip the write if it's already pristine (e.g. landing on Ready before any set).
guard log.currentStateIndex != 0
|| log.status != WorkoutStatus.notStarted.rawValue
|| log.completed else { return }
|| log.status != WorkoutStatus.notStarted.rawValue else { return }
doc.logs[i].currentStateIndex = 0
doc.logs[i].status = WorkoutStatus.notStarted.rawValue
doc.logs[i].completed = false
doc.logs[i].transition(to: .notStarted)
recomputeWorkoutStatus()
doc.updatedAt = Date()
onChange()
@@ -533,8 +542,7 @@ struct ExerciseProgressView: View {
private func completeExercise() {
guard let i = doc.logs.firstIndex(where: { $0.id == logID }) else { return }
doc.logs[i].currentStateIndex = setCount
doc.logs[i].status = WorkoutStatus.completed.rawValue
doc.logs[i].completed = true
doc.logs[i].transition(to: .completed)
recomputeWorkoutStatus()
doc.updatedAt = Date()