import logging from typing import Awaitable, Callable from fastapi import FastAPI, Request, Response from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from config.settings import get_settings from middleware.auth_session import AuthSessionMiddleware from middleware.metrics import MetricsMiddleware from middleware.validation import validate_json from routes.auth import router as auth_router from routes.dashboard import router as dashboard_router from routes.calculations import router as calculations_router from routes.imports import router as imports_router from routes.exports import router as exports_router from routes.projects import router as projects_router from routes.reports import router as reports_router from routes.scenarios import router as scenarios_router from routes.ui import router as ui_router from monitoring import router as monitoring_router from services.bootstrap import bootstrap_admin, bootstrap_pricing_settings from scripts.init_db import init_db as init_db_script app = FastAPI() app.add_middleware(AuthSessionMiddleware) app.add_middleware(MetricsMiddleware) logger = logging.getLogger(__name__) @app.middleware("http") async def json_validation( request: Request, call_next: Callable[[Request], Awaitable[Response]] ) -> Response: return await validate_json(request, call_next) @app.get("/health", summary="Container health probe") async def health() -> dict[str, str]: return {"status": "ok"} @app.get("/favicon.ico", include_in_schema=False) async def favicon() -> Response: static_directory = "static" favicon_img = "favicon.ico" return FileResponse(f"{static_directory}/{favicon_img}") @app.on_event("startup") # TODO: use lifespan events for startup/shutdown tasks async def ensure_admin_bootstrap() -> None: settings = get_settings() admin_settings = settings.admin_bootstrap_settings() pricing_metadata = settings.pricing_metadata() try: # Ensure DB schema/types/seeds required for bootstrapping exist. # The initializer is idempotent and safe to run on every startup. try: init_db_script() except Exception: logger.exception( "DB initializer failed; continuing to bootstrap (non-fatal)") role_result, admin_result = bootstrap_admin(settings=admin_settings) pricing_result = bootstrap_pricing_settings(metadata=pricing_metadata) logger.info( "Admin bootstrap completed: roles=%s created=%s updated=%s rotated=%s assigned=%s", role_result.ensured, admin_result.created_user, admin_result.updated_user, admin_result.password_rotated, admin_result.roles_granted, ) # Avoid accessing ORM-managed attributes that may be detached outside # of the UnitOfWork/session scope. Attempt a safe extraction and # fall back to minimal logging if attributes are unavailable. try: seed = pricing_result.seed slug = getattr(seed.settings, "slug", None) if seed and getattr( seed, "settings", None) else None created = getattr(seed, "created", None) updated_fields = getattr(seed, "updated_fields", None) impurity_upserts = getattr(seed, "impurity_upserts", None) logger.info( "Pricing settings bootstrap completed: slug=%s created=%s updated_fields=%s impurity_upserts=%s projects_assigned=%s", slug, created, updated_fields, impurity_upserts, pricing_result.projects_assigned, ) except Exception: logger.info( "Pricing settings bootstrap completed (partial): projects_assigned=%s", pricing_result.projects_assigned, ) except Exception: # pragma: no cover - defensive logging logger.exception( "Failed to bootstrap administrator or pricing settings") app.include_router(dashboard_router) app.include_router(calculations_router) app.include_router(auth_router) app.include_router(imports_router) app.include_router(exports_router) app.include_router(projects_router) app.include_router(scenarios_router) app.include_router(reports_router) app.include_router(ui_router) app.include_router(monitoring_router) app.mount("/static", StaticFiles(directory="static"), name="static")