Skip to main content

Workout History Service

The Workout History Service provides comprehensive workout logging and tracking capabilities for the OpenLift platform, managing complete workout sessions with detailed exercise performance data.

Overview​

The Workout History Service handles:

  • Complete workout session logging and retrieval
  • Detailed exercise performance tracking (sets, reps, weight, RPE)
  • Workout timeline management with structured data
  • User feedback and session ratings
  • Post-workout stretching and recovery data
  • Workout analytics data foundation

Key Features​

πŸ“Š Comprehensive Workout Logging​

  • Detailed Exercise Tracking: Sets, reps, weight, RPE, rest periods, tempo
  • Workout Timeline: Structured chronological order of exercises and groups
  • Performance Metrics: Duration, volume, intensity, and completion tracking
  • Pain & Injury Tracking: Pain type, location, severity for safety monitoring

πŸ—“οΈ Session Management​

  • Workout Sessions: Complete workout sessions with start/end times
  • Program Integration: Link workouts to program templates and progressions
  • Week Tracking: Associate workouts with specific program weeks
  • Scheduled vs Actual: Track planned vs completed workouts

πŸ’‘ User Experience Features​

  • Session Feedback: Enjoyment rating, overall RPE, session fatigue
  • Recovery Tracking: Post-workout stretching, soreness levels
  • Notes & Comments: Exercise-specific and workout-level notes
  • Stat Suppression: Hide specific workouts from analytics

Architecture​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Client App β”‚ β”‚ Workout History β”‚ β”‚ Database β”‚
β”‚ β”‚ β”‚ Service β”‚ β”‚ β”‚
β”‚ β€’ Workout Form │───▢│ β€’ Data Valid. │───▢│ β€’ Workout Data β”‚
β”‚ β€’ Timer & Sets │◄───│ β€’ Timeline Proc │◄───│ β€’ Timeline β”‚
β”‚ β€’ History View β”‚ β”‚ β€’ Event Emit β”‚ β”‚ β€’ Feedback β”‚
β”‚ β€’ Analytics β”‚ β”‚ β€’ Stats Calc β”‚ β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Service Responsibilities​

βœ… Workout History Service Handles​

  • Workout session creation, update, and retrieval
  • Exercise performance data validation and storage
  • Workout timeline processing and organization
  • Session feedback and rating management
  • Post-workout data collection (stretching, recovery)
  • Workout analytics data preparation
  • Event emission for workout completion

❌ Workout History Service Does NOT Handle​

  • Workout planning and program creation
  • Exercise progression recommendations
  • Advanced analytics and insights calculation
  • Real-time workout guidance or coaching
  • Social features or workout sharing

Core Data Models​

Workout History Entry​

interface WorkoutHistoryEntry {
id: string;
userId: string;

// Program association
programWorkoutTemplateId: string;
programUserInstanceId?: string;
weekNumber?: number;

// Timing information
startTime: Date;
endTime?: Date;
durationMinutes?: number;

// Workout data
completedTimeline: CompletedTimelineItem[];

// Session feedback
feedback?: WorkoutFeedback;
workoutPostStretching?: PostWorkoutStretching;

// Metadata
tags?: string[];
statSupression?: boolean; // Exclude from analytics

createdAt: Date;
updatedAt: Date;
}

Workout Timeline Structure​

interface CompletedTimelineItem {
timelineItemOrder: number;
exercises: CompletedExercise[];

// Group-level data
groupNotes?: string;
actualRestAfterGroupSeconds?: number;
groupFatigueRating?: number; // 1-10 scale
}

interface CompletedExercise {
exerciseId: string;
exerciseNotes?: string;
sets: CompletedSet[];
}

interface CompletedSet {
setNumber: number;
weight: number;
reps: number;
rpe: number; // Rate of Perceived Exertion (1-10)
restSeconds: number;

// Optional performance data
tempo?: string; // e.g., "3-1-2-0"
skipped?: boolean;
notes?: string;

// Pain/injury tracking
painType?: 'sharp' | 'dull' | 'ache' | 'burning';
painLocation?: string;
painSeverity?: number; // 1-10 scale
setType?: 'working' | 'warmup' | 'dropset' | 'failure';
}

Session Feedback​

interface WorkoutFeedback {
enjoymentRating: number; // 1-10 scale
overallRpe: number; // 1-10 scale
sessionFatigue: number; // 1-10 scale
stressLevel?: number; // 1-10 scale

// Pain reporting
painReported?: boolean;
painLocation?: string;
painSeverity?: number;
painNotes?: string;

// General notes
notes?: string;

// Soreness tracking
sorenessEntries?: SorenessEntry[];
}

interface SorenessEntry {
muscleGroupId: string;
level: number; // 1-10 scale
}

Key Operations​

Create Workout Session​

