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,6 +1,7 @@
import logging
import json
from datetime import datetime
from pathlib import Path
from fastapi import FastAPI, Depends, Request, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from .database import get_db
@@ -47,8 +48,11 @@ logger.addHandler(console_handler)
# Configure rotating file handler
from logging.handlers import RotatingFileHandler
# Create logs directory relative to the project root
log_dir = Path(__file__).parent.parent.parent / "logs"
log_dir.mkdir(exist_ok=True)
file_handler = RotatingFileHandler(
filename="/app/logs/app.log",
filename=log_dir / "backend.log",
maxBytes=10*1024*1024, # 10 MB
backupCount=5,
encoding='utf-8'

View File

@@ -1,5 +1,14 @@
from sqlalchemy import Column, Integer, DateTime, String, Text
from sqlalchemy import Column, Integer, DateTime, String, Text, Enum
from .base import BaseModel
import enum
class GarminSyncStatus(str, enum.Enum):
PENDING = "pending"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
AUTH_FAILED = "auth_failed"
FAILED = "failed"
class GarminSyncLog(BaseModel):
@@ -8,5 +17,5 @@ class GarminSyncLog(BaseModel):
last_sync_time = Column(DateTime)
activities_synced = Column(Integer, default=0)
status = Column(String(20)) # success, error, in_progress
status = Column(Enum(GarminSyncStatus), default=GarminSyncStatus.PENDING)
error_message = Column(Text)

View File

@@ -162,7 +162,7 @@ class AIService:
timeout=30.0
)
response.raise_for_status()
data = response.json()
data = await response.json()
return data["choices"][0]["message"]["content"]
except Exception as e:

View File

@@ -1,7 +1,10 @@
import os
from pathlib import Path
import garth
import asyncio
from typing import List, Dict, Any, Optional
from datetime import datetime, timedelta
from sqlalchemy.ext.asyncio import AsyncSession
import logging
logger = logging.getLogger(__name__)
@@ -10,14 +13,16 @@ logger = logging.getLogger(__name__)
class GarminService:
"""Service for interacting with Garmin Connect API."""
def __init__(self):
def __init__(self, db: Optional[AsyncSession] = None):
self.db = db
self.username = os.getenv("GARMIN_USERNAME")
self.password = os.getenv("GARMIN_PASSWORD")
logger.debug(f"GarminService initialized with username: {self.username is not None}, password: {self.password is not None}")
self.client: Optional[garth.Client] = None
self.session_dir = "/app/data/sessions"
self.session_dir = Path("data/sessions")
# Ensure session directory exists
os.makedirs(self.session_dir, exist_ok=True)
self.session_dir.mkdir(parents=True, exist_ok=True)
async def authenticate(self) -> bool:
"""Authenticate with Garmin Connect and persist session."""
@@ -26,14 +31,18 @@ class GarminService:
try:
# Try to load existing session
self.client.load(self.session_dir)
await asyncio.to_thread(self.client.load, self.session_dir)
logger.info("Loaded existing Garmin session")
return True
except Exception:
except Exception as e:
logger.warning(f"Failed to load existing Garmin session: {e}. Attempting fresh authentication.")
# Fresh authentication required
if not self.username or not self.password:
logger.error("Garmin username or password not set in environment variables.")
raise GarminAuthError("Garmin username or password not configured.")
try:
await self.client.login(self.username, self.password)
self.client.save(self.session_dir)
await asyncio.to_thread(self.client.login, self.username, self.password)
await asyncio.to_thread(self.client.save, self.session_dir)
logger.info("Successfully authenticated with Garmin Connect")
return True
except Exception as e:
@@ -49,7 +58,7 @@ class GarminService:
start_date = datetime.now() - timedelta(days=7)
try:
activities = self.client.get_activities(limit=limit, start=start_date)
activities = await asyncio.to_thread(self.client.get_activities, limit=limit, start=start_date)
logger.info(f"Fetched {len(activities)} activities from Garmin")
return activities
except Exception as e:
@@ -62,7 +71,7 @@ class GarminService:
await self.authenticate()
try:
details = self.client.get_activity(activity_id)
details = await asyncio.to_thread(self.client.get_activity, activity_id)
logger.info(f"Fetched details for activity {activity_id}")
return details
except Exception as e:

View File

@@ -62,7 +62,7 @@ class PlanEvolutionService:
)
.order_by(Plan.version)
)
return result.scalars().all()
return (await result.scalars()).all()
async def get_current_active_plan(self) -> Plan:
"""Get the most recent active plan."""

View File

@@ -22,7 +22,7 @@ class PromptManager:
query = query.where(Prompt.model == model)
result = await self.db.execute(query.order_by(Prompt.version.desc()))
prompt = result.scalar_one_or_none()
prompt = await result.scalar_one_or_none()
return prompt.prompt_text if prompt else None
async def create_prompt_version(

View File

@@ -97,14 +97,14 @@ class WorkoutSyncService:
.order_by(desc(GarminSyncLog.created_at))
.limit(1)
)
return result.scalar_one_or_none()
return await result.scalar_one_or_none()
async def activity_exists(self, garmin_activity_id: str) -> bool:
"""Check if activity already exists in database."""
result = await self.db.execute(
select(Workout).where(Workout.garmin_activity_id == garmin_activity_id)
)
return result.scalar_one_or_none() is not None
return (await result.scalar_one_or_none()) is not None
async def parse_activity_data(self, activity: Dict[str, Any]) -> Dict[str, Any]:
"""Parse Garmin activity data into workout model format."""