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

Recovery & Recovery Analysis API

The Recovery and Recovery Analysis APIs provide comprehensive recovery tracking and intelligent analysis services for the OpenLift platform. This system enables users to log daily wellness metrics and receive data-driven insights about their recovery status, fatigue patterns, and personalized training recommendations.

Overview

OpenLift's recovery system operates on two complementary levels:

  1. Recovery Tracking: Simple, user-friendly check-ins for sleep, energy, stress, mood, and muscle soreness
  2. Recovery Analysis: Advanced AI-driven analysis that transforms raw recovery data into actionable insights

The system helps users and coaches understand recovery patterns, detect overreaching/overtraining, and make intelligent program modifications to optimize training outcomes.

Core Concepts

Recovery Check-In

A Recovery Check-In captures a user's wellness status at a specific point in time:

interface WorkoutRecoveryCheckIn {
id: string;
userId: string;
workoutHistoryEntryId?: string; // Optional workout association
timestamp: Date;

// Wellness metrics (0-10 scale)
sleepQuality: number;
nutritionQuality: number;
energyLevel: number;
readinessSleepQuality: number;
mood: number;
stressLevel: number;

// Overall training readiness
readyToTrainToday: 'READY' | 'SOMEWHAT_READY' | 'NOT_READY';

// Optional notes and muscle soreness
notes?: string;
sorenessEntries: RecoverySorenessEntry[];
}

Recovery Score

A Recovery Score is a composite metric (0-100) that weighs multiple recovery dimensions:

interface RecoveryScore {
overallScore: number; // 0-100 weighted composite
dimensions: RecoveryDimension[]; // Individual component scores
lastUpdated: Date;
dataPointsCount: number; // Check-ins used in calculation
confidenceLevel: 'low' | 'medium' | 'high';
}

Fatigue Pattern Analysis

Fatigue Pattern classification helps identify training stress accumulation:

interface FatiguePattern {
classification: 'Normal' | 'Acute_Overreaching' | 'Functional_Overreaching' |
'NonFunctional_Overreaching' | 'Overtraining_Syndrome';
confidence: number; // 0-100
durationDays: number; // How long pattern persisted
keyIndicators: string[]; // Metrics driving classification
riskLevel: 'low' | 'moderate' | 'high' | 'critical';
recommendationUrgency: 'none' | 'monitor' | 'action_recommended' | 'immediate_action';
}

GraphQL Schema

Recovery Tracking Operations

Queries

Get Single Recovery Check-In
query GetRecoveryCheckIn($id: ID!) {
recoveryCheckIn(id: $id) {
id
userId
workoutHistoryEntryId
timestamp
sleepQuality
nutritionQuality
energyLevel
readinessSleepQuality
mood
stressLevel
readyToTrainToday
notes
sorenessEntries {
sorenessLevel
muscleGroup {
id
name
}
}
user {
id
firstName
}
workoutHistoryEntry {
id
startedAt
}
}
}
Get All User Recovery Check-Ins
query GetMyRecoveryCheckIns {
myRecoveryCheckIns {
id
timestamp
sleepQuality
nutritionQuality
energyLevel
readinessSleepQuality
mood
stressLevel
readyToTrainToday
notes
sorenessEntries {
sorenessLevel
muscleGroup {
name
}
}
}
}

Mutations

Create Recovery Check-In
mutation CreateRecoveryCheckIn($input: CreateRecoveryCheckInInput!) {
createRecoveryCheckIn(input: $input) {
id
timestamp
sleepQuality
nutritionQuality
energyLevel
readinessSleepQuality
mood
stressLevel
readyToTrainToday
notes
sorenessEntries {
sorenessLevel
muscleGroup {
name
}
}
}
}
Update Recovery Check-In
mutation UpdateRecoveryCheckIn($checkInId: ID!, $input: UpdateRecoveryCheckInInput!) {
updateRecoveryCheckIn(checkInId: $checkInId, input: $input) {
id
timestamp
sleepQuality
nutritionQuality
energyLevel
readinessSleepQuality
mood
stressLevel
readyToTrainToday
notes
}
}
Delete Recovery Check-In
mutation DeleteRecoveryCheckIn($input: RecoveryCheckInIdInput!) {
deleteRecoveryCheckIn(input: $input)
}

Recovery Analysis Operations

Queries

