mirror of
https://github.com/sstent/GarminSync.git
synced 2026-01-25 08:35:02 +00:00
python v2 - added feartures 1 and 2 - no errors
This commit is contained in:
BIN
data/garmin.db
BIN
data/garmin.db
Binary file not shown.
@@ -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():
|
|
||||||
uvicorn.run(app, host="0.0.0.0", port=port)
|
|
||||||
|
|
||||||
web_thread = threading.Thread(target=run_server, daemon=True)
|
def run_server():
|
||||||
web_thread.start()
|
try:
|
||||||
self.web_server = web_thread
|
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.start()
|
||||||
|
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()
|
||||||
@@ -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)}
|
||||||
|
)
|
||||||
@@ -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
|
|
||||||
logs = session.query(SyncLog).order_by(SyncLog.timestamp.desc()).limit(10).all()
|
# Get recent logs
|
||||||
|
logs = session.query(SyncLog).order_by(SyncLog.timestamp.desc()).limit(10).all()
|
||||||
return {
|
|
||||||
"daemon": {
|
# Convert to dictionaries to avoid session issues
|
||||||
|
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:
|
|
||||||
daemon_config = DaemonConfig()
|
if not daemon_config:
|
||||||
session.add(daemon_config)
|
daemon_config = DaemonConfig()
|
||||||
|
session.add(daemon_config)
|
||||||
daemon_config.enabled = config.enabled
|
|
||||||
daemon_config.schedule_cron = config.cron_schedule
|
daemon_config.enabled = config.enabled
|
||||||
session.commit()
|
daemon_config.schedule_cron = config.cron_schedule
|
||||||
|
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()
|
||||||
Reference in New Issue
Block a user