"""StackPilot backend — FastAPI application entry point.""" from __future__ import annotations import asyncio import logging from contextlib import asynccontextmanager from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from sqlmodel import Session from config import settings from version import APP_VERSION from database import engine, init_db from docker_client import DockerError from routers import ( agents, audit, auth, backups, containers, dashboard, destinations, editor, files, images, networks, ports, schedules, secrets, settings as settings_router, stacks, system, templates, volumes, ws, ) from services import ( backup_destination_service, schedule_service, template_service, update_service, ) logging.basicConfig(level=logging.INFO) logger = logging.getLogger("stackpilot") @asynccontextmanager async def lifespan(app: FastAPI): init_db() # Register stacks that already exist on disk. try: with Session(engine) as session: stacks.sync_discovered_stacks(session) except Exception as exc: # noqa: BLE001 logger.warning("Stack discovery failed: %s", exc) # One-off: encrypt backup-destination credentials written before they were # stored encrypted (see services/crypto_service.py). try: with Session(engine) as session: encrypted = backup_destination_service.migrate_plaintext_configs(session) if encrypted: logger.info("Encrypted %d backup destination config(s) at rest", encrypted) except Exception as exc: # noqa: BLE001 logger.warning("Destination config encryption migration failed: %s", exc) try: moved = template_service.migrate_legacy_db_templates() if moved: logger.info("Migrated %d custom template(s) from the database to folders", moved) except Exception as exc: # noqa: BLE001 logger.warning("Legacy template migration failed: %s", exc) update_task = asyncio.create_task(update_service.background_loop()) schedule_task = asyncio.create_task(schedule_service.scheduler_loop()) logger.info("StackPilot backend ready on port %s", settings.PORT) yield update_task.cancel() schedule_task.cancel() app = FastAPI(title="StackPilot", version=APP_VERSION, lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.exception_handler(DockerError) async def docker_error_handler(_request: Request, exc: DockerError): return JSONResponse( status_code=502, content={"error": exc.error, "detail": exc.detail}, ) app.include_router(auth.router) app.include_router(stacks.router) app.include_router(secrets.router) app.include_router(containers.router) app.include_router(dashboard.router) app.include_router(system.router) app.include_router(volumes.router) app.include_router(editor.router) app.include_router(files.router) app.include_router(images.router) app.include_router(ports.router) app.include_router(templates.router) app.include_router(audit.router) app.include_router(settings_router.router) app.include_router(backups.router) app.include_router(destinations.router) app.include_router(schedules.router) app.include_router(networks.router) app.include_router(agents.router) app.include_router(ws.router) @app.get("/api/health") def health() -> dict: return {"status": "ok", "version": APP_VERSION}