Get Recovery Score
query GetRecoveryScore($timeframe: RecoveryTimeframe = WEEK) {
getRecoveryScore(timeframe: $timeframe) {
overallScore
dimensions {
name
score
trend
weight
}
lastUpdated
dataPointsCount
confidenceLevel
}
}
query GetRecoveryTrends($period: TrendPeriod!) {
getRecoveryTrends(period: $period) {
period
startDate
endDate
trendDirection
trendStrength
averageScore
scoreVariance
dataPoints {
date
score
checkInCount
}
}
}
Get Fatigue Pattern
query GetFatiguePattern {
getFatiguePattern {
classification
confidence
durationDays
keyIndicators
riskLevel
recommendationUrgency
}
}
Get Recovery Prompting Context
query GetRecoveryPromptingContext($type: RecoveryPromptType!) {
getRecoveryPromptingContext(type: $type) {
promptType
contextualInfo {
lastWorkoutDate
currentProgramPhase
recentRecoveryTrend
}
questions {
id
text
inputType
required
contextHint
}
estimatedCompletionTimeSeconds
}
}
Get Coach Recovery Analytics
query GetCoachRecoveryAnalytics($filters: RecoveryAnalyticsFiltersInput) {
getCoachRecoveryAnalytics(filters: $filters) {
clients {
userId
userName
currentRecoveryScore
lastCheckIn
alertLevel
fatiguePattern
trendDirection
programModificationsPending
}
summary {
totalClients
clientsWithAlerts
averageRecoveryScore
clientsNeedingAttention
}
}
}

Mutations

Trigger Recovery Analysis
mutation TriggerRecoveryAnalysis($input: TriggerRecoveryAnalysisInput!) {
triggerRecoveryAnalysis(input: $input) {
analysisId
userId
analysisType
timestamp
recoveryScore {
overallScore
dimensions {
name
score
trend
}
confidenceLevel
}
trendAnalysis {
trendDirection
trendStrength
averageScore
}
fatiguePattern {
classification
confidence
riskLevel
recommendationUrgency
}
recommendations {
type
severity
requiresCoachApproval
autoApply
reasoning
expectedDurationDays
targetRecoveryScoreImprovement
}
insights
}
}
Mark Recovery Prompt Completed
mutation MarkRecoveryPromptCompleted($input: PromptCompletionInput!) {
markRecoveryPromptCompleted(input: $input)
}

Input Types

Recovery Check-In Inputs

input CreateRecoveryCheckInInput {
workoutHistoryEntryId: ID # Optional workout association
timestamp: DateTime! # When check-in was recorded
sleepQuality: Int! # 0-10 rating
nutritionQuality: Int! # 0-10 rating
energyLevel: Int! # 0-10 rating
readinessSleepQuality: Int! # 0-10 rating
mood: Int! # 0-10 rating
stressLevel: Int! # 0-10 rating
readyToTrainToday: RecoveryReadiness! # Overall readiness
notes: String # Optional notes
sorenessEntries: [RecoverySorenessEntryInput!]! # Muscle soreness data
}

input RecoverySorenessEntryInput {
muscleGroupId: ID! # Muscle group reference
sorenessLevel: Int! # 0-10 soreness rating
}

input UpdateRecoveryCheckInInput {
workoutHistoryEntryId: ID
timestamp: DateTime
sleepQuality: Int
nutritionQuality: Int
energyLevel: Int
readinessSleepQuality: Int
mood: Int
stressLevel: Int
readyToTrainToday: RecoveryReadiness
notes: String
sorenessEntries: [RecoverySorenessEntryInput!]
}

Recovery Analysis Inputs

input TriggerRecoveryAnalysisInput {
analysisType: RecoveryAnalysisType! # Type of analysis
workoutHistoryEntryId: String # For post-workout analysis
forceRefresh: Boolean # Bypass caching
}

input RecoveryAnalyticsFiltersInput {
clientIds: [String!] # Specific clients to include
startDate: DateTime # Analysis start date
endDate: DateTime # Analysis end date
alertLevel: AlertLevel # Filter by alert level
fatiguePattern: FatigueClassification # Filter by fatigue pattern
sortBy: String # Sort field
sortOrder: String # Sort order (asc/desc)
limit: Int # Max results
offset: Int # Results to skip
}

Enums

enum RecoveryReadiness {
READY
SOMEWHAT_READY
NOT_READY
}

enum RecoveryTimeframe {
DAY
WEEK
MONTH
}

enum TrendPeriod {
WEEK
MONTH
QUARTER
}

enum TrendDirection {
IMPROVING
STABLE
DECLINING
}

enum FatigueClassification {
NORMAL
ACUTE_OVERREACHING
FUNCTIONAL_OVERREACHING
NON_FUNCTIONAL_OVERREACHING
OVERTRAINING_SYNDROME
}

enum RecoveryAnalysisType {
POST_WORKOUT
DAILY_WELLNESS
WEEKLY_TRENDS
MANUAL
}

enum ModificationType {
DELOAD
VOLUME_REDUCTION
INTENSITY_REDUCTION
REST_DAY_INSERTION
PROGRAM_PAUSE
}

Authentication & Authorization

All Recovery and Recovery Analysis operations require user authentication:

// Add authentication headers
final headers = {
'Authorization': 'Bearer $accessToken',
'Content-Type': 'application/json',
};

Flutter Integration

Setting Up GraphQL Operations

1. Define GraphQL Documents

// lib/graphql/recovery_queries.dart
const String GET_MY_RECOVERY_CHECK_INS = '''
query GetMyRecoveryCheckIns {
myRecoveryCheckIns {
id
timestamp
sleepQuality
nutritionQuality
energyLevel
readinessSleepQuality
mood
stressLevel
readyToTrainToday
notes
sorenessEntries {
sorenessLevel
muscleGroup {
id
name
}
}
}
}
''';

