from typing import Optional from fastapi import APIRouter, HTTPException from pydantic import BaseModel from garminsync.database import Activity, DaemonConfig, SyncLog, get_session router = APIRouter(prefix="/api") class ScheduleConfig(BaseModel): enabled: bool cron_schedule: str @router.get("/status") async def get_status(): """Get current daemon status""" session = get_session() try: config = session.query(DaemonConfig).first() # Get recent logs logs = session.query(SyncLog).order_by(SyncLog.timestamp.desc()).limit(10).all() # Convert to dictionaries to avoid session issues daemon_data = { "running": config.status == "running" if config else False, "next_run": config.next_run if config else None, "schedule": config.schedule_cron if config else None, "last_run": config.last_run if config else None, "enabled": config.enabled if config else False, } log_data = [] for log in logs: log_data.append( { "timestamp": log.timestamp, "operation": log.operation, "status": log.status, "message": log.message, "activities_processed": log.activities_processed, "activities_downloaded": log.activities_downloaded, } ) return {"daemon": daemon_data, "recent_logs": log_data} finally: session.close() @router.post("/schedule") async def update_schedule(config: ScheduleConfig): """Update daemon schedule configuration""" session = get_session() try: daemon_config = session.query(DaemonConfig).first() if not daemon_config: daemon_config = DaemonConfig() session.add(daemon_config) daemon_config.enabled = config.enabled daemon_config.schedule_cron = config.cron_schedule session.commit() return {"message": "Configuration updated successfully"} except Exception as e: session.rollback() raise HTTPException( status_code=500, detail=f"Failed to update configuration: {str(e)}" ) finally: session.close() @router.post("/sync/trigger") async def trigger_sync(): """Manually trigger a sync operation""" try: # Import here to avoid circular imports import os from datetime import datetime from pathlib import Path from garminsync.database import Activity, sync_database from garminsync.garmin import GarminClient # Create client and sync client = GarminClient() sync_database(client) # Download missing activities session = get_session() try: missing_activities = ( session.query(Activity).filter_by(downloaded=False).all() ) downloaded_count = 0 data_dir = Path(os.getenv("DATA_DIR", "data")) data_dir.mkdir(parents=True, exist_ok=True) for activity in missing_activities: try: fit_data = client.download_activity_fit(activity.activity_id) timestamp = activity.start_time.replace(":", "-").replace(" ", "_") filename = f"activity_{activity.activity_id}_{timestamp}.fit" filepath = data_dir / filename with open(filepath, "wb") as f: f.write(fit_data) activity.filename = str(filepath) activity.downloaded = True activity.last_sync = datetime.now().isoformat() downloaded_count += 1 session.commit() except Exception as e: print(f"Failed to download activity {activity.activity_id}: {e}") session.rollback() return { "message": f"Sync completed successfully. Downloaded {downloaded_count} activities." } finally: session.close() except Exception as e: raise HTTPException(status_code=500, detail=f"Sync failed: {str(e)}") @router.get("/activities/stats") async def get_activity_stats(): """Get activity statistics""" from garminsync.database import get_offline_stats return get_offline_stats() @router.get("/logs") async def get_logs( status: str = None, operation: str = None, date: str = None, page: int = 1, per_page: int = 20, ): """Get sync logs with filtering and pagination""" session = get_session() try: query = session.query(SyncLog) # Apply filters if status: query = query.filter(SyncLog.status == status) if operation: query = query.filter(SyncLog.operation == operation) if date: # Filter by date (assuming ISO format) query = query.filter(SyncLog.timestamp.like(f"{date}%")) # Get total count for pagination total = query.count() # Apply pagination logs = ( query.order_by(SyncLog.timestamp.desc()) .offset((page - 1) * per_page) .limit(per_page) .all() ) log_data = [] for log in logs: log_data.append( { "id": log.id, "timestamp": log.timestamp, "operation": log.operation, "status": log.status, "message": log.message, "activities_processed": log.activities_processed, "activities_downloaded": log.activities_downloaded, } ) return {"logs": log_data, "total": total, "page": page, "per_page": per_page} finally: session.close() @router.post("/daemon/start") async def start_daemon(): """Start the daemon process""" from garminsync.daemon import daemon_instance try: # Start the daemon in a separate thread to avoid blocking import threading daemon_thread = threading.Thread(target=daemon_instance.start) daemon_thread.daemon = True daemon_thread.start() # Update daemon status in database session = get_session() config = session.query(DaemonConfig).first() if not config: config = DaemonConfig() session.add(config) config.status = "running" session.commit() return {"message": "Daemon started successfully"} except Exception as e: session.rollback() raise HTTPException(status_code=500, detail=f"Failed to start daemon: {str(e)}") finally: session.close() @router.post("/daemon/stop") async def stop_daemon(): """Stop the daemon process""" from garminsync.daemon import daemon_instance try: # Stop the daemon daemon_instance.stop() # Update daemon status in database session = get_session() config = session.query(DaemonConfig).first() if config: config.status = "stopped" session.commit() return {"message": "Daemon stopped successfully"} except Exception as e: session.rollback() raise HTTPException(status_code=500, detail=f"Failed to stop daemon: {str(e)}") finally: session.close() @router.delete("/logs") async def clear_logs(): """Clear all sync logs""" session = get_session() try: session.query(SyncLog).delete() session.commit() return {"message": "Logs cleared successfully"} except Exception as e: session.rollback() raise HTTPException(status_code=500, detail=f"Failed to clear logs: {str(e)}") finally: session.close() @router.get("/activities") async def get_activities( page: int = 1, per_page: int = 50, activity_type: str = None, date_from: str = None, date_to: str = None, ): """Get paginated activities with filtering""" session = get_session() try: query = session.query(Activity) # Apply filters if activity_type: query = query.filter(Activity.activity_type == activity_type) if date_from: query = query.filter(Activity.start_time >= date_from) if date_to: query = query.filter(Activity.start_time <= date_to) # Get total count for pagination total = query.count() # Apply pagination activities = ( query.order_by(Activity.start_time.desc()) .offset((page - 1) * per_page) .limit(per_page) .all() ) activity_data = [] for activity in activities: activity_data.append( { "activity_id": activity.activity_id, "start_time": activity.start_time, "activity_type": activity.activity_type, "duration": activity.duration, "distance": activity.distance, "max_heart_rate": activity.max_heart_rate, "avg_power": activity.avg_power, "calories": activity.calories, "filename": activity.filename, "downloaded": activity.downloaded, "created_at": activity.created_at, "last_sync": activity.last_sync, } ) return { "activities": activity_data, "total": total, "page": page, "per_page": per_page, } finally: session.close() @router.get("/activities/{activity_id}") async def get_activity_details(activity_id: int): """Get detailed activity information""" session = get_session() try: activity = ( session.query(Activity).filter(Activity.activity_id == activity_id).first() ) if not activity: raise HTTPException( status_code=404, detail=f"Activity with ID {activity_id} not found" ) return { "id": activity.activity_id, "name": activity.filename or "Unnamed Activity", "distance": activity.distance, "duration": activity.duration, "start_time": activity.start_time, "activity_type": activity.activity_type, "max_heart_rate": activity.max_heart_rate, "avg_power": activity.avg_power, "calories": activity.calories, "filename": activity.filename, "downloaded": activity.downloaded, "created_at": activity.created_at, "last_sync": activity.last_sync, } finally: session.close() @router.get("/dashboard/stats") async def get_dashboard_stats(): """Get comprehensive dashboard statistics""" from garminsync.database import get_offline_stats return get_offline_stats() @router.get("/api/activities") async def get_api_activities(page: int = 1, per_page: int = 10): """Get paginated activities for API""" session = get_session() try: # Use the existing get_paginated method from Activity class pagination = Activity.get_paginated(page, per_page) activities = pagination.items total_pages = pagination.pages current_page = pagination.page total_items = pagination.total if not activities and page > 1: raise HTTPException( status_code=404, detail=f"No activities found for page {page}" ) if not activities and page == 1 and total_items == 0: raise HTTPException(status_code=404, detail="No activities found") if not activities: raise HTTPException(status_code=404, detail="No activities found") return { "activities": [ { "id": activity.activity_id, "name": activity.filename or "Unnamed Activity", "distance": activity.distance, "duration": activity.duration, "start_time": activity.start_time, "activity_type": activity.activity_type, "max_heart_rate": activity.max_heart_rate, "avg_power": activity.avg_power, "calories": activity.calories, "downloaded": activity.downloaded, "created_at": activity.created_at, "last_sync": activity.last_sync, "device": activity.device or "Unknown", "intensity": activity.intensity or "Unknown", "average_speed": activity.average_speed, "elevation_gain": activity.elevation_gain, "heart_rate_zones": activity.heart_rate_zones or [], "power_zones": activity.power_zones or [], "training_effect": activity.training_effect or 0, "training_effect_label": activity.training_effect_label or "Unknown", } for activity in activities ], "total_pages": total_pages, "current_page": current_page, "total_items": total_items, "page_size": per_page, "status": "success", } except Exception as e: raise HTTPException( status_code=500, detail=f"An error occurred while fetching activities: {str(e)}", ) finally: session.close()