Commit Graph
12 Commits
Author SHA1 Message Date
rzen 3c7a790e9d Rebuild the exercise figure on an anatomical 3D skeleton
The library's planar world-angle rig becomes a genuine 3D anatomical
model: skeleton.json holds bone-length profiles (real shoulder/pelvis
widths, feet, neutral/female/male) and per-joint ROM; motions pose
joints with anatomical angles (flexion/abduction/rotation from neutral
standing) under a per-exercise orthographic camera, resolved by
kinematics.py (3D FK, analytic two-bone IK with anatomical write-back)
and validated against physiological ranges. All 20 sagittal motions
were migrated by planar decomposition with 0.00 px golden parity against
the old renderer — relabeled to true anatomy, since shading is now
near-dark/far-light by camera depth rather than by limb suffix — and
the face-on machines are re-authored honestly: Abductor/Adductor with
real hip abduction (the foreshortened "frontal" profile is retired) and
Rotary with genuine spine axial rotation. Figures gain articulated
feet; profiles swap without touching a single motion script; --orbit
sweeps the camera 360° while a motion loops.

The in-app SwiftUI renderer (iOS + watch) is ported to the same model
and consumes the exported motions verbatim; figure-fixtures.json pins
its geometry to the Python pipeline within 0.5 px across every
exercise, key frame, tween, and orbit sample. Also makes the watch
bridge logger nonisolated for the newer SDK's stricter isolation
checking.

Claude-Session: https://claude.ai/code/session_01LEoff8bXGBS83tK1c55Mf7
2026-07-06 20:10:50 -04:00
rzen 653c3ac3cf Update the changelog and README for the machine-settings and seeding work
Claude-Session: https://claude.ai/code/session_01LEoff8bXGBS83tK1c55Mf7
2026-07-06 16:29:59 -04:00
rzen 888852cc2e Replace the YAML exercise catalogs with the motion-rig exercise library
The exercise picker now lists the bundled exercise library — the
motion-rig exercises exported from Exercise Library/ — instead of the
two *.exercises.yaml starter catalogs. ExerciseListLoader and the Yams
dependency are removed; ExerciseMotionLibrary gains exerciseNames, the
bundle enumeration the picker reads.
2026-07-06 12:27:44 -04:00
rzen 936a585ece Seed starter splits deterministically with immutable clone-on-edit seeds
Starter splits ship as byte-canonical SplitDocument JSON with fixed
ULIDs (Workouts/Resources/StarterSplits, regenerated by
Scripts/generate_starter_splits.swift) and auto-seed after connect into
a verifiably empty container, re-checked after a settle delay — wrong
guesses are harmless because identical bytes make same-path conflicts
empty and tombstones reap resurrected seeds. Seeds are immutable:
SyncEngine.save(split:) forks an edited seed to a fresh ULID and
soft-deletes the original, whose stub is exempt from pruning
(IndieSync 0.3.0 prune(exempting:)) and vetoes resurrection forever;
split views resolve by id through a redirect map to follow the swap.
Add Starter Splits in Settings restores deleted seeds by lifting the
veto stub and rewriting the bundle bytes.

Also fixes ingestFromWatch bypassing the tombstone veto (a phone-deleted
workout resurrected when a stale watch resent it) and reaps a live file
immediately when its tombstone arrives.

SplitDetailView also picks up the category-grouped exercise sections
from the exercise-category work.
2026-07-06 01:16:05 -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 e2295aa287 Add an "End Workout" flow for partially-done workouts
Replace the in-workout "+" toolbar button with an ellipsis menu offering
"Add Exercise" and "End Workout". Ending opens a Save/Discard action sheet:
Save marks the remaining exercises as skipped and resolves the workout to
completed (stamping end), which drops it off the watch's active list and ends
the watch's HealthKit session; Discard soft-deletes it.

Teach the status-from-logs derivation that a skipped log is terminal, and
consolidate the three duplicated copies into a single shared
WorkoutDocument.recomputeStatusFromLogs() so an ended workout stays finished
regardless of which screen the next edit comes from.

Claude-Session: https://claude.ai/code/session_01SCv7zvGFcKy47KSTnTLxRe
2026-06-22 18:34:14 -04:00
rzen 192aa6f95a Add an Apple Watch face complication that opens the app
A static WidgetKit accessory widget (launcher only — no data sharing,
App Group, or entitlements). Tapping any accessory widget opens its
containing app, so this is enough to place a Workouts button on the
watch face. Supports the circular, corner, inline, and rectangular
accessory families.

New 'Workouts Watch Widget' app-extension target embedded in the watch
app via project.yml.
2026-06-20 22:15:26 -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 3bba78eab5 Open a paged progress run when tapping an iPhone exercise
Tapping an exercise now opens ExerciseProgressView -- the watch's Ready -> work/rest -> Finish flow on iPhone, with rep sets counting up, timed sets and rests counting down and auto-advancing, a work-set dot row (dash on the active set, gap widening at the current rest), and capsule Start/Done/One More buttons. The detail/edit screen moves behind a trailing Edit swipe (leading swipe still completes). Swiping back to the Ready page resets the run.
2026-06-20 14:15:43 -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