const String CREATE_RECOVERY_CHECK_IN = '''
mutation CreateRecoveryCheckIn(\$input: CreateRecoveryCheckInInput!) {
createRecoveryCheckIn(input: \$input) {
id
timestamp
sleepQuality
nutritionQuality
energyLevel
readinessSleepQuality
mood
stressLevel
readyToTrainToday
notes
sorenessEntries {
sorenessLevel
muscleGroup {
id
name
}
}
}
}
''';

const String GET_RECOVERY_SCORE = '''
query GetRecoveryScore(\$timeframe: RecoveryTimeframe) {
getRecoveryScore(timeframe: \$timeframe) {
overallScore
dimensions {
name
score
trend
weight
}
lastUpdated
dataPointsCount
confidenceLevel
}
}
''';

const String GET_RECOVERY_TRENDS = '''
query GetRecoveryTrends(\$period: TrendPeriod!) {
getRecoveryTrends(period: \$period) {
period
startDate
endDate
trendDirection
trendStrength
averageScore
scoreVariance
dataPoints {
date
score
checkInCount
}
}
}
''';

const String GET_FATIGUE_PATTERN = '''
query GetFatiguePattern {
getFatiguePattern {
classification
confidence
durationDays
keyIndicators
riskLevel
recommendationUrgency
}
}
''';

const String TRIGGER_RECOVERY_ANALYSIS = '''
mutation TriggerRecoveryAnalysis(\$input: TriggerRecoveryAnalysisInput!) {
triggerRecoveryAnalysis(input: \$input) {
analysisId
userId
analysisType
timestamp
recoveryScore {
overallScore
dimensions {
name
score
trend
}
confidenceLevel
}
trendAnalysis {
trendDirection
trendStrength
averageScore
}
fatiguePattern {
classification
confidence
riskLevel
recommendationUrgency
}
recommendations {
type
severity
requiresCoachApproval
autoApply
reasoning
expectedDurationDays
targetRecoveryScoreImprovement
}
insights
}
}
''';

2. Create Data Models

// lib/models/recovery.dart
enum RecoveryReadiness { ready, somewhatReady, notReady }
enum TrendDirection { improving, stable, declining }
enum FatigueClassification {
normal, acuteOverreaching, functionalOverreaching,
nonFunctionalOverreaching, overtrainingSyndrome
}

class RecoverySorenessEntry {
final int sorenessLevel;
final MuscleGroup muscleGroup;

RecoverySorenessEntry({
required this.sorenessLevel,
required this.muscleGroup,
});

factory RecoverySorenessEntry.fromJson(Map<String, dynamic> json) {
return RecoverySorenessEntry(
sorenessLevel: json['sorenessLevel'],
muscleGroup: MuscleGroup.fromJson(json['muscleGroup']),
);
}
}

class WorkoutRecoveryCheckIn {
final String id;
final String userId;
final String? workoutHistoryEntryId;
final DateTime timestamp;
final int sleepQuality;
final int nutritionQuality;
final int energyLevel;
final int readinessSleepQuality;
final int mood;
final int stressLevel;
final RecoveryReadiness readyToTrainToday;
final String? notes;
final List<RecoverySorenessEntry> sorenessEntries;

WorkoutRecoveryCheckIn({
required this.id,
required this.userId,
this.workoutHistoryEntryId,
required this.timestamp,
required this.sleepQuality,
required this.nutritionQuality,
required this.energyLevel,
required this.readinessSleepQuality,
required this.mood,
required this.stressLevel,
required this.readyToTrainToday,
this.notes,
required this.sorenessEntries,
});

factory WorkoutRecoveryCheckIn.fromJson(Map<String, dynamic> json) {
return WorkoutRecoveryCheckIn(
id: json['id'],
userId: json['userId'],
workoutHistoryEntryId: json['workoutHistoryEntryId'],
timestamp: DateTime.parse(json['timestamp']),
sleepQuality: json['sleepQuality'],
nutritionQuality: json['nutritionQuality'],
energyLevel: json['energyLevel'],
readinessSleepQuality: json['readinessSleepQuality'],
mood: json['mood'],
stressLevel: json['stressLevel'],
readyToTrainToday: _parseRecoveryReadiness(json['readyToTrainToday']),
notes: json['notes'],
sorenessEntries: (json['sorenessEntries'] as List)
.map((e) => RecoverySorenessEntry.fromJson(e))
.toList(),
);
}

static RecoveryReadiness _parseRecoveryReadiness(String value) {
switch (value) {
case 'READY':
return RecoveryReadiness.ready;
case 'SOMEWHAT_READY':
return RecoveryReadiness.somewhatReady;
case 'NOT_READY':
return RecoveryReadiness.notReady;
default:
return RecoveryReadiness.somewhatReady;
}
}
}

