import logging import os import sys from pathlib import Path import uvicorn from fastapi import FastAPI, HTTPException from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from sqlalchemy import text sys.path.insert(0, str(Path(__file__).parent)) from database import Base, engine from routers import auth_router, caldav_router, google_router, homeassistant_router, ical_router, local_router, profile_router, settings_router, users_router logging.basicConfig(level=logging.INFO) # Create DB tables on startup Base.metadata.create_all(bind=engine) # Run column migrations for new fields (safe: only adds if not exists) def _migrate(): with engine.connect() as conn: # Add week_start_day to user_settings if not present try: conn.execute(text( "ALTER TABLE user_settings ADD COLUMN week_start_day VARCHAR(10) DEFAULT 'monday'" )) conn.commit() logging.info("Migration: added week_start_day column") except Exception: pass # Column already exists try: conn.execute(text("ALTER TABLE calendars ADD COLUMN sidebar_hidden BOOLEAN DEFAULT 0")) conn.commit() logging.info("Migration: added sidebar_hidden to calendars") except Exception: pass try: conn.execute(text("ALTER TABLE google_calendars ADD COLUMN sidebar_hidden BOOLEAN DEFAULT 0")) conn.commit() logging.info("Migration: added sidebar_hidden to google_calendars") except Exception: pass try: conn.execute(text("ALTER TABLE user_settings ADD COLUMN text_contrast INTEGER DEFAULT 3")) conn.commit() except Exception: pass try: conn.execute(text("ALTER TABLE user_settings ADD COLUMN line_contrast INTEGER DEFAULT 3")) conn.commit() except Exception: pass try: conn.execute(text("ALTER TABLE user_settings ADD COLUMN hour_height INTEGER DEFAULT 60")) conn.commit() except Exception: pass try: conn.execute(text("ALTER TABLE user_settings ADD COLUMN language VARCHAR(5) DEFAULT 'de'")) conn.commit() except Exception: pass try: conn.execute(text("ALTER TABLE homeassistant_accounts ADD COLUMN auth_method VARCHAR(20) DEFAULT 'token'")) conn.commit() except Exception: pass try: conn.execute(text("ALTER TABLE homeassistant_accounts ADD COLUMN refresh_token TEXT")) conn.commit() except Exception: pass try: conn.execute(text("ALTER TABLE homeassistant_accounts ADD COLUMN token_expiry DATETIME")) conn.commit() except Exception: pass try: conn.execute(text("ALTER TABLE homeassistant_accounts ADD COLUMN client_id VARCHAR(500)")) conn.commit() except Exception: pass try: conn.execute(text("ALTER TABLE local_events ADD COLUMN rrule TEXT")) conn.commit() logging.info("Migration: added rrule to local_events") except Exception: pass try: conn.execute(text("ALTER TABLE local_events ADD COLUMN exdate TEXT")) conn.commit() logging.info("Migration: added exdate to local_events") except Exception: pass try: conn.execute(text("ALTER TABLE user_settings ADD COLUMN month_divider_color VARCHAR(7) DEFAULT '#7090c0'")) conn.commit() except Exception: pass try: conn.execute(text("ALTER TABLE user_settings ADD COLUMN month_label_color VARCHAR(7) DEFAULT '#7090c0'")) conn.commit() except Exception: pass _migrate() app = FastAPI(title="Calendarr", docs_url=None, redoc_url=None) app.include_router(auth_router.router, prefix="/api/auth", tags=["auth"]) app.include_router(users_router.router, prefix="/api/users", tags=["users"]) app.include_router(caldav_router.router, prefix="/api/caldav", tags=["caldav"]) app.include_router(settings_router.router, prefix="/api/settings", tags=["settings"]) app.include_router(profile_router.router, prefix="/api/profile", tags=["profile"]) app.include_router(local_router.router, prefix="/api/local", tags=["local"]) app.include_router(ical_router.router, prefix="/api/ical", tags=["ical"]) app.include_router(google_router.router, prefix="/api/google", tags=["google"]) app.include_router(homeassistant_router.router, prefix="/api/homeassistant", tags=["homeassistant"]) FRONTEND_DIR = Path(__file__).parent.parent / "frontend" app.mount("/static", StaticFiles(directory=str(FRONTEND_DIR)), name="static") # ── PWA assets that must live at root scope ────────────── @app.get("/manifest.json") async def pwa_manifest(): return FileResponse(str(FRONTEND_DIR / "manifest.json"), media_type="application/manifest+json") @app.get("/sw.js") async def pwa_service_worker(): return FileResponse( str(FRONTEND_DIR / "sw.js"), media_type="application/javascript", headers={"Service-Worker-Allowed": "/", "Cache-Control": "no-cache"}, ) @app.get("/icons/{icon_name}") async def pwa_icon(icon_name: str): icon_path = FRONTEND_DIR / "icons" / icon_name if not icon_path.exists() or not icon_path.is_file(): raise HTTPException(status_code=404, detail="Icon not found") return FileResponse(str(icon_path)) @app.get("/{full_path:path}") async def spa_fallback(full_path: str): if full_path.startswith("api/"): raise HTTPException(status_code=404, detail="API endpoint not found") index = FRONTEND_DIR / "index.html" return FileResponse(str(index)) if __name__ == "__main__": port = int(os.environ.get("PORT", 8080)) host = os.environ.get("HOST", "0.0.0.0") uvicorn.run(app, host=host, port=port)