Workout History & Timeline Logging
Why It Matters
- Single log for athletes and coaches: Timeline groups, set notes, context, and recovery cues live in one immutable audit trail that mirrors what Sam actually did—not just what the template prescribed.
- Data quality for downstream engines: Progression Playbooks, Effective Workout, and analytics all lean on accurate history; this feature enforces structure (sets, tags, soreness) without slowing lifters down.
- Retention + privacy aware: Built-in quotas, soft delete, and stat suppression flags keep historical data aligned with compliance requirements while respecting user controls.
- Media + context rich: Technique segments, pain tracking, rest timers, post-workout stretching, and photo links capture the qualitative data coaches rely on when evaluating performance.
- GraphQL-native automation: Everything in WorkoutHistory—creation, updates, filters, recalculations—can be scripted for migrations, QA fixtures, or companion apps.
Headline Capabilities
Timeline Logging
- Timeline items align with the Effective Workout prescription (group/fatigue/rest). Each item contains one or more completed exercises with set arrays.
- Sets capture weight, reps, RPE, rest seconds, AMRAP flag, set type, tempo, technique type, and optional technique segments (when users have
CAN_LOG_SET_TECHNIQUES). - Pain tracking lives directly on sets for fine-grained injury analysis.
Context & Feedback Stack
- Session-level metadata includes tags, week numbers, completion %, rpe-weighted volume, and average RPE.
WorkoutFeedbackmodels enjoyment, fatigue, stress, pain, freeform notes, and multi-muscle soreness entries, enabling recovery dashboards.TimelineContextentries store group-level notes, actual rest, fatigue ratings, and modality (complexes vs. straight sets).- Post-workout stretching objects hold duration plus per-muscle stretch sets.
Governance & Automation
- Retention policy: Enforced per user via quotas—older entries are soft-deleted automatically, with audit events emitted.
- Stat suppression: A toggle excludes specific workouts from analytics while keeping the raw record for audits.
- Recalculate targets: When programs change mid-block, the service can regenerate target warmup/working sets for a past entry without touching the completed set data.
- Permission-aware views: Admins can inspect any entry; regular users only see their own. Feature flags gate pain & technique data to limit PII exposure.
Feature Walkthrough
- Sam's Daily Log
- Coach Alex Review
- Retrofit & Targets
Sam finishes Monday's squat + hinge session. The mobile client captures the Effective Workout timeline and posts it through createWorkoutHistory.
- Each timeline item tracks actual rest (
actualRestAfterGroupSeconds) and a fatigue emoji. - All sets run through weight-unit normalization before storing; when Sam switches to pounds later, responses convert back automatically.
- RPE-weighted volume and completion % are computed server-side so analytics stay consistent.
Alex filters Sam's history using workoutHistoryForUser with programWorkoutTemplateId.
- He inspects technique segments on paused squats (only visible because Alex has
CAN_LOG_SET_TECHNIQUES). - Alex toggles
statSupressionfor a travel-day workout so it does not contaminate block averages but keeps the notes for reference.
Mid-block, Sam swaps to a heavier bar. The template changes, so Alex runs recalculateWorkoutTargets to regenerate the per-set targets for last week's entry.
- Completed data remains intact; only
targetWarmup,targetWorkingSets, and summaries refresh using Effective Workout rules. - Progression Playbooks consume the updated targets the next time they evaluate.
Data Model Highlights
WorkoutHistoryEntry Fields
- Core timing:
startTime,endTime,durationMinutes,weekNumber. - Performance stats:
completionPercentage,totalVolume,performedExerciseCount,totalRepsPerformed,averageRepsPerSet,averageWeightPerRep,rpeWeightedVolume,trainingIntensityScore,averageRPE. - Context:
tags[],statSupression,bodyWeightKgAtCompletion,timelineContext[],deletedflag (soft delete), relations touser,workoutTemplate,userProgramInstance. - Composite relations:
completedExercises[],feedback,postStretching.
Completed Exercise & Set Payloads
CompletedExercise: order, exercise notes, pain summary,progressionMessage,targetWarmup[],targetWorkingSets[], andworkingSetSummaryfor quick UI hints.ExerciseSet: weight, reps, RPE, rest, notes, set type, tempo, AMRAP flag, failure/spotter booleans, pain metadata, technique + segments.TimelineContext: timeline item order, type (group, circuit, etc.), technique, actual rest, group fatigue, notes.
Feedback & Recovery Objects
WorkoutFeedback: enjoyment rating, session fatigue, overall RPE, stress, pain markers, notes, array ofSorenessEntryobjects (muscle group + level).PostWorkoutStretching: duration in minutes plus individualPostStretchSetentries (muscle group, stretch name, duration seconds, notes).
GraphQL Reference
Queries
query WorkoutHistoryEntry($id: ID!) {
workoutHistoryEntry(id: $id) {
id
startTime
durationMinutes
completionPercentage
timelineContext { type actualRestAfterGroupSeconds groupFatigueRating }
completedExercises {
exercise { name }
sets { setNumber weight reps rpe isAmrap }
workingSetSummary { setCount repsMin repsMax restSeconds }
}
feedback {
enjoymentRating
sessionFatigue
sorenessEntries { muscleGroup { name } level }
}
postStretching {
durationMinutes
postStretchSets { stretchName durationSeconds muscleGroup { name } }
}
}
}
query WorkoutHistoryForUser($take: Int, $skip: Int, $startDate: DateTime, $endDate: DateTime, $programWorkoutTemplateId: ID, $tags: [String!]) {
workoutHistoryForUser(
take: $take
skip: $skip
startDate: $startDate
endDate: $endDate
programWorkoutTemplateId: $programWorkoutTemplateId
tags: $tags
) {
id
startTime
durationMinutes
completionPercentage
tags
statSupression
}
}
Sample filter variables:
{
"take": 10,
"skip": 0,
"startDate": "2025-08-01T00:00:00.000Z",
"endDate": "2025-09-01T00:00:00.000Z",
"programWorkoutTemplateId": "pwt_squat_day",
"tags": ["travel", "deload"]
}
Mutations
Create
mutation CreateWorkoutHistory($input: CreateWorkoutHistoryInput!) {
createWorkoutHistory(input: $input) {
id
durationMinutes
completionPercentage
performedExerciseCount
tags
}
}
{
"input": {
"programWorkoutTemplateId": "pwt_squat_day",
"programUserInstanceId": "pui_sam",
"startTime": "2025-09-10T12:00:00.000Z",
"endTime": "2025-09-10T13:05:00.000Z",
"weekNumber": 8,
"tags": ["travel", "compressed"],
"completedTimeline": [
{
"timelineItemOrder": 1,
"groupNotes": "Warm hips",
"actualRestAfterGroupSeconds": 90,
"groupFatigueRating": 3,
"exercises": [
{
"exerciseId": "ex_back_squat",
"sets": [
{ "setNumber": 1, "weight": 150, "reps": 5, "rpe": 7, "restSeconds": 150 },
{ "setNumber": 2, "weight": 150, "reps": 5, "rpe": 7.5, "restSeconds": 150 }
]
}
]
}
],
"feedback": {
"enjoymentRating": 8,
"overallRPE": 8,
"sessionFatigue": 7,
"stressLevel": 4,
"notes": "Flew in late"
},
"workoutPostStretching": {
"durationMinutes": 10,
"postStretchSets": [
{ "muscleGroupId": "mg_hamstrings", "stretchName": "PNF Hamstring", "durationSeconds": 60 }
]
}
}
}
Update & Delete
mutation UpdateWorkoutHistory($historyId: ID!, $input: UpdateWorkoutHistoryInput!) {
updateWorkoutHistory(historyId: $historyId, input: $input) {
id
endTime
tags
statSupression
}
}
mutation DeleteWorkoutHistory($id: ID!) {
deleteWorkoutHistory(id: $id) {
id
deleted
}
}
Stat Suppression Toggle
mutation ToggleStatSuppression($historyId: ID!, $suppress: Boolean!) {
toggleWorkoutHistoryStatSuppression(history_id: $historyId, suppress: $suppress) {
id
statSupression
}
}
Recalculate Targets
mutation RecalculateTargets($historyId: ID!) {
recalculateWorkoutTargets(input: { historyId: $historyId }) {
id
completedExercises {
exercise { name }
targetWarmup { setNumber targetWeight targetReps }
targetWorkingSets { setNumber targetWeight targetReps isAmrap }
}
}
}
Sample response fragment:
{
"data": {
"recalculateWorkoutTargets": {
"id": "wh_123",
"completedExercises": [
{
"exercise": { "name": "Back Squat" },
"targetWarmup": [
{ "setNumber": 1, "targetWeight": 100, "targetReps": 5 },
{ "setNumber": 2, "targetWeight": 130, "targetReps": 3 }
],
"targetWorkingSets": [
{ "setNumber": 1, "targetWeight": 155, "targetReps": 5, "isAmrap": false },
{ "setNumber": 3, "targetWeight": 155, "targetReps": 5, "isAmrap": true }
]
}
]
}
}
}
Implementation & Testing Notes
- GraphQL resolvers are intentionally thin: authorization, quota enforcement, and transformations all happen in
WorkoutHistoryService. - Service enforces permissions (
CAN_VIEW_WORKOUT_LOGS,CAN_LOG_SET_TECHNIQUES,CAN_LOG_PAIN_TRACKING) before revealing sensitive fields. - Retention jobs run before insertion to keep storage under quota; watch for emitted
workout_history.retention_enforcedevents when testing bulk imports. - When writing tests, seed data under
tests/unit/services/workoutHistory(mirror module path) and runnpm run test -- workoutHistoryor broader suites before PRs. - Effective Workout integration mock: when testing
recalculateWorkoutTargets, stubEffectiveWorkoutService.generateWorkoutor use the provided fixtures intests/fixtures/effectiveWorkout.