class RecoveryDimension {
final String name;
final int score;
final TrendDirection trend;
final double weight;

RecoveryDimension({
required this.name,
required this.score,
required this.trend,
required this.weight,
});

factory RecoveryDimension.fromJson(Map<String, dynamic> json) {
return RecoveryDimension(
name: json['name'],
score: json['score'],
trend: _parseTrendDirection(json['trend']),
weight: json['weight'].toDouble(),
);
}

static TrendDirection _parseTrendDirection(String value) {
switch (value) {
case 'improving':
return TrendDirection.improving;
case 'stable':
return TrendDirection.stable;
case 'declining':
return TrendDirection.declining;
default:
return TrendDirection.stable;
}
}
}

class RecoveryScore {
final int overallScore;
final List<RecoveryDimension> dimensions;
final DateTime lastUpdated;
final int dataPointsCount;
final String confidenceLevel;

RecoveryScore({
required this.overallScore,
required this.dimensions,
required this.lastUpdated,
required this.dataPointsCount,
required this.confidenceLevel,
});

factory RecoveryScore.fromJson(Map<String, dynamic> json) {
return RecoveryScore(
overallScore: json['overallScore'],
dimensions: (json['dimensions'] as List)
.map((e) => RecoveryDimension.fromJson(e))
.toList(),
lastUpdated: DateTime.parse(json['lastUpdated']),
dataPointsCount: json['dataPointsCount'],
confidenceLevel: json['confidenceLevel'],
);
}
}

class FatiguePattern {
final FatigueClassification classification;
final int confidence;
final int durationDays;
final List<String> keyIndicators;
final String riskLevel;
final String recommendationUrgency;

FatiguePattern({
required this.classification,
required this.confidence,
required this.durationDays,
required this.keyIndicators,
required this.riskLevel,
required this.recommendationUrgency,
});

factory FatiguePattern.fromJson(Map<String, dynamic> json) {
return FatiguePattern(
classification: _parseFatigueClassification(json['classification']),
confidence: json['confidence'],
durationDays: json['durationDays'],
keyIndicators: List<String>.from(json['keyIndicators']),
riskLevel: json['riskLevel'],
recommendationUrgency: json['recommendationUrgency'],
);
}

static FatigueClassification _parseFatigueClassification(String value) {
switch (value) {
case 'Normal':
return FatigueClassification.normal;
case 'Acute_Overreaching':
return FatigueClassification.acuteOverreaching;
case 'Functional_Overreaching':
return FatigueClassification.functionalOverreaching;
case 'NonFunctional_Overreaching':
return FatigueClassification.nonFunctionalOverreaching;
case 'Overtraining_Syndrome':
return FatigueClassification.overtrainingSyndrome;
default:
return FatigueClassification.normal;
}
}
}

3. Create Service Class

// lib/services/recovery_service.dart
import 'package:graphql_flutter/graphql_flutter.dart';
import '../graphql/recovery_queries.dart';
import '../models/recovery.dart';

class RecoveryService {
final GraphQLClient _client;

RecoveryService(this._client);

/// Get all recovery check-ins for the authenticated user
Future<List<WorkoutRecoveryCheckIn>> getMyRecoveryCheckIns() async {
final options = QueryOptions(
document: gql(GET_MY_RECOVERY_CHECK_INS),
errorPolicy: ErrorPolicy.all,
);

final result = await _client.query(options);

if (result.hasException) {
throw result.exception!;
}

final data = result.data?['myRecoveryCheckIns'] as List? ?? [];
return data.map((e) => WorkoutRecoveryCheckIn.fromJson(e)).toList();
}

/// Create a new recovery check-in
Future<WorkoutRecoveryCheckIn> createRecoveryCheckIn({
String? workoutHistoryEntryId,
required DateTime timestamp,
required int sleepQuality,
required int nutritionQuality,
required int energyLevel,
required int readinessSleepQuality,
required int mood,
required int stressLevel,
required RecoveryReadiness readyToTrainToday,
String? notes,
required List<Map<String, dynamic>> sorenessEntries,
}) async {
final options = MutationOptions(
document: gql(CREATE_RECOVERY_CHECK_IN),
variables: {
'input': {
'workoutHistoryEntryId': workoutHistoryEntryId,
'timestamp': timestamp.toIso8601String(),
'sleepQuality': sleepQuality,
'nutritionQuality': nutritionQuality,
'energyLevel': energyLevel,
'readinessSleepQuality': readinessSleepQuality,
'mood': mood,
'stressLevel': stressLevel,
'readyToTrainToday': _recoveryReadinessToString(readyToTrainToday),
'notes': notes,
'sorenessEntries': sorenessEntries,
},
},
errorPolicy: ErrorPolicy.all,
);

final result = await _client.mutate(options);

if (result.hasException) {
throw result.exception!;
}

final data = result.data?['createRecoveryCheckIn'];
if (data == null) {
throw Exception('No recovery check-in data received');
}

return WorkoutRecoveryCheckIn.fromJson(data);
}

/// Get recovery score for a specific timeframe
Future<RecoveryScore?> getRecoveryScore({
String timeframe = 'WEEK',
}) async {
final options = QueryOptions(
document: gql(GET_RECOVERY_SCORE),
variables: {
'timeframe': timeframe,
},
errorPolicy: ErrorPolicy.all,
);

final result = await _client.query(options);

if (result.hasException) {
throw result.exception!;
}

final data = result.data?['getRecoveryScore'];
if (data == null) {
return null; // Insufficient data
}

return RecoveryScore.fromJson(data);
}

/// Get fatigue pattern analysis
Future<FatiguePattern?> getFatiguePattern() async {
final options = QueryOptions(
document: gql(GET_FATIGUE_PATTERN),
errorPolicy: ErrorPolicy.all,
);

final result = await _client.query(options);

if (result.hasException) {
throw result.exception!;
}

final data = result.data?['getFatiguePattern'];
if (data == null) {
return null; // Insufficient data
}

return FatiguePattern.fromJson(data);
}

/// Trigger comprehensive recovery analysis
Future<Map<String, dynamic>> triggerRecoveryAnalysis({
required String analysisType,
String? workoutHistoryEntryId,
bool forceRefresh = false,
}) async {
final options = MutationOptions(
document: gql(TRIGGER_RECOVERY_ANALYSIS),
variables: {
'input': {
'analysisType': analysisType,
'workoutHistoryEntryId': workoutHistoryEntryId,
'forceRefresh': forceRefresh,
},
},
errorPolicy: ErrorPolicy.all,
);

final result = await _client.mutate(options);

if (result.hasException) {
throw result.exception!;
}

final data = result.data?['triggerRecoveryAnalysis'];
if (data == null) {
throw Exception('No analysis result received');
}

return data;
}

String _recoveryReadinessToString(RecoveryReadiness readiness) {
switch (readiness) {
case RecoveryReadiness.ready:
return 'READY';
case RecoveryReadiness.somewhatReady:
return 'SOMEWHAT_READY';
case RecoveryReadiness.notReady:
return 'NOT_READY';
}
}
}

