Commit Graph
25 Commits
Author SHA1 Message Date
rzen b53381e8ce Give the watch workout screen entirely to the up/down timer
The half-and-half split (paged timer over the looping form-guide figure)
is now iPhone-only. On the watch the ExerciseProgressView TabView fills the
whole screen — the figure slot is gone from both the active flow and the
Completed state, and the ExerciseFigure sources and ExerciseMotions
resources are dropped from the watch target since nothing there uses them.

Claude-Session: https://claude.ai/code/session_01HJDQQDA9QdP8zByg43H5v3
2026-07-08 14:45:53 -04:00
rzen 394ec0989e 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
2026-07-08 12:48:37 -04:00
rzen c05e83cff7 Queue document writes durably and surface sync trouble in the UI
iCloud Drive writes now flow through a persistent WriteBacklog sidecar
(drained with backoff, flushed on backgrounding, wiped with the cache on
account change), so a save can never be lost to a transient coordinator
error. A status banner on the workout list surfaces stuck syncing.
Also: the split picker gains a Recent section with day labels, split
rows fold SplitItem into SplitListView, and list rows dim the multiply
sign in sets-by-reps.

Claude-Session: https://claude.ai/code/session_01HJDQQDA9QdP8zByg43H5v3
2026-07-08 12:48:12 -04:00
rzen 2c1e4759ae 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
2026-07-06 16:29:44 -04:00
rzen 7274f155e9 Add the exercise reference library, animated exercise figures, and exercise categories
Exercise Library/ holds per-exercise reference docs (setup, cues,
mistakes, progressions) with SVG visuals and a Python-rendered motion
pipeline; Workouts/ExerciseFigure renders the bundled *.motion.json
rigs as animated stick figures on the exercise screen. Exercises gain
a warm-up/main-circuit category, timed exercises display hold time via
planSummary, and a completed exercise reopens to a check screen instead
of its timers.
2026-07-06 01:15:52 -04:00
rzen 1f2df491db Migrate the sync file layer onto the IndieSync package
Replaces the app-local copies of the extracted storage core with the
IndieSync 0.1.0 package (pinned from the new tag): ICloudFileManager ->
DocumentFileStore + TombstoneStore, ICloudFileMonitor ->
MetadataObserver (batched events with the content-date churn gate),
and the shared ULID / DocumentCoder / Tombstone / VersionedDocument
pieces. SyncEngine stays app-specific and is rewired onto the package
types; documents rename currentSchema -> currentSchemaVersion to adopt
the package protocol. ULID.make() remains as a shim minting the string
form the app keys on.

Behavior-preserving: identical JSON bytes (same coder config), same
stub wire format (kind encodes as the same strings), same soft-delete
ordering, same 30-day prune. WorkoutDocument keeps its local-calendar
month bucketing rather than adopting TimeBucketedLayout's UTC paths --
changing derivation would strand existing files. The watch target
links the package too (ULID + DocumentCoder via Shared); iOS, watchOS,
and widget targets all build.
2026-07-05 08:04:29 -04:00
rzen 0ad93a09da Save workouts to Apple Health with live watch metrics and a post-workout summary
Watch sessions now run an HKLiveWorkoutBuilder: heart rate and active
energy are collected live (shown in an in-workout HUD), saved to Health
as a real HKWorkout on completion, and carried back to the phone as
WorkoutMetrics on the workout document. Phone-only workouts get an
estimated Health workout (MET x bodyweight x duration) after a 10s
debounce that a late-arriving watch session cancels; a launch sweep
backfills workouts completed within the last day. Dedupe is keyed on
metrics.healthKitWorkoutUUID plus the metrics source.

Splits gain an activity type (strength, functional, HIIT, core, cardio,
cycling) that categorizes the Health workout and picks the MET value.
A post-workout summary sheet (duration, calories, avg/max HR, HR zones,
total volume) fills in live and is also shown on completed workouts.
Weights can now display in lb or kg (display-only relabel), synced to
the watch over the existing application context.

