6/8 - Public Beta (Discord)
|See Changelog
Skip to main content

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.
  • WorkoutFeedback models enjoyment, fatigue, stress, pain, freeform notes, and multi-muscle soreness entries, enabling recovery dashboards.
  • TimelineContext entries 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 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.

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[], deleted flag (soft delete), relations to user, workoutTemplate, userProgramInstance.
  • Composite relations: completedExercises[], feedback, postStretching.

Completed Exercise & Set Payloads

  • CompletedExercise: order, exercise notes, pain summary, progressionMessage, targetWarmup[], targetWorkingSets[], and workingSetSummary for 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 of SorenessEntry objects (muscle group + level).
  • PostWorkoutStretching: duration in minutes plus individual PostStretchSet entries (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_enforced events when testing bulk imports.
  • When writing tests, seed data under tests/unit/services/workoutHistory (mirror module path) and run npm run test -- workoutHistory or broader suites before PRs.
  • Effective Workout integration mock: when testing recalculateWorkoutTargets, stub EffectiveWorkoutService.generateWorkout or use the provided fixtures in tests/fixtures/effectiveWorkout.