4. State Management with Provider

// lib/providers/recovery_provider.dart
import 'package:flutter/foundation.dart';
import '../services/recovery_service.dart';
import '../models/recovery.dart';

class RecoveryProvider extends ChangeNotifier {
final RecoveryService _service;

List<WorkoutRecoveryCheckIn> _checkIns = [];
RecoveryScore? _recoveryScore;
FatiguePattern? _fatiguePattern;
bool _isLoading = false;
String? _errorMessage;

RecoveryProvider(this._service);

// Getters
List<WorkoutRecoveryCheckIn> get checkIns => _checkIns;
RecoveryScore? get recoveryScore => _recoveryScore;
FatiguePattern? get fatiguePattern => _fatiguePattern;
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;

/// Load user's recovery check-ins
Future<void> loadCheckIns() async {
_isLoading = true;
_errorMessage = null;
notifyListeners();

try {
_checkIns = await _service.getMyRecoveryCheckIns();
} catch (e) {
_errorMessage = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}

/// Create new recovery check-in
Future<void> createCheckIn({
String? workoutHistoryEntryId,
required DateTime timestamp,
required int sleepQuality,
required int nutritionQuality,
required int energyLevel,
required int readinessSleepQuality,
required int mood,
required int stressLevel,
required RecoveryReadiness readyToTrainToday,
String? notes,
required List<Map<String, dynamic>> sorenessEntries,
}) async {
_isLoading = true;
_errorMessage = null;
notifyListeners();

try {
final checkIn = await _service.createRecoveryCheckIn(
workoutHistoryEntryId: workoutHistoryEntryId,
timestamp: timestamp,
sleepQuality: sleepQuality,
nutritionQuality: nutritionQuality,
energyLevel: energyLevel,
readinessSleepQuality: readinessSleepQuality,
mood: mood,
stressLevel: stressLevel,
readyToTrainToday: readyToTrainToday,
notes: notes,
sorenessEntries: sorenessEntries,
);

// Add to local list and sort by timestamp
_checkIns.add(checkIn);
_checkIns.sort((a, b) => b.timestamp.compareTo(a.timestamp));
} catch (e) {
_errorMessage = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}

/// Load recovery score
Future<void> loadRecoveryScore({String timeframe = 'WEEK'}) async {
try {
_recoveryScore = await _service.getRecoveryScore(timeframe: timeframe);
notifyListeners();
} catch (e) {
_errorMessage = e.toString();
notifyListeners();
}
}

/// Load fatigue pattern
Future<void> loadFatiguePattern() async {
try {
_fatiguePattern = await _service.getFatiguePattern();
notifyListeners();
} catch (e) {
_errorMessage = e.toString();
notifyListeners();
}
}

/// Trigger comprehensive analysis
Future<Map<String, dynamic>?> triggerAnalysis({
required String analysisType,
String? workoutHistoryEntryId,
bool forceRefresh = false,
}) async {
try {
final result = await _service.triggerRecoveryAnalysis(
analysisType: analysisType,
workoutHistoryEntryId: workoutHistoryEntryId,
forceRefresh: forceRefresh,
);

// Update local state with new analysis
if (result['recoveryScore'] != null) {
_recoveryScore = RecoveryScore.fromJson(result['recoveryScore']);
}
if (result['fatiguePattern'] != null) {
_fatiguePattern = FatiguePattern.fromJson(result['fatiguePattern']);
}

notifyListeners();
return result;
} catch (e) {
_errorMessage = e.toString();
notifyListeners();
return null;
}
}
}

5. UI Implementation

// lib/screens/recovery_dashboard_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/recovery_provider.dart';
import '../models/recovery.dart';

class RecoveryDashboardScreen extends StatefulWidget {
@override
_RecoveryDashboardScreenState createState() => _RecoveryDashboardScreenState();
}

class _RecoveryDashboardScreenState extends State<RecoveryDashboardScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadRecoveryData();
});
}

