working again stable

This commit is contained in:
2025-08-22 16:36:45 -07:00
parent 9c4e652047
commit 5f0cd85406
14 changed files with 867 additions and 462 deletions

View File

@@ -1,9 +1,11 @@
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.responses import JSONResponse
import os
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from .routes import router
app = FastAPI(title="GarminSync Dashboard")
@@ -26,73 +28,80 @@ else:
# Include API routes
app.include_router(router)
@app.get("/")
async def dashboard(request: Request):
"""Dashboard route with fallback for missing templates"""
if not templates:
# Return JSON response if templates are not available
from garminsync.database import get_offline_stats
stats = get_offline_stats()
return JSONResponse({
"message": "GarminSync Dashboard",
"stats": stats,
"note": "Web UI templates not found, showing JSON response"
})
return JSONResponse(
{
"message": "GarminSync Dashboard",
"stats": stats,
"note": "Web UI templates not found, showing JSON response",
}
)
try:
# Get current statistics
from garminsync.database import get_offline_stats
stats = get_offline_stats()
return templates.TemplateResponse("dashboard.html", {
"request": request,
"stats": stats
})
return templates.TemplateResponse(
"dashboard.html", {"request": request, "stats": stats}
)
except Exception as e:
return JSONResponse({
"error": f"Failed to load dashboard: {str(e)}",
"message": "Dashboard unavailable, API endpoints still functional"
})
return JSONResponse(
{
"error": f"Failed to load dashboard: {str(e)}",
"message": "Dashboard unavailable, API endpoints still functional",
}
)
@app.get("/health")
async def health_check():
"""Health check endpoint"""
return {"status": "healthy", "service": "GarminSync Dashboard"}
@app.get("/config")
async def config_page(request: Request):
"""Configuration page"""
if not templates:
return JSONResponse({
"message": "Configuration endpoint",
"note": "Use /api/schedule endpoints for configuration"
})
return templates.TemplateResponse("config.html", {
"request": request
})
return JSONResponse(
{
"message": "Configuration endpoint",
"note": "Use /api/schedule endpoints for configuration",
}
)
return templates.TemplateResponse("config.html", {"request": request})
@app.get("/activities")
async def activities_page(request: Request):
"""Activities page route"""
if not templates:
return JSONResponse({"message": "Activities endpoint"})
return templates.TemplateResponse("activities.html", {
"request": request
})
return templates.TemplateResponse("activities.html", {"request": request})
# Error handlers
@app.exception_handler(404)
async def not_found_handler(request: Request, exc):
return JSONResponse(
status_code=404,
content={"error": "Not found", "path": str(request.url.path)}
status_code=404, content={"error": "Not found", "path": str(request.url.path)}
)
@app.exception_handler(500)
async def server_error_handler(request: Request, exc):
return JSONResponse(
status_code=500,
content={"error": "Internal server error", "detail": str(exc)}
status_code=500, content={"error": "Internal server error", "detail": str(exc)}
)

View File

@@ -1,144 +1,159 @@
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from garminsync.database import get_session, DaemonConfig, SyncLog, Activity
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
"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
}
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)}")
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
from garminsync.garmin import GarminClient
from garminsync.database import sync_database, Activity
from datetime import datetime
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()
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."}
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
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)
@@ -147,48 +162,50 @@ async def get_logs(
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()
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
}
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()
@@ -197,7 +214,7 @@ async def start_daemon():
session.add(config)
config.status = "running"
session.commit()
return {"message": "Daemon started successfully"}
except Exception as e:
session.rollback()
@@ -205,21 +222,23 @@ async def start_daemon():
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()
@@ -227,6 +246,7 @@ async def stop_daemon():
finally:
session.close()
@router.delete("/logs")
async def clear_logs():
"""Clear all sync logs"""
@@ -241,19 +261,20 @@ async def clear_logs():
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
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)
@@ -261,70 +282,147 @@ async def get_activities(
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()
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
})
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
"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()
activity = (
session.query(Activity).filter(Activity.activity_id == activity_id).first()
)
if not activity:
raise HTTPException(status_code=404, detail="Activity not found")
raise HTTPException(
status_code=404, detail=f"Activity with ID {activity_id} not found"
)
return {
"activity_id": activity.activity_id,
"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,
"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
"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()

View File

@@ -3,18 +3,20 @@
Simple test script to verify the new UI is working correctly
"""
import requests
import time
import sys
import time
from pathlib import Path
import requests
# Add the parent directory to the path to import garminsync modules
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
def test_ui_endpoints():
"""Test that the new UI endpoints are working correctly"""
base_url = "http://localhost:8000"
# Test endpoints to check
endpoints = [
"/",
@@ -23,26 +25,26 @@ def test_ui_endpoints():
"/logs",
"/api/status",
"/api/activities/stats",
"/api/dashboard/stats"
"/api/dashboard/stats",
]
print("Testing UI endpoints...")
failed_endpoints = []
for endpoint in endpoints:
try:
url = base_url + endpoint
print(f"Testing {url}...")
response = requests.get(url, timeout=10)
if response.status_code == 200:
print(f"{endpoint} - OK")
else:
print(f"{endpoint} - Status code: {response.status_code}")
failed_endpoints.append(endpoint)
except requests.exceptions.ConnectionError:
print(f"{endpoint} - Connection error (server not running?)")
failed_endpoints.append(endpoint)
@@ -52,7 +54,7 @@ def test_ui_endpoints():
except Exception as e:
print(f"{endpoint} - Error: {e}")
failed_endpoints.append(endpoint)
if failed_endpoints:
print(f"\nFailed endpoints: {failed_endpoints}")
return False
@@ -60,45 +62,51 @@ def test_ui_endpoints():
print("\nAll endpoints are working correctly!")
return True
def test_api_endpoints():
"""Test that the new API endpoints are working correctly"""
base_url = "http://localhost:8000"
# Test API endpoints
api_endpoints = [
("/api/activities", "GET"),
("/api/activities/1", "GET"), # This might fail if activity doesn't exist, which is OK
("/api/dashboard/stats", "GET")
(
"/api/activities/1",
"GET",
), # This might fail if activity doesn't exist, which is OK
("/api/dashboard/stats", "GET"),
]
print("\nTesting API endpoints...")
for endpoint, method in api_endpoints:
try:
url = base_url + endpoint
print(f"Testing {method} {url}...")
if method == "GET":
response = requests.get(url, timeout=10)
else:
response = requests.post(url, timeout=10)
# For activity details, 404 is acceptable if activity doesn't exist
if endpoint == "/api/activities/1" and response.status_code == 404:
print(f"{endpoint} - OK (404 expected if activity doesn't exist)")
continue
if response.status_code == 200:
print(f"{endpoint} - OK")
# Try to parse JSON
try:
data = response.json()
print(f" Response keys: {list(data.keys()) if isinstance(data, dict) else 'Not a dict'}")
print(
f" Response keys: {list(data.keys()) if isinstance(data, dict) else 'Not a dict'}"
)
except:
print(" Response is not JSON")
else:
print(f"{endpoint} - Status code: {response.status_code}")
except requests.exceptions.ConnectionError:
print(f"{endpoint} - Connection error (server not running?)")
except requests.exceptions.Timeout:
@@ -106,16 +114,17 @@ def test_api_endpoints():
except Exception as e:
print(f"{endpoint} - Error: {e}")
if __name__ == "__main__":
print("GarminSync UI Test Script")
print("=" * 30)
# Test UI endpoints
ui_success = test_ui_endpoints()
# Test API endpoints
test_api_endpoints()
print("\n" + "=" * 30)
if ui_success:
print("UI tests completed successfully!")