Close the live-mirror flow hand-off gaps that skipped a timed phase
Field incident (2026-07-17): a Cardio flow run driven from a locked phone durably completed the 30-min Main Circuit ~5 minutes in, then fought the watch until manual swipes forced agreement. Second bulletproofing pass (BULLETPROOFING.md, "2026-07-18 — Live-run mirror & flow hand-off"): - H4: RunFlowView (both platforms) follows the driver's cross-log hand-off frame (forward-only by plan order, runnable target only), and a remote completion mid-flow hands off via onAdvance instead of dismissing the run (which tore down the Live Activity and stranded the flow in the mirror cover). - H5: wall-clock plausibility gate — a duration exercise never auto-completes before startedAt + sets × duration; the flow-terminal rest self-repairs to computeResume() and the Finish auto-Done holds for a human tap. Human Done taps stay ungated. - M6: both bridges persist liveVersion as a UserDefaults high-water mark so an app relaunch can't get every subsequent frame dropped as stale. - M7: phone list + mirror cover absorbs gain the watch's strictly-newer guard, so a stale cache echo can't regress the working doc. - Diagnostics: run-flow os_log breadcrumbs (page transitions, applied frames, repairs, hand-offs, gate trips) on both drivers. Open, documented: M5 (no reconciliation signal across a single-set duration phase), M8 (watch context apply lacks per-log staleness guard), L5 (spurious pager write-backs read as human swipes).
This commit is contained in:
@@ -79,6 +79,26 @@ struct RunFlowView: View {
|
||||
// than in the child's onDisappear — no longer cuts off the next exercise's cue.
|
||||
speechAnnouncer?.stop()
|
||||
}
|
||||
.onChange(of: liveRun.current) { _, frame in
|
||||
followRemoteHandoff(frame)
|
||||
}
|
||||
}
|
||||
|
||||
/// Follow the driver across an exercise boundary. Auto-advances are never broadcast, so
|
||||
/// a frame for a *different* log of this flow workout is always the peer's discrete
|
||||
/// hand-off into the next exercise — which the per-exercise child can't follow (its
|
||||
/// incoming-frame filter is scoped to `currentLogID`). Forward-only by plan order and
|
||||
/// only into a still-runnable log, so a late-delivered frame for an earlier exercise
|
||||
/// can never yank the flow backwards.
|
||||
private func followRemoteHandoff(_ frame: LiveProgress?) {
|
||||
guard doc.autoAdvance == true,
|
||||
let frame, frame.workoutID == doc.id, frame.logID != currentLogID else { return }
|
||||
let sorted = doc.logs.sorted { $0.order != $1.order ? $0.order < $1.order : $0.id < $1.id }
|
||||
guard let from = sorted.firstIndex(where: { $0.id == currentLogID }),
|
||||
let to = sorted.firstIndex(where: { $0.id == frame.logID }), to > from else { return }
|
||||
let status = WorkoutStatus(rawValue: sorted[to].status) ?? .notStarted
|
||||
guard status == .notStarted || status == .inProgress else { return }
|
||||
advance(to: frame.logID)
|
||||
}
|
||||
|
||||
/// Hand off to the next exercise. Suppresses the leaving exercise's live-ended/activity
|
||||
|
||||
Reference in New Issue
Block a user