Future<void> _loadRecoveryData() async {
final provider = context.read<RecoveryProvider>();
await Future.wait([
provider.loadCheckIns(),
provider.loadRecoveryScore(),
provider.loadFatiguePattern(),
]);
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Recovery Dashboard'),
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: _loadRecoveryData,
),
],
),
body: Consumer<RecoveryProvider>(
builder: (context, provider, child) {
if (provider.isLoading && provider.checkIns.isEmpty) {
return Center(child: CircularProgressIndicator());
}

if (provider.errorMessage != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 48, color: Colors.red),
SizedBox(height: 16),
Text('Error loading recovery data'),
SizedBox(height: 8),
Text(provider.errorMessage!),
SizedBox(height: 16),
ElevatedButton(
onPressed: _loadRecoveryData,
child: Text('Retry'),
),
],
),
);
}

return RefreshIndicator(
onRefresh: _loadRecoveryData,
child: SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildRecoveryScoreCard(provider.recoveryScore),
SizedBox(height: 16),
_buildFatiguePatternCard(provider.fatiguePattern),
SizedBox(height: 16),
_buildQuickActions(),
SizedBox(height: 16),
_buildRecentCheckIns(provider.checkIns),
],
),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.pushNamed(context, '/create-recovery-checkin'),
child: Icon(Icons.add),
),
);
}

Widget _buildRecoveryScoreCard(RecoveryScore? score) {
if (score == null) {
return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
Icon(Icons.info_outline, size: 48, color: Colors.grey),
SizedBox(height: 8),
Text(
'No Recovery Score Available',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 4),
Text('Complete a few check-ins to see your score'),
],
),
),
);
}

Color scoreColor = _getScoreColor(score.overallScore);

return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Recovery Score',
style: Theme.of(context).textTheme.headlineSmall,
),
CircleAvatar(
radius: 30,
backgroundColor: scoreColor,
child: Text(
'${score.overallScore}',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
],
),
SizedBox(height: 12),
Text(
'Confidence: ${score.confidenceLevel.toUpperCase()}',
style: TextStyle(color: Colors.grey[600]),
),
Text(
'Based on ${score.dataPointsCount} check-ins',
style: TextStyle(color: Colors.grey[600]),
),
SizedBox(height: 16),
Text(
'Dimensions',
style: TextStyle(fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
...score.dimensions.map((dimension) => _buildDimensionRow(dimension)),
],
),
),
);
}

Widget _buildDimensionRow(RecoveryDimension dimension) {
IconData trendIcon;
Color trendColor;

switch (dimension.trend) {
case TrendDirection.improving:
trendIcon = Icons.trending_up;
trendColor = Colors.green;
break;
case TrendDirection.declining:
trendIcon = Icons.trending_down;
trendColor = Colors.red;
break;
case TrendDirection.stable:
trendIcon = Icons.trending_flat;
trendColor = Colors.grey;
break;
}

return Padding(
padding: EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
dimension.name.capitalize(),
style: TextStyle(fontSize: 14),
),
),
Row(
children: [
Text(
'${dimension.score}',
style: TextStyle(fontWeight: FontWeight.bold),
),
SizedBox(width: 8),
Icon(
trendIcon,
size: 16,
color: trendColor,
),
],
),
],
),
);
}

Widget _buildFatiguePatternCard(FatiguePattern? pattern) {
if (pattern == null) {
return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
Icon(Icons.info_outline, size: 48, color: Colors.grey),
SizedBox(height: 8),
Text(
'No Fatigue Pattern Available',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 4),
Text('Complete more check-ins for analysis'),
],
),
),
);
}

Color riskColor = _getRiskColor(pattern.riskLevel);

return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Fatigue Pattern',
style: Theme.of(context).textTheme.headlineSmall,
),
Chip(
label: Text(pattern.riskLevel.toUpperCase()),
backgroundColor: riskColor.withOpacity(0.2),
labelStyle: TextStyle(color: riskColor, fontWeight: FontWeight.bold),
),
],
),
SizedBox(height: 12),
Text(
_formatClassification(pattern.classification),
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
SizedBox(height: 8),
Text('Confidence: ${pattern.confidence}%'),
Text('Duration: ${pattern.durationDays} days'),
if (pattern.keyIndicators.isNotEmpty) ...[
SizedBox(height: 12),
Text(
'Key Indicators:',
style: TextStyle(fontWeight: FontWeight.bold),
),
...pattern.keyIndicators.map((indicator) =>
Padding(
padding: EdgeInsets.only(left: 8, top: 2),
child: Text('• $indicator'),
)
),
],
],
),
),
);
}