WorkoutDocument schema 1->2 (metrics are irreplaceable, so older apps
must quarantine rather than strip them); cache schema 2 rebuilds the
SwiftData store with the new metric columns. Deleting a workout in the
app intentionally leaves its Health record in place.
2026-07-05 07:46:35 -04:00
rzen 7400094eda End the watch session on Discard, plus start-flow UX tweaks
Watch-side follow-through for the End Workout flow:
- The phone now pushes an authoritative set (in-progress, not-started, and
  completed within 24h) instead of the 25 most-recent workouts, and the watch
  prunes any workout absent from it. So a Discard/Delete (or a completed run aging
  out) drops off the watch, empties its active list, and ends the HKWorkoutSession
  — fixing the persistent wrist-raise re-foregrounding. The watch never originates
  a workout, so pruning can't lose local data; the 24h grace keeps a just-finished
  run on screen. The gate pops if the run you're viewing is pruned.

UX tweaks:
- The in-workout ⋯ is now a pull-down Menu (Add Exercise / End Workout) rather than
  an action sheet.
- Starting a split while another workout is still active now prompts to end the
  current one(s) — keeping their progress — or run in parallel. Wired into both
  start paths (the split picker and "Start This Split"), via a shared
  WorkoutDocument.endKeepingProgress() helper.

Claude-Session: https://claude.ai/code/session_01SCv7zvGFcKy47KSTnTLxRe
2026-06-22 21:30:06 -04:00
rzen ecf753fdec Hold a numeric count on the watch's Always-On timer
In the Always-On / wrist-down state, watchOS throttles the system timer
text and collapses sub-minute values to a relative "< 1 min", which
overflows the 50pt counter font and truncates with an ellipsis.

Gate PhaseTimerLayout on isLuminanceReduced: keep the live system timer
while active, but render our own held "~m:ss" snapshot from the wall-clock
anchors in Always-On — so the count stays numeric and on-screen. Timing is
unaffected (haptics/auto-advance still run off the anchor, not the text).

Claude-Session: https://claude.ai/code/session_01SCv7zvGFcKy47KSTnTLxRe
2026-06-21 10:04:39 -04:00
rzen fad629338e Make live-run mirroring symmetric: phone-driven runs reach the watch
Two-way driving only worked watch->phone: the watch's navigated driver
broadcast and the phone auto-presented a follower cover. The reverse
failed on both ends — the phone's in-list ExerciseProgressView never
broadcast (only its cover did), and the watch had no surface to present
an incoming run.

- Wire the live channel into the phone's in-list driver (broadcast +
  follow) via a progressView(logID:) helper in WorkoutLogListView.
