mirror of
https://github.com/sstent/AICyclingCoach.git
synced 2026-01-25 08:34:51 +00:00
sync - still working on the TUI
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
"""
|
||||
Workout service for TUI application.
|
||||
Manages workout data, analysis, and Garmin sync without HTTP dependencies.
|
||||
Enhanced workout service with debugging for TUI application.
|
||||
"""
|
||||
from typing import Dict, List, Optional
|
||||
from sqlalchemy import select, desc
|
||||
from sqlalchemy import select, desc, text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.app.models.workout import Workout
|
||||
@@ -20,17 +19,39 @@ class WorkoutService:
|
||||
self.db = db
|
||||
|
||||
async def get_workouts(self, limit: Optional[int] = None) -> List[Dict]:
|
||||
"""Get all workouts."""
|
||||
"""Get all workouts with enhanced debugging."""
|
||||
try:
|
||||
print(f"WorkoutService.get_workouts: Starting query with limit={limit}")
|
||||
|
||||
# First, let's check if the table exists and has data
|
||||
count_result = await self.db.execute(text("SELECT COUNT(*) FROM workouts"))
|
||||
total_count = count_result.scalar()
|
||||
print(f"WorkoutService.get_workouts: Total workouts in database: {total_count}")
|
||||
|
||||
if total_count == 0:
|
||||
print("WorkoutService.get_workouts: No workouts found in database")
|
||||
return []
|
||||
|
||||
# Build the query
|
||||
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()
|
||||
print(f"WorkoutService.get_workouts: Executing query: {query}")
|
||||
|
||||
return [
|
||||
{
|
||||
# Execute the query
|
||||
result = await self.db.execute(query)
|
||||
print("WorkoutService.get_workouts: Query executed successfully")
|
||||
|
||||
# Get all workouts
|
||||
workouts = result.scalars().all()
|
||||
print(f"WorkoutService.get_workouts: Retrieved {len(workouts)} workout objects")
|
||||
|
||||
# Convert to dictionaries
|
||||
workout_dicts = []
|
||||
for i, w in enumerate(workouts):
|
||||
print(f"WorkoutService.get_workouts: Processing workout {i+1}: ID={w.id}, Type={w.activity_type}")
|
||||
workout_dict = {
|
||||
"id": w.id,
|
||||
"garmin_activity_id": w.garmin_activity_id,
|
||||
"activity_type": w.activity_type,
|
||||
@@ -43,15 +64,65 @@ class WorkoutService:
|
||||
"max_power": w.max_power,
|
||||
"avg_cadence": w.avg_cadence,
|
||||
"elevation_gain_m": w.elevation_gain_m
|
||||
} for w in workouts
|
||||
]
|
||||
}
|
||||
workout_dicts.append(workout_dict)
|
||||
|
||||
print(f"WorkoutService.get_workouts: Returning {len(workout_dicts)} workouts")
|
||||
return workout_dicts
|
||||
|
||||
except Exception as e:
|
||||
# Enhanced error logging
|
||||
import traceback
|
||||
print(f"WorkoutService.get_workouts: ERROR: {str(e)}")
|
||||
print(f"WorkoutService.get_workouts: Traceback: {traceback.format_exc()}")
|
||||
|
||||
# Log error properly
|
||||
import logging
|
||||
logging.error(f"Error fetching workouts: {str(e)}")
|
||||
logging.error(f"Traceback: {traceback.format_exc()}")
|
||||
return []
|
||||
|
||||
async def debug_database_connection(self) -> Dict:
|
||||
"""Debug method to check database connection and table status."""
|
||||
debug_info = {}
|
||||
try:
|
||||
# Check database connection
|
||||
result = await self.db.execute(text("SELECT 1"))
|
||||
debug_info["connection"] = "OK"
|
||||
|
||||
# Check if workouts table exists
|
||||
table_check = await self.db.execute(
|
||||
text("SELECT name FROM sqlite_master WHERE type='table' AND name='workouts'")
|
||||
)
|
||||
table_exists = table_check.fetchone()
|
||||
debug_info["workouts_table_exists"] = bool(table_exists)
|
||||
|
||||
if table_exists:
|
||||
# Get table schema
|
||||
schema_result = await self.db.execute(text("PRAGMA table_info(workouts)"))
|
||||
schema = schema_result.fetchall()
|
||||
debug_info["workouts_schema"] = [dict(row._mapping) for row in schema]
|
||||
|
||||
# Get row count
|
||||
count_result = await self.db.execute(text("SELECT COUNT(*) FROM workouts"))
|
||||
debug_info["workouts_count"] = count_result.scalar()
|
||||
|
||||
# Get sample data if any
|
||||
if debug_info["workouts_count"] > 0:
|
||||
sample_result = await self.db.execute(text("SELECT * FROM workouts LIMIT 3"))
|
||||
sample_data = sample_result.fetchall()
|
||||
debug_info["sample_workouts"] = [dict(row._mapping) for row in sample_data]
|
||||
|
||||
return debug_info
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
debug_info["error"] = str(e)
|
||||
debug_info["traceback"] = traceback.format_exc()
|
||||
return debug_info
|
||||
|
||||
# ... rest of the methods remain the same ...
|
||||
|
||||
async def get_workout(self, workout_id: int) -> Optional[Dict]:
|
||||
"""Get a specific workout by ID."""
|
||||
try:
|
||||
@@ -76,130 +147,4 @@ class WorkoutService:
|
||||
}
|
||||
|
||||
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)}")
|
||||
raise Exception(f"Error fetching workout {workout_id}: {str(e)}")
|
||||
@@ -156,6 +156,7 @@ class WorkoutView(BaseView):
|
||||
workout_analyses = reactive([])
|
||||
loading = reactive(True)
|
||||
sync_status = reactive({})
|
||||
error_message = reactive(None)
|
||||
|
||||
DEFAULT_CSS = """
|
||||
.view-title {
|
||||
@@ -191,6 +192,15 @@ class WorkoutView(BaseView):
|
||||
padding: 1;
|
||||
margin: 1 0;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: $error;
|
||||
text-style: bold;
|
||||
background: #2a0a0a; /* Dark red background */
|
||||
padding: 1;
|
||||
margin: 1 0;
|
||||
border: round $error;
|
||||
}
|
||||
"""
|
||||
|
||||
class WorkoutSelected(Message):
|
||||
@@ -210,7 +220,14 @@ class WorkoutView(BaseView):
|
||||
sys.stdout.write("WorkoutView.compose: START\n")
|
||||
yield Static("Workout Management", classes="view-title")
|
||||
|
||||
if self.loading:
|
||||
if self.error_message:
|
||||
yield Static(
|
||||
f"Error: {self.error_message}",
|
||||
classes="error-message",
|
||||
id="error-display"
|
||||
)
|
||||
yield Button("Retry Loading", id="retry-loading-btn", variant="primary")
|
||||
elif self.loading:
|
||||
yield LoadingSpinner("Loading workouts...")
|
||||
else:
|
||||
with TabbedContent():
|
||||
@@ -231,12 +248,31 @@ class WorkoutView(BaseView):
|
||||
self.load_data()
|
||||
sys.stdout.write("WorkoutView.on_mount: END\n")
|
||||
|
||||
async def _load_workouts_with_timeout(self) -> tuple[list, dict]:
|
||||
"""Load workouts with 5-second timeout."""
|
||||
try:
|
||||
# Wrap the actual loading with timeout
|
||||
result = await asyncio.wait_for(
|
||||
self._load_workouts_data(),
|
||||
timeout=5.0
|
||||
)
|
||||
return result
|
||||
except asyncio.TimeoutError:
|
||||
raise Exception("Loading timed out after 5 seconds")
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
def load_data(self) -> None:
|
||||
"""Public method to trigger data loading for the workout view."""
|
||||
sys.stdout.write("WorkoutView.load_data: START\n")
|
||||
self.loading = True
|
||||
self.run_async(self._load_workouts_data(), self.on_workouts_loaded)
|
||||
sys.stdout.write("WorkoutView.load_data: END\n")
|
||||
"""Public method to trigger data loading for the workout view."""
|
||||
sys.stdout.write("WorkoutView.load_data: START\n")
|
||||
self.loading = True
|
||||
self.run_async(
|
||||
self._async_wrapper(
|
||||
self._load_workouts_with_timeout(),
|
||||
self.on_workouts_loaded
|
||||
)
|
||||
)
|
||||
sys.stdout.write("WorkoutView.load_data: END\n")
|
||||
|
||||
async def _load_workouts_data(self) -> tuple[list, dict]:
|
||||
"""Load workouts and sync status (async worker)."""
|
||||
@@ -272,6 +308,7 @@ class WorkoutView(BaseView):
|
||||
self.workouts = workouts
|
||||
self.sync_status = sync_status
|
||||
self.loading = False
|
||||
self.error_message = None
|
||||
self.refresh(layout=True)
|
||||
self.populate_workouts_table()
|
||||
self.update_sync_status()
|
||||
@@ -280,9 +317,11 @@ class WorkoutView(BaseView):
|
||||
sys.stdout.write(f"WorkoutView.on_workouts_loaded: ERROR: {e}\n")
|
||||
self.log(f"Error in on_workouts_loaded: {e}", severity="error")
|
||||
self.loading = False
|
||||
self.error_message = f"Failed to process loaded data: {str(e)}"
|
||||
self.refresh()
|
||||
finally:
|
||||
sys.stdout.write("WorkoutView.on_workouts_loaded: FINALLY\n")
|
||||
|
||||
|
||||
async def populate_workouts_table(self) -> None:
|
||||
"""Populate the workouts table."""
|
||||
@@ -361,6 +400,9 @@ class WorkoutView(BaseView):
|
||||
await self.check_sync_status()
|
||||
elif event.button.id == "analyze-workout-btn":
|
||||
await self.analyze_selected_workout()
|
||||
elif event.button.id == "retry-loading-btn":
|
||||
self.error_message = None
|
||||
await self.load_data()
|
||||
elif event.button.id.startswith("approve-analysis-"):
|
||||
analysis_id = int(event.button.id.split("-")[-1])
|
||||
await self.approve_analysis(analysis_id)
|
||||
@@ -487,5 +529,10 @@ class WorkoutView(BaseView):
|
||||
|
||||
def watch_loading(self, loading: bool) -> None:
|
||||
"""React to loading state changes."""
|
||||
if hasattr(self, '_mounted') and self._mounted:
|
||||
self.refresh()
|
||||
|
||||
def watch_error_message(self, error_message: Optional[str]) -> None:
|
||||
"""React to error message changes."""
|
||||
if hasattr(self, '_mounted') and self._mounted:
|
||||
self.refresh()
|
||||
Reference in New Issue
Block a user