Widget _buildQuickActions() {
return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Quick Actions',
style: Theme.of(context).textTheme.titleLarge,
),
SizedBox(height: 12),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () => Navigator.pushNamed(context, '/create-recovery-checkin'),
icon: Icon(Icons.add_circle_outline),
label: Text('New Check-in'),
),
),
SizedBox(width: 12),
Expanded(
child: OutlinedButton.icon(
onPressed: () => _triggerAnalysis(),
icon: Icon(Icons.analytics_outlined),
label: Text('Run Analysis'),
),
),
],
),
],
),
),
);
}

Widget _buildRecentCheckIns(List<WorkoutRecoveryCheckIn> checkIns) {
if (checkIns.isEmpty) {
return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Center(
child: Column(
children: [
Icon(Icons.history, size: 48, color: Colors.grey),
SizedBox(height: 8),
Text('No recovery check-ins yet'),
SizedBox(height: 4),
Text('Start tracking your recovery!'),
],
),
),
),
);
}

return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Recent Check-ins',
style: Theme.of(context).textTheme.titleLarge,
),
SizedBox(height: 12),
...checkIns.take(5).map((checkIn) => _buildCheckInRow(checkIn)),
if (checkIns.length > 5) ...[
SizedBox(height: 8),
TextButton(
onPressed: () => Navigator.pushNamed(context, '/recovery-history'),
child: Text('View All Check-ins'),
),
],
],
),
),
);
}

Widget _buildCheckInRow(WorkoutRecoveryCheckIn checkIn) {
final avgScore = (checkIn.sleepQuality + checkIn.energyLevel + checkIn.mood) / 3;
final readinessIcon = _getReadinessIcon(checkIn.readyToTrainToday);

return Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Icon(readinessIcon.icon, color: readinessIcon.color),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_formatDateTime(checkIn.timestamp),
style: TextStyle(fontWeight: FontWeight.w500),
),
Text(
'Avg: ${avgScore.toStringAsFixed(1)}/10',
style: TextStyle(color: Colors.grey[600]),
),
],
),
),
CircleAvatar(
radius: 16,
backgroundColor: _getScoreColor((avgScore * 10).round()),
child: Text(
'${(avgScore * 10).round()}',
style: TextStyle(color: Colors.white, fontSize: 12),
),
),
],
),
);
}

void _triggerAnalysis() async {
final provider = context.read<RecoveryProvider>();

showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
content: Row(
children: [
CircularProgressIndicator(),
SizedBox(width: 16),
Text('Running analysis...'),
],
),
),
);

final result = await provider.triggerAnalysis(
analysisType: 'MANUAL',
forceRefresh: true,
);

Navigator.pop(context); // Close loading dialog

if (result != null) {
_showAnalysisResults(result);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Analysis failed. Please try again.')),
);
}
}

void _showAnalysisResults(Map<String, dynamic> result) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Analysis Results'),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (result['insights'] != null) ...[
Text(
'Key Insights:',
style: TextStyle(fontWeight: FontWeight.bold),
),
...(result['insights'] as List).map((insight) =>
Padding(
padding: EdgeInsets.only(top: 4, left: 8),
child: Text('• $insight'),
)
),
SizedBox(height: 16),
],
if (result['recommendations'] != null) ...[
Text(
'Recommendations:',
style: TextStyle(fontWeight: FontWeight.bold),
),
...(result['recommendations'] as List).map((rec) =>
Padding(
padding: EdgeInsets.only(top: 4, left: 8),
child: Text('• ${rec['reasoning']}'),
)
),
],
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Close'),
),
],
),
);
}

Color _getScoreColor(int score) {
if (score >= 80) return Colors.green;
if (score >= 60) return Colors.orange;
return Colors.red;
}

Color _getRiskColor(String riskLevel) {
switch (riskLevel) {
case 'low':
return Colors.green;
case 'moderate':
return Colors.orange;
case 'high':
return Colors.red;
case 'critical':
return Colors.red.shade900;
default:
return Colors.grey;
}
}

IconData _getReadinessIcon(RecoveryReadiness readiness) {
switch (readiness) {
case RecoveryReadiness.ready:
return Icons.sentiment_very_satisfied;
case RecoveryReadiness.somewhatReady:
return Icons.sentiment_neutral;
case RecoveryReadiness.notReady:
return Icons.sentiment_very_dissatisfied;
}
}

({IconData icon, Color color}) _getReadinessIcon(RecoveryReadiness readiness) {
switch (readiness) {
case RecoveryReadiness.ready:
return (icon: Icons.sentiment_very_satisfied, color: Colors.green);
case RecoveryReadiness.somewhatReady:
return (icon: Icons.sentiment_neutral, color: Colors.orange);
case RecoveryReadiness.notReady:
return (icon: Icons.sentiment_very_dissatisfied, color: Colors.red);
}
}

