import logging import os from fastapi import FastAPI, Request from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles from src.api.auth import router as auth_router from src.api.dashboard import router as dashboard_router from src.api.events import router as events_router from src.api.git_repositories import router as git_repositories_router from src.api.health import router as health_router from src.api.projects import router as projects_router from src.api.ssh_keys import router as ssh_keys_router from src.api.terminal import router as terminal_router from src.api.instance_proxy import router as instance_proxy_router from src.api.config_profiles import router as config_profiles_router from src.api.tool_definitions import router as tool_definitions_router from src.api.tool_instances import router as tool_instances_router from src.api.tool_instances import sessions_router from src.api.tool_types import router as tool_types_router from src.api.notifications import router as notifications_router from src.api.user_config import router as user_config_router from src.api.users import router as users_router from src.api.workspaces import all_workspaces_router, router as workspaces_router from src.config import Settings from src.models.notification import Notification # noqa: F401 – Alembic model discovery from src.models.terminal_session import TerminalSessionModel # noqa: F401 – Alembic model discovery from src.database import init_database from src.logging_config import ( ExceptionLoggingMiddleware, RequestLoggingMiddleware, configure_logging, ) from src.services.correlation import CorrelationIdMiddleware from src.services.event_bus import InstanceEventBus from src.services.health_monitor import HealthMonitor # Configure logging early log_level = os.getenv("LOG_LEVEL", "INFO").upper() configure_logging(level=getattr(logging, log_level, logging.INFO)) logger = logging.getLogger(__name__) settings = Settings() app = FastAPI(title="Headquarter API") # Configure CORS - must be before other middleware # Build allowed origins list including web and api domains cors_origins = [settings.web_base_url] if settings.api_base_url != settings.web_base_url: cors_origins.append(settings.api_base_url) logger.info("CORS configured with origins: %s", cors_origins) app.add_middleware( CORSMiddleware, allow_origins=cors_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.add_middleware(CorrelationIdMiddleware) app.add_middleware(RequestLoggingMiddleware) app.add_middleware(ExceptionLoggingMiddleware) def _sanitize_validation_errors(errors): """Convert validation errors to JSON-safe format.""" sanitized = [] for error in errors: safe_error = { "type": error.get("type"), "loc": error.get("loc"), "msg": error.get("msg"), "input": str(error.get("input")) if error.get("input") is not None else None, } # Convert ctx to safe format ctx = error.get("ctx") if ctx: safe_ctx = {} for key, value in ctx.items(): if isinstance(value, Exception): safe_ctx[key] = str(value) elif isinstance(value, (str, int, float, bool, type(None))): safe_ctx[key] = value else: safe_ctx[key] = str(value) safe_error["ctx"] = safe_ctx sanitized.append(safe_error) return sanitized @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): """Log validation errors and return detailed response.""" errors = exc.errors() logger.warning( "Validation error for %s %s: %s", request.method, request.url.path, errors, ) safe_errors = _sanitize_validation_errors(errors) return JSONResponse( status_code=422, content={"detail": safe_errors}, ) # Global services _event_bus = InstanceEventBus() _health_monitor = HealthMonitor(_event_bus) @app.on_event("startup") async def on_startup(): logger.info("Starting up Headquarter API...") # Initialize database (run migrations) db_ready = await init_database() if not db_ready: logger.error("Database initialization failed. Shutting down.") import sys sys.exit(1) # Start background health monitor _health_monitor.start() logger.info("Health monitor started") logger.info("Startup complete.") @app.on_event("shutdown") async def on_shutdown(): logger.info("Shutting down Headquarter API...") _health_monitor.stop() logger.info("Health monitor stopped") logger.info("Shutdown complete.") app.include_router(health_router) app.include_router(auth_router) app.include_router(dashboard_router) app.include_router(projects_router) app.include_router(users_router) app.include_router(ssh_keys_router) app.include_router(git_repositories_router) app.include_router(user_config_router) app.include_router(tool_types_router) app.include_router(tool_definitions_router) app.include_router(config_profiles_router) app.include_router(tool_instances_router) app.include_router(sessions_router) app.include_router(instance_proxy_router) app.include_router(terminal_router) app.include_router(events_router) app.include_router(notifications_router) app.include_router(all_workspaces_router) app.include_router(workspaces_router) app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")