python v2 - added feartures 1 and 2 - no errors

This commit is contained in:
2025-08-08 14:11:55 -07:00
parent 9418823915
commit 0d3a974be4
4 changed files with 302 additions and 65 deletions

Binary file not shown.

View File

@@ -19,13 +19,13 @@ class GarminSyncDaemon:
"""Start daemon with scheduler and web UI""" """Start daemon with scheduler and web UI"""
try: try:
# Load configuration from database # Load configuration from database
config = self.load_config() config_data = self.load_config()
# Setup scheduled job # Setup scheduled job
if config.enabled: if config_data['enabled']:
self.scheduler.add_job( self.scheduler.add_job(
func=self.sync_and_download, func=self.sync_and_download,
trigger=CronTrigger.from_crontab(config.schedule_cron), trigger=CronTrigger.from_crontab(config_data['schedule_cron']),
id='sync_job', id='sync_job',
replace_existing=True replace_existing=True
) )
@@ -34,6 +34,9 @@ class GarminSyncDaemon:
self.scheduler.start() self.scheduler.start()
self.running = True self.running = True
# Update daemon status to running
self.update_daemon_status("running")
# Start web UI in separate thread # Start web UI in separate thread
self.start_web_ui(web_port) self.start_web_ui(web_port)
@@ -49,10 +52,12 @@ class GarminSyncDaemon:
except Exception as e: except Exception as e:
logger.error(f"Failed to start daemon: {str(e)}") logger.error(f"Failed to start daemon: {str(e)}")
self.update_daemon_status("error")
self.stop() self.stop()
def sync_and_download(self): def sync_and_download(self):
"""Scheduled job function""" """Scheduled job function"""
session = None
try: try:
self.log_operation("sync", "started") self.log_operation("sync", "started")
@@ -99,36 +104,85 @@ class GarminSyncDaemon:
logger.error(f"Failed to download activity {activity.activity_id}: {e}") logger.error(f"Failed to download activity {activity.activity_id}: {e}")
session.rollback() session.rollback()
session.close()
self.log_operation("sync", "success", self.log_operation("sync", "success",
f"Downloaded {downloaded_count} new activities") f"Downloaded {downloaded_count} new activities")
# Update last run time
self.update_daemon_last_run()
except Exception as e: except Exception as e:
logger.error(f"Sync failed: {e}") logger.error(f"Sync failed: {e}")
self.log_operation("sync", "error", str(e)) self.log_operation("sync", "error", str(e))
finally:
if session:
session.close()
def load_config(self): def load_config(self):
"""Load daemon configuration from database""" """Load daemon configuration from database and return dict"""
session = get_session() session = get_session()
config = session.query(DaemonConfig).first() try:
if not config: config = session.query(DaemonConfig).first()
# Create default configuration if not config:
config = DaemonConfig() # Create default configuration
session.add(config) config = DaemonConfig()
session.add(config)
session.commit()
session.refresh(config) # Ensure we have the latest data
# Return configuration as dictionary to avoid session issues
return {
'id': config.id,
'enabled': config.enabled,
'schedule_cron': config.schedule_cron,
'last_run': config.last_run,
'next_run': config.next_run,
'status': config.status
}
finally:
session.close()
def update_daemon_status(self, status):
"""Update daemon status in database"""
session = get_session()
try:
config = session.query(DaemonConfig).first()
if not config:
config = DaemonConfig()
session.add(config)
config.status = status
session.commit() session.commit()
return config finally:
session.close()
def update_daemon_last_run(self):
"""Update daemon last run timestamp"""
session = get_session()
try:
config = session.query(DaemonConfig).first()
if config:
config.last_run = datetime.now().isoformat()
session.commit()
finally:
session.close()
def start_web_ui(self, port): def start_web_ui(self, port):
"""Start FastAPI web server in a separate thread""" """Start FastAPI web server in a separate thread"""
from .web.app import app try:
import uvicorn from .web.app import app
import uvicorn
def run_server(): def run_server():
uvicorn.run(app, host="0.0.0.0", port=port) try:
uvicorn.run(app, host="0.0.0.0", port=port, log_level="info")
except Exception as e:
logger.error(f"Failed to start web server: {e}")
web_thread = threading.Thread(target=run_server, daemon=True) web_thread = threading.Thread(target=run_server, daemon=True)
web_thread.start() web_thread.start()
self.web_server = web_thread self.web_server = web_thread
except ImportError as e:
logger.warning(f"Could not start web UI: {e}")
def signal_handler(self, signum, frame): def signal_handler(self, signum, frame):
"""Handle shutdown signals""" """Handle shutdown signals"""
@@ -140,21 +194,33 @@ class GarminSyncDaemon:
if self.scheduler.running: if self.scheduler.running:
self.scheduler.shutdown() self.scheduler.shutdown()
self.running = False self.running = False
self.update_daemon_status("stopped")
self.log_operation("daemon", "stopped", "Daemon shutdown completed")
logger.info("Daemon stopped") logger.info("Daemon stopped")
def log_operation(self, operation, status, message=None): def log_operation(self, operation, status, message=None):
"""Log sync operation to database""" """Log sync operation to database"""
session = get_session() session = get_session()
log = SyncLog( try:
timestamp=datetime.now().isoformat(), log = SyncLog(
operation=operation, timestamp=datetime.now().isoformat(),
status=status, operation=operation,
message=message status=status,
) message=message,
session.add(log) activities_processed=0, # Can be updated later if needed
session.commit() activities_downloaded=0 # Can be updated later if needed
)
session.add(log)
session.commit()
except Exception as e:
logger.error(f"Failed to log operation: {e}")
finally:
session.close()
def count_missing(self): def count_missing(self):
"""Count missing activities""" """Count missing activities"""
session = get_session() session = get_session()
return session.query(Activity).filter_by(downloaded=False).count() try:
return session.query(Activity).filter_by(downloaded=False).count()
finally:
session.close()