String _formatClassification(FatigueClassification classification) {
switch (classification) {
case FatigueClassification.normal:
return 'Normal Recovery';
case FatigueClassification.acuteOverreaching:
return 'Acute Overreaching';
case FatigueClassification.functionalOverreaching:
return 'Functional Overreaching';
case FatigueClassification.nonFunctionalOverreaching:
return 'Non-Functional Overreaching';
case FatigueClassification.overtrainingSyndrome:
return 'Overtraining Syndrome';
}
}

String _formatDateTime(DateTime dateTime) {
return '${dateTime.day}/${dateTime.month} ${dateTime.hour}:${dateTime.minute.toString().padLeft(2, '0')}';
}
}

extension StringExtension on String {
String capitalize() {
return this.isEmpty ? this : this[0].toUpperCase() + this.substring(1);
}
}

Common Use Cases

1. Daily Recovery Check-In

// Simple daily wellness check
await recoveryProvider.createCheckIn(
timestamp: DateTime.now(),
sleepQuality: 8,
nutritionQuality: 7,
energyLevel: 6,
readinessSleepQuality: 7,
mood: 8,
stressLevel: 4,
readyToTrainToday: RecoveryReadiness.ready,
notes: 'Feeling great today!',
sorenessEntries: [],
);

2. Post-Workout Recovery Assessment

// Recovery check-in after workout
await recoveryProvider.createCheckIn(
workoutHistoryEntryId: workout.id,
timestamp: DateTime.now(),
sleepQuality: 7,
nutritionQuality: 8,
energyLevel: 5, // Tired after workout
readinessSleepQuality: 6,
mood: 9, // Feel accomplished
stressLevel: 3,
readyToTrainToday: RecoveryReadiness.somewhatReady,
sorenessEntries: [
{'muscleGroupId': 'chest_id', 'sorenessLevel': 6},
{'muscleGroupId': 'triceps_id', 'sorenessLevel': 4},
],
);
// Track recovery trends over time
await recoveryProvider.loadRecoveryScore(timeframe: 'WEEK');

final score = recoveryProvider.recoveryScore;
if (score != null && score.overallScore < 60) {
// Show recommendation to reduce training intensity
showRecoveryWarning();
}

4. Coach Dashboard Analytics

// Coach viewing multiple clients
final coachAnalytics = await recoveryService.getCoachRecoveryAnalytics(
filters: {
'alertLevel': 'WARNING',
'sortBy': 'recoveryScore',
'sortOrder': 'asc',
}
);

// Show clients needing attention first
displayClientsNeedingAttention(coachAnalytics.clients);

Error Handling

Common Error Types

Insufficient Data Errors

// Handle cases where user hasn't logged enough data
try {
final score = await recoveryService.getRecoveryScore();
} catch (e) {
if (e.toString().contains('INSUFFICIENT_DATA')) {
showInsufficientDataDialog();
}
}

Invalid Recovery Data

// Handle validation errors
try {
await recoveryService.createRecoveryCheckIn(
sleepQuality: 15, // Invalid: should be 0-10
// ...
);
} catch (e) {
if (e.toString().contains('BAD_USER_INPUT')) {
showValidationError('Please enter values between 0 and 10');
}
}

Error Recovery Strategies

Data Validation

bool _validateCheckInData({
required int sleepQuality,
required int nutritionQuality,
required int energyLevel,
required int mood,
required int stressLevel,
}) {
final values = [sleepQuality, nutritionQuality, energyLevel, mood, stressLevel];
return values.every((value) => value >= 0 && value <= 10);
}

Graceful Degradation

Future<void> _loadRecoveryDataWithFallback() async {
try {
await recoveryProvider.loadRecoveryScore();
} catch (e) {
// If weekly analysis fails, try daily
try {
await recoveryProvider.loadRecoveryScore(timeframe: 'DAY');
} catch (fallbackError) {
// Show message about needing more data
showInsufficientDataMessage();
}
}
}

Best Practices

1. Recovery Check-In Timing

  • Consistent timing: Encourage users to check in at the same time daily
  • Post-workout prompts: Trigger check-ins after workout completion
  • Morning assessments: Capture overnight recovery status

2. Data Quality

  • Input validation: Ensure all ratings are within valid ranges (0-10)
  • Required fields: Make essential metrics mandatory
  • Optional context: Allow notes for additional context

3. User Experience

  • Quick entry: Minimize friction for daily check-ins
  • Visual feedback: Show trends and patterns clearly
  • Actionable insights: Provide specific, implementable recommendations

4. Analytics Integration

  • Trend visualization: Show recovery patterns over time
  • Alert systems: Notify users and coaches of concerning patterns
  • Program integration: Connect recovery data to training modifications

5. Privacy & Data Security

  • User consent: Clear communication about data use
  • Coach permissions: Proper authorization for coach access
  • Data retention: Appropriate policies for historical data

The Recovery and Recovery Analysis APIs provide a comprehensive foundation for intelligent recovery management in fitness applications. By combining simple user input with sophisticated analysis, they enable data-driven decisions that optimize training outcomes and prevent overreaching.