Endurance routines (HIIT, cardio, cycling) can carry an optional target bpm (RoutineDocument.targetHeartRate, not schema-bumped — same preference-field rationale as restSeconds/autoAdvance), snapshotted onto the WorkoutDocument at plan time like the other pacing fields. During a run, the watch streams its live HR sample to the phone over a new best-effort liveHeartRate message — deliberately outside the LiveProgress machinery (no version bump, no staging/retry; a gauge, not a record), throttled to changed-bpm-or-10s in the watch bridge. LiveRunState holds the sample with a 30s staleness auto-clear so a dead stream never shows a frozen number. Both run screens show the reading only when the run carries a target: the phone's ExerciseProgressView as a top pill, the watch's in the top-trailing toolbar slot, each tinted by a shared ±5 bpm HeartRateBand with an arrow cue to push harder (low) or ease off (high) — e.g. dialing in a treadmill incline to hold a steady effort. Claude-Session: https://claude.ai/code/session_01Y7ZhkCYWNiTSAFhFCGnJ8n
47 lines
1.8 KiB
Swift
47 lines
1.8 KiB
Swift
//
|
|
// WorkoutStarter.swift
|
|
// Workouts
|
|
//
|
|
// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
|
|
//
|
|
|
|
import IndieSync
|
|
import Foundation
|
|
|
|
/// The one place a workout is created from a routine. Both start paths — the routine
|
|
/// picker sheet and the Today board — call this so the plan-time log snapshot and
|
|
/// the returned id stay identical per site.
|
|
@MainActor
|
|
enum WorkoutStarter {
|
|
/// Builds and saves a fresh workout from a routine (plan-time log snapshot per
|
|
/// exercise) and returns the new workout's id so the caller can navigate once the
|
|
/// cache catches up. Deliberately does NOT touch the watch: the wrist session
|
|
/// launches only when the first exercise actually starts (see
|
|
/// `SyncEngine.onWorkoutBecameActive`), so a peek-then-back never spins one up.
|
|
static func start(routine: Routine, sync: SyncEngine) async -> String {
|
|
let startDate = Date()
|
|
let logs = routine.exercisesArray.enumerated().map { index, exercise in
|
|
WorkoutLogDocument(planFrom: ExerciseDocument(from: exercise), order: index, date: startDate)
|
|
}
|
|
|
|
// A freshly started workout has no `end` — only completion stamps it.
|
|
let doc = WorkoutDocument(
|
|
schemaVersion: WorkoutDocument.currentSchemaVersion,
|
|
id: ULID.make(),
|
|
routineID: routine.id,
|
|
routineName: routine.name,
|
|
start: startDate,
|
|
end: nil,
|
|
status: WorkoutStatus.notStarted.rawValue,
|
|
createdAt: startDate,
|
|
updatedAt: startDate,
|
|
logs: logs,
|
|
restSeconds: routine.restSeconds, autoAdvance: routine.autoAdvance,
|
|
targetHeartRate: routine.targetHeartRate
|
|
)
|
|
|
|
await sync.save(workout: doc)
|
|
return doc.id
|
|
}
|
|
}
|