View File

@@ -1,24 +1,88 @@
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from fastapi.responses import JSONResponse
import os
from pathlib import Path
from .routes import router from .routes import router
app = FastAPI(title="GarminSync Dashboard") app = FastAPI(title="GarminSync Dashboard")
# Mount static files and templates # Get the current directory path
app.mount("/static", StaticFiles(directory="garminsync/web/static"), name="static") current_dir = Path(__file__).parent
templates = Jinja2Templates(directory="garminsync/web/templates")
# Mount static files and templates with error handling
static_dir = current_dir / "static"
templates_dir = current_dir / "templates"
if static_dir.exists():
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
if templates_dir.exists():
templates = Jinja2Templates(directory=str(templates_dir))
else:
templates = None
# Include API routes # Include API routes
app.include_router(router) app.include_router(router)
@app.get("/") @app.get("/")
async def dashboard(request: Request): async def dashboard(request: Request):
# Get current statistics """Dashboard route with fallback for missing templates"""
from garminsync.database import get_offline_stats if not templates:
stats = get_offline_stats() # 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 templates.TemplateResponse("dashboard.html", { try:
"request": request, # Get current statistics
"stats": stats from garminsync.database import get_offline_stats
stats = get_offline_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"
})
@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
}) })
# 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)}
)
@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)}
)

View File

@@ -12,45 +12,152 @@ class ScheduleConfig(BaseModel):
async def get_status(): async def get_status():
"""Get current daemon status""" """Get current daemon status"""
session = get_session() session = get_session()
config = session.query(DaemonConfig).first() try:
config = session.query(DaemonConfig).first()
# Get recent logs # Get recent logs
logs = session.query(SyncLog).order_by(SyncLog.timestamp.desc()).limit(10).all() logs = session.query(SyncLog).order_by(SyncLog.timestamp.desc()).limit(10).all()
return { # Convert to dictionaries to avoid session issues
"daemon": { daemon_data = {
"running": config.status == "running" if config else False, "running": config.status == "running" if config else False,
"next_run": config.next_run if config else None, "next_run": config.next_run if config else None,
"schedule": config.schedule_cron if config else None "schedule": config.schedule_cron if config else None,
}, "last_run": config.last_run if config else None,
"recent_logs": [ "enabled": config.enabled if config else False
{ }
log_data = []
for log in logs:
log_data.append({
"timestamp": log.timestamp, "timestamp": log.timestamp,
"operation": log.operation, "operation": log.operation,
"status": log.status, "status": log.status,
"message": log.message "message": log.message,
} for log in logs "activities_processed": log.activities_processed,
] "activities_downloaded": log.activities_downloaded
} })
return {
"daemon": daemon_data,
"recent_logs": log_data
}
finally:
session.close()
@router.post("/schedule") @router.post("/schedule")
async def update_schedule(config: ScheduleConfig): async def update_schedule(config: ScheduleConfig):
"""Update daemon schedule configuration""" """Update daemon schedule configuration"""
session = get_session() session = get_session()
daemon_config = session.query(DaemonConfig).first() try:
daemon_config = session.query(DaemonConfig).first()
if not daemon_config: if not daemon_config:
daemon_config = DaemonConfig() daemon_config = DaemonConfig()
session.add(daemon_config) session.add(daemon_config)
daemon_config.enabled = config.enabled daemon_config.enabled = config.enabled
daemon_config.schedule_cron = config.cron_schedule daemon_config.schedule_cron = config.cron_schedule
session.commit() session.commit()
return {"message": "Configuration updated successfully"} 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") @router.post("/sync/trigger")
async def trigger_sync(): async def trigger_sync():
"""Manually trigger a sync operation""" """Manually trigger a sync operation"""
# TODO: Implement sync triggering try:
return {"message": "Sync triggered successfully"} # 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 pathlib import Path
# 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(limit: int = 50):
"""Get recent sync logs"""
session = get_session()
try:
logs = session.query(SyncLog).order_by(SyncLog.timestamp.desc()).limit(limit).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}
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()