change to TUI

This commit is contained in:
2025-09-12 09:08:10 -07:00
parent 7c7dcb5b10
commit e0e70f6508
165 changed files with 3438 additions and 16154 deletions

18
tui/services/__init__.py Normal file
View File

@@ -0,0 +1,18 @@
"""
Service layer for TUI application.
Contains business logic extracted from FastAPI routes for direct method calls.
"""
from .dashboard_service import DashboardService
from .plan_service import PlanService
from .workout_service import WorkoutService
from .rule_service import RuleService
from .route_service import RouteService
__all__ = [
'DashboardService',
'PlanService',
'WorkoutService',
'RuleService',
'RouteService'
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,110 @@
"""
Dashboard service for TUI application.
Provides dashboard data without HTTP dependencies.
"""
from datetime import datetime, timedelta
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.plan import Plan
from backend.app.models.garmin_sync_log import GarminSyncLog
class DashboardService:
"""Service for dashboard data operations."""
def __init__(self, db: AsyncSession):
self.db = db
async def get_dashboard_data(self) -> Dict:
"""Get consolidated dashboard data."""
try:
# Recent workouts (last 7 days)
workout_result = await self.db.execute(
select(Workout)
.where(Workout.start_time >= datetime.now() - timedelta(days=7))
.order_by(desc(Workout.start_time))
.limit(5)
)
recent_workouts = workout_result.scalars().all()
# Current active plan - Modified since Plan model doesn't have 'active' field
plan_result = await self.db.execute(
select(Plan)
.order_by(desc(Plan.created_at))
.limit(1)
)
current_plan = plan_result.scalar_one_or_none()
# Sync status
sync_result = await self.db.execute(
select(GarminSyncLog)
.order_by(desc(GarminSyncLog.created_at))
.limit(1)
)
last_sync = sync_result.scalar_one_or_none()
# Calculate metrics
total_duration = sum(w.duration_seconds or 0 for w in recent_workouts)
weekly_volume = total_duration / 3600 if total_duration else 0
return {
"recent_workouts": [
{
"id": w.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,
"avg_power": w.avg_power
} for w in recent_workouts
],
"current_plan": {
"id": current_plan.id,
"version": current_plan.version,
"created_at": current_plan.created_at.isoformat() if current_plan.created_at else None
} if current_plan else None,
"last_sync": {
"last_sync_time": last_sync.last_sync_time.isoformat() if last_sync and last_sync.last_sync_time else None,
"activities_synced": last_sync.activities_synced if last_sync else 0,
"status": last_sync.status if last_sync else "never_synced",
"error_message": last_sync.error_message if last_sync else None
} if last_sync else None,
"metrics": {
"weekly_volume": weekly_volume,
"workout_count": len(recent_workouts),
"plan_progress": 0 # TODO: Calculate actual progress
}
}
except Exception as e:
raise Exception(f"Dashboard data error: {str(e)}")
async def get_weekly_stats(self) -> Dict:
"""Get weekly workout statistics."""
try:
week_start = datetime.now() - timedelta(days=7)
workout_result = await self.db.execute(
select(Workout)
.where(Workout.start_time >= week_start)
)
workouts = workout_result.scalars().all()
total_distance = sum(w.distance_m or 0 for w in workouts) / 1000 # Convert to km
total_time = sum(w.duration_seconds or 0 for w in workouts) / 3600 # Convert to hours
return {
"workout_count": len(workouts),
"total_distance_km": round(total_distance, 1),
"total_time_hours": round(total_time, 1),
"avg_distance_km": round(total_distance / len(workouts), 1) if workouts else 0,
"avg_duration_hours": round(total_time / len(workouts), 1) if workouts else 0
}
except Exception as e:
raise Exception(f"Weekly stats error: {str(e)}")

View File

@@ -0,0 +1,199 @@
"""
Plan service for TUI application.
Manages training plans and plan generation without HTTP dependencies.
"""
from typing import Dict, List, Optional
from sqlalchemy import select, desc
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.models.plan import Plan
from backend.app.models.rule import Rule
from backend.app.services.ai_service import AIService
from backend.app.services.plan_evolution import PlanEvolutionService
class PlanService:
"""Service for training plan operations."""
def __init__(self, db: AsyncSession):
self.db = db
async def get_plans(self) -> List[Dict]:
"""Get all training plans."""
try:
result = await self.db.execute(
select(Plan).order_by(desc(Plan.created_at))
)
plans = result.scalars().all()
return [
{
"id": p.id,
"version": p.version,
"parent_plan_id": p.parent_plan_id,
"created_at": p.created_at.isoformat() if p.created_at else None,
"plan_data": p.jsonb_plan
} for p in plans
]
except Exception as e:
raise Exception(f"Error fetching plans: {str(e)}")
async def get_plan(self, plan_id: int) -> Optional[Dict]:
"""Get a specific plan by ID."""
try:
plan = await self.db.get(Plan, plan_id)
if not plan:
return None
return {
"id": plan.id,
"version": plan.version,
"parent_plan_id": plan.parent_plan_id,
"created_at": plan.created_at.isoformat() if plan.created_at else None,
"plan_data": plan.jsonb_plan
}
except Exception as e:
raise Exception(f"Error fetching plan {plan_id}: {str(e)}")
async def create_plan(self, plan_data: Dict, version: int = 1, parent_plan_id: Optional[int] = None) -> Dict:
"""Create a new training plan."""
try:
db_plan = Plan(
jsonb_plan=plan_data,
version=version,
parent_plan_id=parent_plan_id
)
self.db.add(db_plan)
await self.db.commit()
await self.db.refresh(db_plan)
return {
"id": db_plan.id,
"version": db_plan.version,
"parent_plan_id": db_plan.parent_plan_id,
"created_at": db_plan.created_at.isoformat() if db_plan.created_at else None,
"plan_data": db_plan.jsonb_plan
}
except Exception as e:
raise Exception(f"Error creating plan: {str(e)}")
async def update_plan(self, plan_id: int, plan_data: Dict, version: Optional[int] = None) -> Dict:
"""Update an existing plan."""
try:
db_plan = await self.db.get(Plan, plan_id)
if not db_plan:
raise Exception("Plan not found")
db_plan.jsonb_plan = plan_data
if version is not None:
db_plan.version = version
await self.db.commit()
await self.db.refresh(db_plan)
return {
"id": db_plan.id,
"version": db_plan.version,
"parent_plan_id": db_plan.parent_plan_id,
"created_at": db_plan.created_at.isoformat() if db_plan.created_at else None,
"plan_data": db_plan.jsonb_plan
}
except Exception as e:
raise Exception(f"Error updating plan: {str(e)}")
async def delete_plan(self, plan_id: int) -> Dict:
"""Delete a plan."""
try:
plan = await self.db.get(Plan, plan_id)
if not plan:
raise Exception("Plan not found")
await self.db.delete(plan)
await self.db.commit()
return {"message": "Plan deleted successfully"}
except Exception as e:
raise Exception(f"Error deleting plan: {str(e)}")
async def generate_plan(self, rule_ids: List[int], goals: Dict, preferred_routes: Optional[List[str]] = None) -> Dict:
"""Generate a new training plan using AI."""
try:
# Get all rules from the provided rule IDs
rules = []
for rule_id in rule_ids:
rule = await self.db.get(Rule, rule_id)
if not rule:
raise Exception(f"Rule with ID {rule_id} not found")
rules.append(rule.rule_text)
# Generate plan using AI service
ai_service = AIService(self.db)
generated_plan = await ai_service.generate_training_plan(
rule_set=rules,
goals=goals,
preferred_routes=preferred_routes or []
)
# Create and store the plan
plan_dict = await self.create_plan(generated_plan, version=1)
return {
"plan": plan_dict,
"generation_metadata": {
"status": "success",
"rule_ids": rule_ids,
"goals": goals
}
}
except Exception as e:
raise Exception(f"Error generating plan: {str(e)}")
async def get_plan_evolution_history(self, plan_id: int) -> List[Dict]:
"""Get full evolution history for a plan."""
try:
evolution_service = PlanEvolutionService(self.db)
plans = await evolution_service.get_plan_evolution_history(plan_id)
if not plans:
raise Exception("Plan not found")
return [
{
"id": p.id,
"version": p.version,
"parent_plan_id": p.parent_plan_id,
"created_at": p.created_at.isoformat() if p.created_at else None,
"plan_data": p.jsonb_plan
} for p in plans
]
except Exception as e:
raise Exception(f"Error fetching plan evolution: {str(e)}")
async def get_current_plan(self) -> Optional[Dict]:
"""Get the most recent active plan."""
try:
result = await self.db.execute(
select(Plan).order_by(desc(Plan.created_at)).limit(1)
)
plan = result.scalar_one_or_none()
if not plan:
return None
return {
"id": plan.id,
"version": plan.version,
"parent_plan_id": plan.parent_plan_id,
"created_at": plan.created_at.isoformat() if plan.created_at else None,
"plan_data": plan.jsonb_plan
}
except Exception as e:
raise Exception(f"Error fetching current plan: {str(e)}")

View File

@@ -0,0 +1,215 @@
"""
Route service for TUI application.
Manages GPX routes and route visualization without HTTP dependencies.
"""
import gpxpy
from pathlib import Path
from typing import Dict, List, Optional
from sqlalchemy import select, desc
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.models.route import Route
from backend.app.models.section import Section
class RouteService:
"""Service for route and GPX operations."""
def __init__(self, db: AsyncSession):
self.db = db
async def get_routes(self) -> List[Dict]:
"""Get all routes."""
try:
result = await self.db.execute(
select(Route).order_by(desc(Route.created_at))
)
routes = result.scalars().all()
return [
{
"id": r.id,
"name": r.name,
"description": r.description,
"total_distance": r.total_distance,
"elevation_gain": r.elevation_gain,
"gpx_file_path": r.gpx_file_path,
"created_at": r.created_at.isoformat() if r.created_at else None
} for r in routes
]
except Exception as e:
raise Exception(f"Error fetching routes: {str(e)}")
async def get_route(self, route_id: int) -> Optional[Dict]:
"""Get a specific route by ID."""
try:
route = await self.db.get(Route, route_id)
if not route:
return None
return {
"id": route.id,
"name": route.name,
"description": route.description,
"total_distance": route.total_distance,
"elevation_gain": route.elevation_gain,
"gpx_file_path": route.gpx_file_path,
"created_at": route.created_at.isoformat() if route.created_at else None
}
except Exception as e:
raise Exception(f"Error fetching route {route_id}: {str(e)}")
async def load_gpx_file(self, file_path: str) -> Dict:
"""Load and parse GPX file."""
try:
gpx_path = Path(file_path)
if not gpx_path.exists():
raise Exception(f"GPX file not found: {file_path}")
with open(gpx_path, 'r', encoding='utf-8') as gpx_file:
gpx = gpxpy.parse(gpx_file)
# Extract track points
track_points = []
total_distance = 0
min_elevation = float('inf')
max_elevation = float('-inf')
for track in gpx.tracks:
for segment in track.segments:
for point in segment.points:
track_points.append({
"lat": point.latitude,
"lon": point.longitude,
"ele": point.elevation if point.elevation else 0,
"time": point.time.isoformat() if point.time else None
})
if point.elevation:
min_elevation = min(min_elevation, point.elevation)
max_elevation = max(max_elevation, point.elevation)
# Calculate total distance and elevation gain
if track_points:
total_distance = self._calculate_total_distance(track_points)
elevation_gain = max_elevation - min_elevation if max_elevation != float('-inf') else 0
return {
"name": gpx_path.stem,
"total_distance": total_distance,
"elevation_gain": elevation_gain,
"track_points": track_points,
"gpx_file_path": file_path
}
except Exception as e:
raise Exception(f"Error loading GPX file {file_path}: {str(e)}")
def _calculate_total_distance(self, track_points: List[Dict]) -> float:
"""Calculate total distance from track points."""
if len(track_points) < 2:
return 0
total_distance = 0
for i in range(1, len(track_points)):
prev_point = track_points[i-1]
curr_point = track_points[i]
# Simple haversine distance calculation
distance = self._haversine_distance(
prev_point["lat"], prev_point["lon"],
curr_point["lat"], curr_point["lon"]
)
total_distance += distance
return total_distance
def _haversine_distance(self, lat1: float, lon1: float, lat2: float, lon2: float) -> float:
"""Calculate distance between two points using haversine formula."""
import math
R = 6371000 # Earth's radius in meters
lat1_rad = math.radians(lat1)
lat2_rad = math.radians(lat2)
delta_lat = math.radians(lat2 - lat1)
delta_lon = math.radians(lon2 - lon1)
a = (math.sin(delta_lat / 2) ** 2 +
math.cos(lat1_rad) * math.cos(lat2_rad) *
math.sin(delta_lon / 2) ** 2)
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
return R * c
async def create_route(self, name: str, description: str, gpx_file_path: str) -> Dict:
"""Create a new route from GPX data."""
try:
# Load GPX data
gpx_data = await self.load_gpx_file(gpx_file_path)
# Create route record
db_route = Route(
name=name,
description=description,
total_distance=gpx_data["total_distance"],
elevation_gain=gpx_data["elevation_gain"],
gpx_file_path=gpx_file_path
)
self.db.add(db_route)
await self.db.commit()
await self.db.refresh(db_route)
return {
"id": db_route.id,
"name": db_route.name,
"description": db_route.description,
"total_distance": db_route.total_distance,
"elevation_gain": db_route.elevation_gain,
"gpx_file_path": db_route.gpx_file_path,
"created_at": db_route.created_at.isoformat() if db_route.created_at else None
}
except Exception as e:
raise Exception(f"Error creating route: {str(e)}")
async def delete_route(self, route_id: int) -> Dict:
"""Delete a route."""
try:
route = await self.db.get(Route, route_id)
if not route:
raise Exception("Route not found")
await self.db.delete(route)
await self.db.commit()
return {"message": "Route deleted successfully"}
except Exception as e:
raise Exception(f"Error deleting route: {str(e)}")
async def get_route_sections(self, route_id: int) -> List[Dict]:
"""Get sections for a specific route."""
try:
result = await self.db.execute(
select(Section).where(Section.route_id == route_id)
)
sections = result.scalars().all()
return [
{
"id": s.id,
"route_id": s.route_id,
"gpx_file_path": s.gpx_file_path,
"distance_m": s.distance_m,
"grade_avg": s.grade_avg,
"min_gear": s.min_gear,
"est_time_minutes": s.est_time_minutes
} for s in sections
]
except Exception as e:
raise Exception(f"Error fetching route sections: {str(e)}")

View File

@@ -0,0 +1,244 @@
"""
Rule service for TUI application.
Manages training rules without HTTP dependencies.
"""
from typing import Dict, List, Optional
from sqlalchemy import select, desc
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.models.rule import Rule
class RuleService:
"""Service for training rule operations."""
def __init__(self, db: AsyncSession):
self.db = db
async def get_rules(self) -> List[Dict]:
"""Get all training rules."""
try:
result = await self.db.execute(
select(Rule).order_by(desc(Rule.created_at))
)
rules = result.scalars().all()
return [
{
"id": r.id,
"name": r.name,
"description": r.description,
"user_defined": r.user_defined,
"rule_text": r.rule_text,
"version": r.version,
"parent_rule_id": r.parent_rule_id,
"created_at": r.created_at.isoformat() if r.created_at else None
} for r in rules
]
except Exception as e:
raise Exception(f"Error fetching rules: {str(e)}")
async def get_rule(self, rule_id: int) -> Optional[Dict]:
"""Get a specific rule by ID."""
try:
rule = await self.db.get(Rule, rule_id)
if not rule:
return None
return {
"id": rule.id,
"name": rule.name,
"description": rule.description,
"user_defined": rule.user_defined,
"rule_text": rule.rule_text,
"version": rule.version,
"parent_rule_id": rule.parent_rule_id,
"created_at": rule.created_at.isoformat() if rule.created_at else None
}
except Exception as e:
raise Exception(f"Error fetching rule {rule_id}: {str(e)}")
async def create_rule(
self,
name: str,
rule_text: str,
description: Optional[str] = None,
user_defined: bool = True,
version: int = 1,
parent_rule_id: Optional[int] = None
) -> Dict:
"""Create a new training rule."""
try:
db_rule = Rule(
name=name,
description=description,
user_defined=user_defined,
rule_text=rule_text,
version=version,
parent_rule_id=parent_rule_id
)
self.db.add(db_rule)
await self.db.commit()
await self.db.refresh(db_rule)
return {
"id": db_rule.id,
"name": db_rule.name,
"description": db_rule.description,
"user_defined": db_rule.user_defined,
"rule_text": db_rule.rule_text,
"version": db_rule.version,
"parent_rule_id": db_rule.parent_rule_id,
"created_at": db_rule.created_at.isoformat() if db_rule.created_at else None
}
except Exception as e:
raise Exception(f"Error creating rule: {str(e)}")
async def update_rule(
self,
rule_id: int,
name: Optional[str] = None,
rule_text: Optional[str] = None,
description: Optional[str] = None,
version: Optional[int] = None
) -> Dict:
"""Update an existing rule."""
try:
db_rule = await self.db.get(Rule, rule_id)
if not db_rule:
raise Exception("Rule not found")
if name is not None:
db_rule.name = name
if rule_text is not None:
db_rule.rule_text = rule_text
if description is not None:
db_rule.description = description
if version is not None:
db_rule.version = version
await self.db.commit()
await self.db.refresh(db_rule)
return {
"id": db_rule.id,
"name": db_rule.name,
"description": db_rule.description,
"user_defined": db_rule.user_defined,
"rule_text": db_rule.rule_text,
"version": db_rule.version,
"parent_rule_id": db_rule.parent_rule_id,
"created_at": db_rule.created_at.isoformat() if db_rule.created_at else None
}
except Exception as e:
raise Exception(f"Error updating rule: {str(e)}")
async def delete_rule(self, rule_id: int) -> Dict:
"""Delete a rule."""
try:
rule = await self.db.get(Rule, rule_id)
if not rule:
raise Exception("Rule not found")
await self.db.delete(rule)
await self.db.commit()
return {"message": "Rule deleted successfully"}
except Exception as e:
raise Exception(f"Error deleting rule: {str(e)}")
async def get_user_defined_rules(self) -> List[Dict]:
"""Get only user-defined rules."""
try:
result = await self.db.execute(
select(Rule)
.where(Rule.user_defined == True)
.order_by(desc(Rule.created_at))
)
rules = result.scalars().all()
return [
{
"id": r.id,
"name": r.name,
"description": r.description,
"rule_text": r.rule_text,
"version": r.version,
"created_at": r.created_at.isoformat() if r.created_at else None
} for r in rules
]
except Exception as e:
raise Exception(f"Error fetching user-defined rules: {str(e)}")
async def get_system_rules(self) -> List[Dict]:
"""Get only system-defined rules."""
try:
result = await self.db.execute(
select(Rule)
.where(Rule.user_defined == False)
.order_by(desc(Rule.created_at))
)
rules = result.scalars().all()
return [
{
"id": r.id,
"name": r.name,
"description": r.description,
"rule_text": r.rule_text,
"version": r.version,
"created_at": r.created_at.isoformat() if r.created_at else None
} for r in rules
]
except Exception as e:
raise Exception(f"Error fetching system rules: {str(e)}")
async def create_rule_version(self, parent_rule_id: int, rule_text: str, description: Optional[str] = None) -> Dict:
"""Create a new version of an existing rule."""
try:
parent_rule = await self.db.get(Rule, parent_rule_id)
if not parent_rule:
raise Exception("Parent rule not found")
# Get the highest version number for this rule family
result = await self.db.execute(
select(Rule.version)
.where(Rule.parent_rule_id == parent_rule_id)
.order_by(desc(Rule.version))
.limit(1)
)
max_version = result.scalar_one_or_none() or parent_rule.version
new_version = max_version + 1
new_rule = Rule(
name=parent_rule.name,
description=description or parent_rule.description,
user_defined=parent_rule.user_defined,
rule_text=rule_text,
version=new_version,
parent_rule_id=parent_rule_id
)
self.db.add(new_rule)
await self.db.commit()
await self.db.refresh(new_rule)
return {
"id": new_rule.id,
"name": new_rule.name,
"description": new_rule.description,
"user_defined": new_rule.user_defined,
"rule_text": new_rule.rule_text,
"version": new_rule.version,
"parent_rule_id": new_rule.parent_rule_id,
"created_at": new_rule.created_at.isoformat() if new_rule.created_at else None
}
except Exception as e:
raise Exception(f"Error creating rule version: {str(e)}")

View File

@@ -0,0 +1,202 @@
"""
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:
raise Exception(f"Error fetching workouts: {str(e)}")
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)}")