mirror of
https://github.com/sstent/AICyclingCoach.git
synced 2025-12-05 23:52:06 +00:00
205 lines
7.6 KiB
Python
205 lines
7.6 KiB
Python
"""
|
|
Workout service for TUI application.
|
|
Manages workout data, analysis, and Garmin sync without HTTP dependencies.
|
|
"""
|
|
from typing import Dict, List, Optional
|
|
from sqlalchemy import select, desc
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from backend.app.models.workout import Workout
|
|
from backend.app.models.analysis import Analysis
|
|
from backend.app.models.garmin_sync_log import GarminSyncLog
|
|
from backend.app.services.workout_sync import WorkoutSyncService
|
|
from backend.app.services.ai_service import AIService
|
|
|
|
|
|
class WorkoutService:
|
|
"""Service for workout operations."""
|
|
|
|
def __init__(self, db: AsyncSession):
|
|
self.db = db
|
|
|
|
async def get_workouts(self, limit: Optional[int] = None) -> List[Dict]:
|
|
"""Get all workouts."""
|
|
try:
|
|
query = select(Workout).order_by(desc(Workout.start_time))
|
|
if limit:
|
|
query = query.limit(limit)
|
|
|
|
result = await self.db.execute(query)
|
|
workouts = result.scalars().all()
|
|
|
|
return [
|
|
{
|
|
"id": w.id,
|
|
"garmin_activity_id": w.garmin_activity_id,
|
|
"activity_type": w.activity_type,
|
|
"start_time": w.start_time.isoformat() if w.start_time else None,
|
|
"duration_seconds": w.duration_seconds,
|
|
"distance_m": w.distance_m,
|
|
"avg_hr": w.avg_hr,
|
|
"max_hr": w.max_hr,
|
|
"avg_power": w.avg_power,
|
|
"max_power": w.max_power,
|
|
"avg_cadence": w.avg_cadence,
|
|
"elevation_gain_m": w.elevation_gain_m
|
|
} for w in workouts
|
|
]
|
|
|
|
except Exception as e:
|
|
# Log error properly
|
|
import logging
|
|
logging.error(f"Error fetching workouts: {str(e)}")
|
|
return []
|
|
|
|
async def get_workout(self, workout_id: int) -> Optional[Dict]:
|
|
"""Get a specific workout by ID."""
|
|
try:
|
|
workout = await self.db.get(Workout, workout_id)
|
|
if not workout:
|
|
return None
|
|
|
|
return {
|
|
"id": workout.id,
|
|
"garmin_activity_id": workout.garmin_activity_id,
|
|
"activity_type": workout.activity_type,
|
|
"start_time": workout.start_time.isoformat() if workout.start_time else None,
|
|
"duration_seconds": workout.duration_seconds,
|
|
"distance_m": workout.distance_m,
|
|
"avg_hr": workout.avg_hr,
|
|
"max_hr": workout.max_hr,
|
|
"avg_power": workout.avg_power,
|
|
"max_power": workout.max_power,
|
|
"avg_cadence": workout.avg_cadence,
|
|
"elevation_gain_m": workout.elevation_gain_m,
|
|
"metrics": workout.metrics
|
|
}
|
|
|
|
except Exception as e:
|
|
raise Exception(f"Error fetching workout {workout_id}: {str(e)}")
|
|
|
|
async def get_workout_metrics(self, workout_id: int) -> List[Dict]:
|
|
"""Get time-series metrics for a workout."""
|
|
try:
|
|
workout = await self.db.get(Workout, workout_id)
|
|
if not workout or not workout.metrics:
|
|
return []
|
|
|
|
return workout.metrics
|
|
|
|
except Exception as e:
|
|
raise Exception(f"Error fetching workout metrics: {str(e)}")
|
|
|
|
async def sync_garmin_activities(self, days_back: int = 14) -> Dict:
|
|
"""Trigger Garmin sync in background."""
|
|
try:
|
|
sync_service = WorkoutSyncService(self.db)
|
|
result = await sync_service.sync_recent_activities(days_back=days_back)
|
|
|
|
return {
|
|
"message": "Garmin sync completed",
|
|
"activities_synced": result.get("activities_synced", 0),
|
|
"status": "success"
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
"message": f"Garmin sync failed: {str(e)}",
|
|
"activities_synced": 0,
|
|
"status": "error"
|
|
}
|
|
|
|
async def get_sync_status(self) -> Dict:
|
|
"""Get the latest sync status."""
|
|
try:
|
|
result = await self.db.execute(
|
|
select(GarminSyncLog).order_by(desc(GarminSyncLog.created_at)).limit(1)
|
|
)
|
|
sync_log = result.scalar_one_or_none()
|
|
|
|
if not sync_log:
|
|
return {"status": "never_synced"}
|
|
|
|
return {
|
|
"status": sync_log.status,
|
|
"last_sync_time": sync_log.last_sync_time.isoformat() if sync_log.last_sync_time else None,
|
|
"activities_synced": sync_log.activities_synced,
|
|
"error_message": sync_log.error_message
|
|
}
|
|
|
|
except Exception as e:
|
|
raise Exception(f"Error fetching sync status: {str(e)}")
|
|
|
|
async def analyze_workout(self, workout_id: int) -> Dict:
|
|
"""Trigger AI analysis of a specific workout."""
|
|
try:
|
|
workout = await self.db.get(Workout, workout_id)
|
|
if not workout:
|
|
raise Exception("Workout not found")
|
|
|
|
ai_service = AIService(self.db)
|
|
analysis_result = await ai_service.analyze_workout(workout, None)
|
|
|
|
# Store analysis
|
|
analysis = Analysis(
|
|
workout_id=workout.id,
|
|
jsonb_feedback=analysis_result.get("feedback", {}),
|
|
suggestions=analysis_result.get("suggestions", {})
|
|
)
|
|
self.db.add(analysis)
|
|
await self.db.commit()
|
|
|
|
return {
|
|
"message": "Analysis completed",
|
|
"workout_id": workout_id,
|
|
"analysis_id": analysis.id,
|
|
"feedback": analysis_result.get("feedback", {}),
|
|
"suggestions": analysis_result.get("suggestions", {})
|
|
}
|
|
|
|
except Exception as e:
|
|
raise Exception(f"Error analyzing workout: {str(e)}")
|
|
|
|
async def get_workout_analyses(self, workout_id: int) -> List[Dict]:
|
|
"""Get all analyses for a specific workout."""
|
|
try:
|
|
workout = await self.db.get(Workout, workout_id)
|
|
if not workout:
|
|
raise Exception("Workout not found")
|
|
|
|
result = await self.db.execute(
|
|
select(Analysis).where(Analysis.workout_id == workout_id)
|
|
)
|
|
analyses = result.scalars().all()
|
|
|
|
return [
|
|
{
|
|
"id": a.id,
|
|
"analysis_type": a.analysis_type,
|
|
"feedback": a.jsonb_feedback,
|
|
"suggestions": a.suggestions,
|
|
"approved": a.approved,
|
|
"created_at": a.created_at.isoformat() if a.created_at else None
|
|
} for a in analyses
|
|
]
|
|
|
|
except Exception as e:
|
|
raise Exception(f"Error fetching workout analyses: {str(e)}")
|
|
|
|
async def approve_analysis(self, analysis_id: int) -> Dict:
|
|
"""Approve analysis suggestions."""
|
|
try:
|
|
analysis = await self.db.get(Analysis, analysis_id)
|
|
if not analysis:
|
|
raise Exception("Analysis not found")
|
|
|
|
analysis.approved = True
|
|
await self.db.commit()
|
|
|
|
return {
|
|
"message": "Analysis approved",
|
|
"analysis_id": analysis_id
|
|
}
|
|
|
|
except Exception as e:
|
|
raise Exception(f"Error approving analysis: {str(e)}") |