Restructure into a three-tab app with Progress, goals, and Meditation
The UX redesign's first landing (spec in UX-REDESIGN.md): ContentView becomes a Today / Progress / Settings TabView, "Routine" replaces "Split" in every user-facing string and view name (code-level types keep their names), and workout starting moves to shared WorkoutStarter / StartedWorkoutNavigator plumbing. - New Progress tab: weekly goal streaks, workout trends, per-exercise weight progression, achievements, and the full history list (WorkoutLogsView -> WorkoutHistoryView). - Goals: stable categories workouts roll up to, managed from Settings. - New Meditation exercise + starter routine; timed sits record to Apple Health as Mind & Body sessions. Claude-Session: https://claude.ai/code/session_012qw2itfzKyEJ1HpsFt8Ex4
This commit is contained in:
@@ -22,10 +22,10 @@ struct WriteBacklogTests {
|
||||
return Calendar(identifier: .gregorian).date(from: comps)!
|
||||
}
|
||||
|
||||
private func workoutDoc(id: String, start: Date, updatedAt: Date, splitName: String? = nil) -> WorkoutDocument {
|
||||
private func workoutDoc(id: String, start: Date, updatedAt: Date, routineName: String? = nil) -> WorkoutDocument {
|
||||
WorkoutDocument(
|
||||
schemaVersion: WorkoutDocument.currentSchemaVersion, id: id, splitID: nil,
|
||||
splitName: splitName, start: start, end: nil,
|
||||
schemaVersion: WorkoutDocument.currentSchemaVersion, id: id, routineID: nil,
|
||||
routineName: routineName, start: start, end: nil,
|
||||
status: WorkoutStatus.notStarted.rawValue, createdAt: updatedAt, updatedAt: updatedAt,
|
||||
logs: [], metrics: nil
|
||||
)
|
||||
@@ -35,12 +35,12 @@ struct WriteBacklogTests {
|
||||
/// bookkeeping field, so replacement/retry/backoff tests can set up exact
|
||||
/// preconditions directly instead of driving them indirectly through `SyncEngine`.
|
||||
private func pendingWorkout(
|
||||
id: String, start: Date, timestamp: Date, enqueuedAt: Date, splitName: String? = nil,
|
||||
id: String, start: Date, timestamp: Date, enqueuedAt: Date, routineName: String? = nil,
|
||||
attempts: Int = 0, lastAttemptAt: Date? = nil, faultMessage: String? = nil,
|
||||
stalePaths: Set<String> = []
|
||||
) -> PendingWrite {
|
||||
PendingWrite(
|
||||
payload: .workout(workoutDoc(id: id, start: start, updatedAt: timestamp, splitName: splitName)),
|
||||
payload: .workout(workoutDoc(id: id, start: start, updatedAt: timestamp, routineName: routineName)),
|
||||
timestamp: timestamp, stalePaths: stalePaths, enqueuedAt: enqueuedAt,
|
||||
attempts: attempts, lastAttemptAt: lastAttemptAt, faultMessage: faultMessage
|
||||
)
|
||||
@@ -117,14 +117,14 @@ struct WriteBacklogTests {
|
||||
@Test func equalTimestampIncomingWinsSlot() throws {
|
||||
var backlog = WriteBacklog()
|
||||
let id = "01EQUALTIMESTAMP000000001"
|
||||
backlog.enqueue(pendingWorkout(id: id, start: Self.t0, timestamp: Self.t0, enqueuedAt: Self.t0, splitName: "Original"))
|
||||
backlog.enqueue(pendingWorkout(id: id, start: Self.t0, timestamp: Self.t0, enqueuedAt: Self.t0, splitName: "Incoming"))
|
||||
backlog.enqueue(pendingWorkout(id: id, start: Self.t0, timestamp: Self.t0, enqueuedAt: Self.t0, routineName: "Original"))
|
||||
backlog.enqueue(pendingWorkout(id: id, start: Self.t0, timestamp: Self.t0, enqueuedAt: Self.t0, routineName: "Incoming"))
|
||||
|
||||
let slot = try #require(backlog.pendingWrite(for: id))
|
||||
guard case .workout(let doc) = slot.payload else {
|
||||
Issue.record("expected a workout payload"); return
|
||||
}
|
||||
#expect(doc.splitName == "Incoming")
|
||||
#expect(doc.routineName == "Incoming")
|
||||
}
|
||||
|
||||
/// An incoming write strictly older than the occupant is dropped outright — the
|
||||
@@ -132,14 +132,14 @@ struct WriteBacklogTests {
|
||||
@Test func strictlyOlderIncomingIsDropped() throws {
|
||||
var backlog = WriteBacklog()
|
||||
let id = "01OLDERDROPPED0000000001"
|
||||
backlog.enqueue(pendingWorkout(id: id, start: Self.t0, timestamp: Self.t0, enqueuedAt: Self.t0, splitName: "Kept"))
|
||||
backlog.enqueue(pendingWorkout(id: id, start: Self.t0, timestamp: Self.t0.addingTimeInterval(-5), enqueuedAt: Self.t0, splitName: "Ignored"))
|
||||
backlog.enqueue(pendingWorkout(id: id, start: Self.t0, timestamp: Self.t0, enqueuedAt: Self.t0, routineName: "Kept"))
|
||||
backlog.enqueue(pendingWorkout(id: id, start: Self.t0, timestamp: Self.t0.addingTimeInterval(-5), enqueuedAt: Self.t0, routineName: "Ignored"))
|
||||
|
||||
let slot = try #require(backlog.pendingWrite(for: id))
|
||||
guard case .workout(let doc) = slot.payload else {
|
||||
Issue.record("expected a workout payload"); return
|
||||
}
|
||||
#expect(doc.splitName == "Kept")
|
||||
#expect(doc.routineName == "Kept")
|
||||
#expect(slot.timestamp == Self.t0)
|
||||
}
|
||||
|
||||
@@ -338,16 +338,16 @@ struct SyncEngineWriteQueueTests {
|
||||
|
||||
@MainActor
|
||||
private func makeEngine(backlogURL: URL) throws -> (engine: SyncEngine, container: ModelContainer) {
|
||||
let schema = Schema([Split.self, Exercise.self, Workout.self, WorkoutLog.self])
|
||||
let schema = Schema([Routine.self, Exercise.self, Workout.self, WorkoutLog.self])
|
||||
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true, cloudKitDatabase: .none)
|
||||
let container = try ModelContainer(for: schema, configurations: [config])
|
||||
return (SyncEngine(container: container, backlogURL: backlogURL), container)
|
||||
}
|
||||
|
||||
private func makeWorkoutDoc(id: String, updatedAt: Date, splitName: String? = "Push Day") -> WorkoutDocument {
|
||||
private func makeWorkoutDoc(id: String, updatedAt: Date, routineName: String? = "Push Day") -> WorkoutDocument {
|
||||
WorkoutDocument(
|
||||
schemaVersion: WorkoutDocument.currentSchemaVersion, id: id, splitID: "SPLIT-1",
|
||||
splitName: splitName, start: Self.t0, end: nil,
|
||||
schemaVersion: WorkoutDocument.currentSchemaVersion, id: id, routineID: "RT-1",
|
||||
routineName: routineName, start: Self.t0, end: nil,
|
||||
status: WorkoutStatus.notStarted.rawValue, createdAt: Self.t0, updatedAt: updatedAt,
|
||||
logs: [], metrics: nil
|
||||
)
|
||||
@@ -397,25 +397,25 @@ struct SyncEngineWriteQueueTests {
|
||||
@Test func ingestFromWatchAppliesUpdatedAtGate() async throws {
|
||||
let (engine, container) = try makeEngine(backlogURL: tempBacklogURL())
|
||||
let id = "01INGESTGATE000000000001"
|
||||
await engine.save(workout: makeWorkoutDoc(id: id, updatedAt: Self.t0, splitName: "Push Day"))
|
||||
await engine.save(workout: makeWorkoutDoc(id: id, updatedAt: Self.t0, routineName: "Push Day"))
|
||||
|
||||
// (a) strictly older — dropped, cache unchanged.
|
||||
await engine.ingestFromWatch(makeWorkoutDoc(id: id, updatedAt: Self.t0.addingTimeInterval(-60), splitName: "Push Day"))
|
||||
await engine.ingestFromWatch(makeWorkoutDoc(id: id, updatedAt: Self.t0.addingTimeInterval(-60), routineName: "Push Day"))
|
||||
var cached = try #require(CacheMapper.fetchWorkout(id: id, in: container.mainContext))
|
||||
#expect(cached.updatedAt == Self.t0)
|
||||
|
||||
// (b) same updatedAt but a changed field — the echo case, ignored entirely
|
||||
// (equal timestamp means duplicate, not a genuine edit).
|
||||
await engine.ingestFromWatch(makeWorkoutDoc(id: id, updatedAt: Self.t0, splitName: "Changed Name"))
|
||||
await engine.ingestFromWatch(makeWorkoutDoc(id: id, updatedAt: Self.t0, routineName: "Changed Name"))
|
||||
cached = try #require(CacheMapper.fetchWorkout(id: id, in: container.mainContext))
|
||||
#expect(cached.updatedAt == Self.t0)
|
||||
#expect(cached.splitName == "Push Day")
|
||||
#expect(cached.routineName == "Push Day")
|
||||
|
||||
// (c) strictly newer — accepted.
|
||||
await engine.ingestFromWatch(makeWorkoutDoc(id: id, updatedAt: Self.t0.addingTimeInterval(60), splitName: "Changed Name"))
|
||||
await engine.ingestFromWatch(makeWorkoutDoc(id: id, updatedAt: Self.t0.addingTimeInterval(60), routineName: "Changed Name"))
|
||||
cached = try #require(CacheMapper.fetchWorkout(id: id, in: container.mainContext))
|
||||
#expect(cached.updatedAt == Self.t0.addingTimeInterval(60))
|
||||
#expect(cached.splitName == "Changed Name")
|
||||
#expect(cached.routineName == "Changed Name")
|
||||
}
|
||||
|
||||
/// A queued-but-unwritten delete vetoes resurrection: a watch push racing the
|
||||
|
||||
Reference in New Issue
Block a user