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');
}
}
Related Servicesβ
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:
- Review the Workout History API documentation
- Test workout operations in GraphQL Playground
- Check workout data structure and timeline format
- Contact the development team for workout tracking questions