mutation CreateWorkoutHistory($input: CreateWorkoutHistoryInput!) {
createWorkoutHistory(input: $input) {
id
startTime
endTime
durationMinutes
completedTimeline {
timelineItemOrder
exercises {
exerciseId
sets {
setNumber
weight
reps
rpe
restSeconds
}
}
}
feedback {
enjoymentRating
overallRpe
sessionFatigue
}
}
}

Get User Workout History​

query GetWorkoutHistory(
$take: Int,
$skip: Int,
$startDate: DateTime,
$endDate: DateTime
) {
workoutHistoryForUser(
take: $take,
skip: $skip,
startDate: $startDate,
endDate: $endDate
) {
id
startTime
endTime
durationMinutes
programWorkoutTemplate {
id
name
}
completedTimeline {
exercises {
exercise {
name
}
sets {
weight
reps
rpe
}
}
}
}
}

Update Workout Session​

mutation UpdateWorkoutHistory(
$historyId: ID!,
$input: UpdateWorkoutHistoryInput!
) {
updateWorkoutHistory(historyId: $historyId, input: $input) {
id
endTime
durationMinutes
feedback {
enjoymentRating
overallRpe
}
}
}

Integration Patterns​

Flutter Workout Logging​

class WorkoutLoggingService {
final GraphQLClient _client;

// Start a new workout session
Future<WorkoutHistoryEntry> startWorkout({
required String programWorkoutTemplateId,
String? programUserInstanceId,
int? weekNumber,
}) async {
const mutation = '''
mutation CreateWorkoutHistory(\$input: CreateWorkoutHistoryInput!) {
createWorkoutHistory(input: \$input) {
id
startTime
programWorkoutTemplate {
name
exercises {
id
name
targetSets
}
}
}
}
''';

final input = CreateWorkoutHistoryInput(
programWorkoutTemplateId: programWorkoutTemplateId,
startTime: DateTime.now(),
completedTimeline: [],
programUserInstanceId: programUserInstanceId,
weekNumber: weekNumber,
);

final result = await _client.mutate(MutationOptions(
document: gql(mutation),
variables: {'input': input.toJson()},
));

return WorkoutHistoryEntry.fromJson(result.data!['createWorkoutHistory']);
}

// Log completed exercise set
Future<void> logExerciseSet({
required String workoutId,
required String exerciseId,
required CompletedSet setData,
}) async {
// Update the workout timeline with new set data
await updateWorkoutTimeline(workoutId, exerciseId, setData);
}

// Complete workout session
Future<WorkoutHistoryEntry> completeWorkout({
required String workoutId,
required WorkoutFeedback feedback,
PostWorkoutStretching? stretching,
}) async {
const mutation = '''
mutation UpdateWorkoutHistory(
\$historyId: ID!,
\$input: UpdateWorkoutHistoryInput!
) {
updateWorkoutHistory(historyId: \$historyId, input: \$input) {
id
endTime
durationMinutes
feedback {
enjoymentRating
overallRpe
sessionFatigue
}
}
}
''';

final input = UpdateWorkoutHistoryInput(
endTime: DateTime.now(),
feedback: feedback,
workoutPostStretching: stretching,
);

final result = await _client.mutate(MutationOptions(
document: gql(mutation),
variables: {
'historyId': workoutId,
'input': input.toJson(),
},
));

return WorkoutHistoryEntry.fromJson(result.data!['updateWorkoutHistory']);
}
}

Real-time Workout Tracking​

class WorkoutTracker extends ChangeNotifier {
WorkoutHistoryEntry? currentWorkout;
List<CompletedSet> currentExerciseSets = [];
Stopwatch restTimer = Stopwatch();

// Start new exercise
Future<void> startExercise(String exerciseId) async {
currentExerciseId = exerciseId;
currentExerciseSets.clear();
notifyListeners();
}

// Complete set with automatic rest timer
Future<void> completeSet({
required double weight,
required int reps,
required int rpe,
String? notes,
}) async {
final set = CompletedSet(
setNumber: currentExerciseSets.length + 1,
weight: weight,
reps: reps,
rpe: rpe,
restSeconds: 0, // Will be updated when next set starts
notes: notes,
);

currentExerciseSets.add(set);

// Start rest timer for next set
restTimer.reset();
restTimer.start();

// Log to backend
await workoutService.logExerciseSet(
workoutId: currentWorkout!.id,
exerciseId: currentExerciseId,
setData: set,
);

notifyListeners();
}
}

Event System Integration​

Workout Events​

interface WorkoutEvents {
'workout.started': {
userId: string;
workoutId: string;
programTemplateId: string;
startTime: Date;
};

'workout.completed': {
userId: string;
workoutId: string;
duration: number;
totalVolume: number;
exerciseCount: number;
};

'exercise.completed': {
userId: string;
workoutId: string;
exerciseId: string;
sets: CompletedSet[];
};

'set.logged': {
userId: string;
workoutId: string;
exerciseId: string;
setData: CompletedSet;
};
}

Event Listeners​

