sync - still working on the TUI

This commit is contained in:
2025-09-27 13:24:20 -07:00
parent 72b5cc3aaa
commit ec02b923af
25 changed files with 1091 additions and 367 deletions

View File

@@ -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)}")

View File

@@ -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()