Files
MileVault/backend/app/main.py
T
owain 6d224d51c5
Build and push images / validate (push) Successful in 2s
Build and push images / build-backend (push) Successful in 7s
Build and push images / build-worker (push) Successful in 6s
Build and push images / build-frontend (push) Successful in 8s
Add Garmin Connect auto-sync via python-garminconnect
- GarminConnectConfig model stores encrypted credentials and OAuth token
- garmin_connect_sync service: token-based auth with password fallback,
  activity FIT download + queue, daily wellness from JSON API
- Celery beat schedule: sync_all_garmin_connect fires every hour
- New API router /api/garmin-sync: config CRUD, manual trigger
- Beat container added to docker-compose.yml and docker-compose.deploy.yml
- ProfilePage: Garmin Connect section with connect/update/disconnect and Sync now

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 00:08:12 +01:00

107 lines
3.6 KiB
Python

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
from sqlalchemy import text
import asyncio
from app.core.database import engine, AsyncSessionLocal, Base
from app.core.config import settings
from app.api import auth, activities, routes, health, records, upload, profile, garmin_sync
async def init_db():
"""Create tables then seed admin, with retries for slow DB startup.
Multiple uvicorn workers may race here on first start. We tolerate
duplicate table errors since they just mean another worker got there first.
"""
for attempt in range(15):
try:
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
break
except Exception as e:
msg = str(e).lower()
if "already exists" in msg or "duplicate" in msg or "pg_type_typname" in msg:
print("Tables already created by another worker - skipping")
break
if attempt == 14:
raise
print(f"DB not ready yet (attempt {attempt + 1}/15): {e}")
await asyncio.sleep(2)
# Try TimescaleDB hypertable (non-fatal)
try:
async with engine.begin() as conn:
await conn.execute(text(
"SELECT create_hypertable('activity_data_points', 'timestamp', "
"if_not_exists => TRUE, migrate_data => TRUE)"
))
except Exception as e:
print(f"TimescaleDB hypertable skipped: {e}")
# Seed admin user (only if password is configured)
if not settings.admin_password:
print("ADMIN_PASSWORD not set - skipping admin user seed")
return
from sqlalchemy import select
from app.models.user import User
from app.core.security import hash_password
try:
async with AsyncSessionLocal() as db:
result = await db.execute(
select(User).where(User.username == settings.admin_username)
)
if not result.scalar_one_or_none():
admin = User(
username=settings.admin_username,
hashed_password=hash_password(settings.admin_password),
is_admin=True,
)
db.add(admin)
await db.commit()
print(f"Admin user '{settings.admin_username}' created")
except Exception as e:
msg = str(e).lower()
if "duplicate" in msg or "unique" in msg:
print("Admin user already exists - skipping seed")
else:
raise
@asynccontextmanager
async def lifespan(app: FastAPI):
await init_db()
yield
app = FastAPI(
title="MileVault",
version="1.0.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"] if settings.environment == "development" else [],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
app.include_router(activities.router, prefix="/api/activities", tags=["activities"])
app.include_router(routes.router, prefix="/api/routes", tags=["routes"])
app.include_router(health.router, prefix="/api/health-metrics", tags=["health"])
app.include_router(records.router, prefix="/api/records", tags=["records"])
app.include_router(upload.router, prefix="/api/upload", tags=["upload"])
app.include_router(profile.router, prefix="/api/profile", tags=["profile"])
app.include_router(garmin_sync.router, prefix="/api/garmin-sync", tags=["garmin-sync"])
@app.get("/health")
async def healthcheck():
return {"status": "ok"}