class WorkoutHistoryEventHandlers {
// Trigger progression analysis when workout completes
@EventListener('workout.completed')
async handleWorkoutCompleted(event: WorkoutCompletedEvent) {
await this.progressionPlaybookService.analyzeWorkoutForProgression(
event.userId,
event.workoutId
);

await this.workoutAnalyticsService.processWorkoutMetrics(
event.workoutId
);
}

// Update recovery recommendations
@EventListener('workout.completed')
async handleWorkoutForRecovery(event: WorkoutCompletedEvent) {
await this.recoveryService.updateRecoveryRecommendations(
event.userId,
event.workoutId
);
}
}

Data Validation & Processing​

Workout Data Validation​

class WorkoutDataValidator {
validateWorkoutSession(dto: CreateWorkoutHistoryDto): ValidationResult {
const errors: string[] = [];

// Validate timeline structure
if (!dto.completedTimeline || dto.completedTimeline.length === 0) {
errors.push('Workout must contain at least one exercise');
}

// Validate exercise data
dto.completedTimeline.forEach((item, index) => {
if (!item.exercises || item.exercises.length === 0) {
errors.push(`Timeline item ${index} must contain exercises`);
}

item.exercises.forEach(exercise => {
if (!exercise.sets || exercise.sets.length === 0) {
errors.push(`Exercise ${exercise.exerciseId} must contain sets`);
}

exercise.sets.forEach(set => {
if (set.weight < 0 || set.reps <= 0 || set.rpe < 1 || set.rpe > 10) {
errors.push(`Invalid set data for exercise ${exercise.exerciseId}`);
}
});
});
});

return {
isValid: errors.length === 0,
errors
};
}
}

Performance Optimization​

Efficient Data Loading​

class OptimizedWorkoutHistoryService {
// Load workout history with pagination
Future<WorkoutHistoryPage> getWorkoutHistory({
int limit = 20,
int offset = 0,
DateTime? startDate,
DateTime? endDate,
}) async {
const query = '''
query GetWorkoutHistory(
\$take: Int,
\$skip: Int,
\$startDate: DateTime,
\$endDate: DateTime
) {
workoutHistoryForUser(
take: \$take,
skip: \$skip,
startDate: \$startDate,
endDate: \$endDate
) {
id
startTime
endTime
durationMinutes
# Load only essential data for list view
programWorkoutTemplate {
name
}
}
}
''';

// Cache frequently accessed data
final result = await _client.query(QueryOptions(
document: gql(query),
variables: {
'take': limit,
'skip': offset,
'startDate': startDate?.toIso8601String(),
'endDate': endDate?.toIso8601String(),
},
cachePolicy: CachePolicy.cacheFirst,
));

return WorkoutHistoryPage.fromJson(result.data!);
}
}

Analytics Integration​

Workout Metrics Calculation​

class WorkoutMetricsProcessor {
calculateWorkoutMetrics(workout: WorkoutHistoryEntry): WorkoutMetrics {
const timeline = workout.completedTimeline;

// Calculate total volume
const totalVolume = timeline.reduce((volume, item) => {
return volume + item.exercises.reduce((exerciseVolume, exercise) => {
return exerciseVolume + exercise.sets.reduce((setVolume, set) => {
return setVolume + (set.weight * set.reps);
}, 0);
}, 0);
}, 0);

// Calculate average RPE
const allSets = timeline.flatMap(item =>
item.exercises.flatMap(ex => ex.sets)
);
const averageRpe = allSets.reduce((sum, set) => sum + set.rpe, 0) / allSets.length;

return {
totalVolume,
averageRpe,
totalSets: allSets.length,
exerciseCount: timeline.reduce((count, item) => count + item.exercises.length, 0),
duration: workout.durationMinutes || 0,
};
}
}

Error Handling​

Workout Service Errors​

try {
await workoutService.createWorkout(workoutData);
} on GraphQLError catch (e) {
switch (e.extensions?['code']) {
case 'INVALID_PROGRAM_TEMPLATE':
showError('Selected workout template not found');
break;
case 'INVALID_EXERCISE_DATA':
showError('Exercise data contains invalid values');
break;
case 'WORKOUT_IN_PROGRESS':
showError('Cannot start workout while another is in progress');
break;
case 'UNAUTHORIZED':
showError('You can only log your own workouts');
break;
default:
showError('Failed to save workout');
}
}

Dependencies​

  • Authentication Service: User verification
  • Exercise Service: Exercise metadata and validation
  • Program Plans Service: Workout template information

Consumers​

  • Workout Analytics Service: Performance analysis and insights
  • Progression Playbook Service: Progression recommendations
  • Recovery Service: Recovery tracking and recommendations
  • Effective Workout Service: Workout effectiveness analysis
  • Coaching Service: Performance-based coaching recommendations

API Documentation​

For detailed GraphQL schema and usage examples, see:

  • Workout History API documentation: coming soon

Support​

For workout history questions:

  1. Review the Workout History API documentation
  2. Test workout operations in GraphQL Playground
  3. Check workout data structure and timeline format
  4. Contact the development team for workout tracking questions