- Add a watch follower cover (LiveRunCoverView, mirroring the phone's),
  presented from ContentView when the phone drives a run the watch isn't
  already in; the watch bridge gains presentable / muteLive.
- Add a navigatedRunID guard on both sides so a device already in the run
  follows it inline rather than stacking a cover over itself.

Now starting or driving on either device surfaces the run on the other —
as a follower cover when idle, or inline when already in that run — and
either side can take over.

Claude-Session: https://claude.ai/code/session_01SCv7zvGFcKy47KSTnTLxRe
2026-06-20 23:21:04 -04:00
rzen b2d670724a Anchor mirrored live-run timers to the sender's wall clock
The two-way live run unified the propped-phone mirror onto the real
ExerciseProgressView driver, whose phase views anchor their timers to a
local Date() captured when the page becomes active. For a remote-driven
transition that meant anchoring at phaseStart + delivery-latency +
render-time, so the receiver's countdown lagged the driver — visibly so
iPhone->watch, where the watch is the slower receiver (apply -> SwiftUI
re-render -> TabView animation -> phase view activates).

The frame already carries phaseStart/phaseEnd wall-clock anchors (the way
the old read-only mirror counted off them). Honor them: when a phase is
reached by applying a remote frame, the active phase view anchors its
timer to the frame's phaseStart/phaseEnd instead of local now. Scoped to
the remote-driven page (remoteAnchorPage) so a local swipe or auto-advance
still self-anchors; any local transition clears it. Applied symmetrically
to both the iPhone and watch drivers.

Delivery latency no longer shifts the displayed timer in either direction
(limited only by sub-second inter-device clock skew), and the auto-advance
at zero fires together since both share the same phaseEnd.

Claude-Session: https://claude.ai/code/session_01SCv7zvGFcKy47KSTnTLxRe
2026-06-20 22:31:51 -04:00
rzen 8f69497b24 Make the live run two-way: drive from either device
The propped-up iPhone now runs the real ExerciseProgressView for a live
watch workout instead of a read-only mirror, and the live-run channel is
symmetric — either device can drive the flow and the other follows.

Each page transition is classified human / auto / remote: only human
transitions (swipe, Start, One More, swipe-back reset) are broadcast and
recorded by the actor; auto-advances (rest / timed-work countdown) record
locally but aren't sent, since both devices reach them independently off
the shared wall-clock anchors; an applied remote frame jumps the page
without re-recording or re-broadcasting. That rule is also what stops an
echo loop.

- PhoneConnectivityBridge gains sendLiveProgress/sendLiveEnded (the
  missing phone->watch direction); WatchConnectivityBridge receives
  frames into an observable liveIncoming via a new didReceiveMessage
  route. Both share one increasing per-run version sequence so the
  stale-frame guard works across the two devices' counters.
- Both ExerciseProgressViews gain an incomingFrame input + applyIncoming
  (syncing setCount for a remote One More); the iPhone one gains the
  liveSnapshot/broadcast machinery the watch already had.
- New LiveRunCoverView wraps the real driver for the cover (resolves the
  workout, persists via SyncEngine, wires the live channel + close);
  ContentView presents it; LiveProgressMirrorView is removed.

Claude-Session: https://claude.ai/code/session_01SCv7zvGFcKy47KSTnTLxRe
2026-06-20 22:11:05 -04:00
rzen a16e8ec270 Mirror a live Apple Watch run on a propped-up iPhone
Add an ephemeral live-run presence channel (separate from the durable
iCloud progress sync) so a propped-up iPhone can mirror the Watch's
Ready → work/rest → Finish flow in real time as the user swipes.

Watch drives, phone mirrors (read-only), so there's no echo loop:
- Watch's ExerciseProgressView broadcasts a LiveProgress frame on every
  phase transition (and an ended signal on leave) via sendMessage,
  reachable-only — throwaway presence, never written to iCloud.
- Timers ride as wall-clock anchors (Date kept native in the WC dict to
  preserve sub-second precision), so both devices count independently
  off shared start times and stay in lockstep without streaming ticks.
- Phone holds a transient LiveRunState; ContentView auto-presents a
  read-only LiveProgressMirrorView full-screen cover while a run is live.

Claude-Session: https://claude.ai/code/session_01SCv7zvGFcKy47KSTnTLxRe
2026-06-20 21:08:32 -04:00
rzen 8ef0e96b31 Park the Watch run while iPhone edits an exercise or split
Publish an exclusive-edit lock (editingWorkoutID / editingSplitID) in the
phone→watch application context. While the phone has a workout's exercise
(ExerciseView) or a split (SplitDetailView) open in an editor, the watch pops
out of that run, blocks re-entry, and shows it as "Editing on iPhone" — so the
two devices never drive the same run at once and the watch can't clobber the
phone's edit with a stale optimistic write. The lock clears when the editor
closes; absent keys in the latest-wins context mean "not editing".

Claude-Session: https://claude.ai/code/session_01SCv7zvGFcKy47KSTnTLxRe
2026-06-20 19:54:31 -04:00
rzen 707d71eaf0 Fix watch freeze on the progress flow; make Ready? always reachable
Tapping an in-progress exercise on the watch froze the app in an infinite
SwiftUI re-render loop. WorkoutLogListView.body observed SwiftData two ways
the iPhone list deliberately avoids: a @Query-bound Split, and a traversal of
its `exercises` relationship during render (availableExercises). Reading an
observed model's relationship inside body keeps the view perpetually
subscribed and re-invalidating. Fix: fetch the split imperatively (not via
@Query), gate the Add-Exercise affordances on the value-type doc.splitID, and
evaluate availableExercises only from the picker sheet's closure. The list
body now depends solely on the value-type working doc.

Also remove the temporary on-screen diagnostics/PVDiag plumbing and restore
PhaseTimerLayout and the dot-row animation that were dropped while debugging.

Make the Ready? page always lead the exercise flow on both watch and iPhone
(previously only for not-started exercises), so a resumed run can swipe back
to it. A deliberate swipe back to Ready? resets the run; the transient paged
TabView snap-to-0 on open is guarded by a settle gate plus an adjacent-swipe
check, so an in-progress exercise lands on its set and is never reset on open.

Claude-Session: https://claude.ai/code/session_01SCv7zvGFcKy47KSTnTLxRe
2026-06-20 19:02:09 -04:00
rzen 21ee05053e Make the brand purple the accent color and exercise Done check
Populate the previously-empty AccentColor asset (iOS + watch) with the
logo purple — a deep shade in light mode, brightened for dark mode and
the watch's black background. The exercise Done check now uses that
accent color and the in-progress indicator reads as a neutral gray, on
both iPhone and Apple Watch.
2026-06-20 17:48:05 -04:00
rzen 9c3d146b89 TEMP: on-screen diagnostic overlay for the watch hang
Records recent ExerciseProgressView lifecycle/mutation events (init count, page changes, bridge updates, phase starts) and shows them as a green overlay so they can be read off a frozen screen on-device. To be reverted once the hang is diagnosed.
2026-06-20 16:03:30 -04:00
rzen 2ae8dc6dc9 Match the Apple Watch progress run to the iPhone redesign
Times work sets count down for timed exercises (rep sets still count up), rest tint is now gray, and the dot row shows one marker per work set (dash on the active set, gap widening at the current rest). Start/Done/One More are capsule buttons with a heavier rounded label and Start is text-only; the Finished! label is gone; swiping back to Ready resets the run. Shares a timer-layout skeleton so work/rest timers match; keeps WatchKit haptics, the cancel-X toolbar, the screenshot hook, and watch-sized fonts.
2026-06-20 15:36:10 -04:00
rzen f06c4e996e Rework the Apple Watch progress flow
Watch root lists every in-progress workout; picking an exercise runs a paged Ready -> work/rest -> Finish flow (One More + auto-firing Done), with a phase-dot row and brand-tinted count-up/down timers. Includes the configurable rest and auto-finish settings synced over WatchConnectivity and the wrist-down timer fix.
2026-06-20 14:15:31 -04:00
rzen f2da47a70a Keep the screen awake during workouts; fix watch timers freezing
- iPhone: disable the idle timer while the exercise detail screen is open,
  so the display no longer sleeps mid-set.
- Watch: the work/rest timers counted on a run-loop Timer that watchOS
  throttles in the Always-On (wrist-down) state, so they froze. Anchor both
  to a wall-clock Date rendered with SwiftUI's self-updating timer text;
  rest haptics + auto-advance now derive from the end time so they catch up
  after a stall instead of stalling.
2026-06-20 12:00:34 -04:00
rzen 84d45a6d41 Add App Store screenshot harness + listing metadata
DEBUG-only screenshot support (seeded sample data via ScreenshotSeed, per-screen
launch args, screenshot roots for both apps) so iPhone + Apple Watch App Store
shots can be captured deterministically from the simulator — all excluded from
release builds. Also seed ExerciseView's set-grid progress in init so it renders
correctly on the first frame. Adds Scripts/metadata/ as the listing source of
truth (copy, URLs, review notes, and the captured screenshots).

Claude-Session: https://claude.ai/code/session_018gg69MaUetDNzWzBXisfMV
2026-06-19 19:23:54 -04:00
rzen 8c6e798aba Make watch HIIT progress monotonic and resume to next unfinished set
Completing a work phase advances the set count to the iPhone, and a finished
set is never un-counted — a transient paged-TabView snap to page 0 can no longer
overwrite progress with 0. Reopening an exercise now jumps to the first
unfinished set's work page (re-asserted after first layout) instead of starting
back at set 1.

Claude-Session: https://claude.ai/code/session_018gg69MaUetDNzWzBXisfMV
2026-06-19 16:41:41 -04:00
rzen d5915a9552 Add HIIT watch runner, rest-time setting, and HealthKit watch auto-launch
- Redesign the watch app into an active-workout runner: a root gate shows the
  in-progress workout's exercises or prompts to start one on iPhone, and each
  exercise runs as a horizontally-paged HIIT cycle (count-up work, count-down
  rest with final-three-second haptics + auto-advance, One More / Done on the
  last set). Replaces the old history list.
- Add a configurable rest-between-sets duration in iPhone Settings (default 45s),
  synced to the watch over WatchConnectivity.
- Launch the watch app into the session when a workout starts on the phone via
  HealthKit (startWatchApp); the watch runs an HKWorkoutSession for foreground
  runtime and ends it when the workout finishes. Adds the HealthKit entitlement +
  Health usage strings on both targets and WKBackgroundModes on the watch.

Claude-Session: https://claude.ai/code/session_018gg69MaUetDNzWzBXisfMV
2026-06-19 16:16:44 -04:00
rzen 85d0eaddbb Workouts 2.0: re-base persistence on iCloud Drive documents
Replace Core Data + NSPersistentCloudKitContainer + App-Group store +
WatchConnectivity dictionary sync with the QuickRabbit iCloud-documents
architecture:

- iCloud Drive JSON documents are the sole source of truth (one file per
  aggregate: Splits/<ULID>.json, Workouts/YYYY/MM/<ULID>.json), with a
  rebuildable SwiftData cache populated only by an NSMetadataQuery observer
  and a connect-time reconcile. Soft-delete tombstones; hard iCloud gate.
- Shared model layer (ULID, Codable *Documents + stateless mappers, @Model
  cache entities, SwiftData container) compiled into both targets.
- New iPhone<->Watch bridge over WatchConnectivity keyed by ULIDs; the phone
  is the sole writer of iCloud Drive, the watch round-trips documents.
- AppServices DI + iCloud-required root gate; Swift 6 strict concurrency.
- Starter splits generated on demand from the bundled YAML catalogs.
- Migrate to XcodeGen (project.yml), iOS 26 / watchOS 26; CloudDocuments
  entitlement (drop CloudKit/App Group/aps-environment).
- Duration stored as Int seconds (was a Date epoch hack); fix workout
  end-on-create, undismissable delete dialog, toolbar-hiding nav stacks,
  and the Settings placeholder.
- Add README/CHANGELOG/LICENSE, .gitignore, refreshed REQUIREMENTS, and the
  Scripts/ TestFlight pipeline (release.sh + ASC API scripts).

MARKETING_VERSION 2.0.
2026-06-19 14:25:27 -04:00
rzen 9a881e841b Add WatchConnectivity for bidirectional iOS-Watch sync
Implement real-time sync between iOS and Apple Watch apps using
WatchConnectivity framework. This replaces reliance on CloudKit
which doesn't work reliably in simulators.

- Add WatchConnectivityManager to both iOS and Watch targets
- Sync workouts, splits, exercises, and logs between devices
- Update iOS views to trigger sync on data changes
- Add onChange observer to ExerciseView for live progress updates
- Configure App Groups for shared container storage
- Add Watch app views: WorkoutLogsView, WorkoutLogListView, ExerciseProgressView
2026-01-19 19:15:38 -05:00