Workout results
What this is
A workout result is a single logged performance of a workout by a single member: the canonical numeric score (seconds for time, kg for weight, count for reps/rounds, rounds + reps for AMRAP encoded as R*1000+r), Rx/scaled flags, optional set-by-set breakdown (weight kg / distance m / duration seconds / RPE / reps), and a back-reference to the snapshot workout the member performed. The domain also owns personal records (PRs) — auto-detected per (user, library workout) and per (user, exercise) for single-movement weight workouts, plus standalone manual PRs.
Who uses it
| Persona | Role |
|---|---|
| Member | Logs results, owns the row, can update or delete. |
| Coach | Reads any member’s results for compliance / programming feedback (findMemberResults). |
| Owner | Same as coach. |
| Platform | Read-only for analytics rollups. |
Persona impact
- Member gets a permanent history anchored to the exact snapshot they performed. Edits the coach makes to the library Fran after Mia’s completion do not retroactively change Mia’s logged data.
- Coach sees a leaderboard per workout, sorted with Rx rows first, then by
scoreNumeric(lower-is-better fortimescoring). - Owner sees aggregated PR + result trends per member at
/dashboard/members/[id].
Capabilities
- Log a result —
POST /workouts/:workoutId/resultswith optionalassignmentId(auto-completes the assignment + forks the snapshot). OptionalsetResults[]for per-set detail. - Update / delete own result —
PATCH/DELETE /workouts/:workoutId/results/:id. - Workout leaderboard —
GET /workouts/:workoutId/results— all results for the canonical library row (joined viaforkedFromId). Rx-first, then score-sorted. Ranks are assigned post-sort. - My latest result on a workout —
GET /workouts/:workoutId/results/me/latest(with sets). - My result feed —
GET /results/me— all my results across all workouts, most recent first. - PRs —
POST /personal-records/me— manual PR (standalone, by exercise or library workout).GET /personal-records/me— my PRs.GET /members/:membershipId/personal-records— a member’s PRs (any caller).GET /members/:membershipId/results— a member’s full result history (coach role required).
- PR auto-detection — every new result triggers
checkIsPR(libraryWorkoutId, userId). First-ever result is a PR; subsequent results compare numerically against all prior. Lower-is-better fortimescoring; higher-is-better otherwise. Single-exercise weight workouts also write an exercise-level PR (“Back Squat 100kg”). - Canonical numeric storage with display-unit retention —
weight_kg,distance_m,duration_secondsare canonical;weight_display_unit,distance_display_unitretain what the athlete entered so the UI can format back to lb/mi without drift.
Relationship to other features
- workouts —
snapshot_workout_idanchors the result;library_workout_iddenormalizesforkedFromId ?? idfor “everyone’s Fran scores” queries. - workout-assignments — result creation calls
forkSnapshotIfNeeded(assignmentId)ifassignmentIdis supplied, and auto-flips the assignment tocompleted. - exercises — per-set rows reference an
exercise_id; manual PRs and single-exercise-weight workout PRs also key onexercise_id. - community feed — TODO: verify whether
workout_loggedfeed item is emitted on result creation.
Current status
Shipped. Result CRUD, PR auto-detection, manual PRs, leaderboard ordering, and per-set storage are all live. The lazy-fork integration (FIT-152) is in place — every new result forks the assignment’s snapshot if it hasn’t already.
Known gaps / open Linear issues
- PR detection per-exercise only fires for single-movement weight workouts. Multi-movement strength workouts do not generate per-exercise PRs from result data — only manual PR entry. TODO: verify scope.
- No “PR achieved” push notification on auto-detection. TODO: verify.
- Set-result update endpoint not present —
PATCH /results/:idupdates the header (score, notes, rx, scaled, performedAt) but does not let the member edit individual sets after the fact. TODO: verify. findByUser/findMyResultsreturns results without joining set details — for “my latest with sets” the member must call/workouts/:workoutId/results/me/latest.