""" App Service - FastAPI Backend Main application entry point. """ from pathlib import Path from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse, FileResponse from fastapi.staticfiles import StaticFiles import logging import time from app.config import settings from app.api.v1 import router as api_v1_router # from app.api.websocket import router as websocket_router # TODO: Create later from app.db.session import engine from app.db.base import Base # Static files path STATIC_DIR = Path(__file__).parent.parent / "static" # Configure logging logging.basicConfig( level=getattr(logging, settings.LOG_LEVEL.upper()), format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # Create FastAPI application app = FastAPI( title=settings.APP_NAME, version=settings.APP_VERSION, description="Modern web application for service management", docs_url="/docs", redoc_url="/redoc", openapi_url=f"{settings.API_V1_PREFIX}/openapi.json" ) # CORS middleware app.add_middleware( CORSMiddleware, allow_origins=settings.all_cors_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Request logging middleware @app.middleware("http") async def log_requests(request: Request, call_next): """Log all incoming requests with timing information.""" start_time = time.time() # Process request response = await call_next(request) # Calculate duration process_time = time.time() - start_time # Log request logger.info( f"{request.method} {request.url.path} " f"completed in {process_time:.3f}s " f"with status {response.status_code}" ) # Add timing header response.headers["X-Process-Time"] = str(process_time) return response # Exception handlers @app.exception_handler(Exception) async def global_exception_handler(request: Request, exc: Exception): """Handle uncaught exceptions.""" logger.error(f"Unhandled exception: {exc}", exc_info=True) return JSONResponse( status_code=500, content={ "detail": "Internal server error" if not settings.DEBUG else str(exc) } ) # API routers app.include_router(api_v1_router, prefix=settings.API_V1_PREFIX) # app.include_router(websocket_router) # TODO: Add WebSocket router # Health check endpoint @app.get("/health", tags=["System"]) async def health_check(): """Health check endpoint for monitoring.""" return { "status": "healthy", "app_name": settings.APP_NAME, "version": settings.APP_VERSION } # Root endpoint @app.get("/", tags=["System"]) async def root(): """Root endpoint - serve frontend or API info.""" # If frontend exists, serve it index_path = STATIC_DIR / "index.html" if index_path.exists(): return FileResponse(index_path) # Otherwise return API info return { "message": "App Service API", "version": settings.APP_VERSION, "docs": "/docs", "api": settings.API_V1_PREFIX } # Startup event @app.on_event("startup") async def startup_event(): """Run on application startup.""" logger.info(f"Starting {settings.APP_NAME} v{settings.APP_VERSION}") logger.info(f"Debug mode: {settings.DEBUG}") logger.info(f"Database: {settings.DATABASE_URL.split('@')[1] if '@' in settings.DATABASE_URL else 'configured'}") # Create tables if they don't exist (for development) # In production, use Alembic migrations if settings.DEBUG: logger.info("Creating database tables (debug mode)") Base.metadata.create_all(bind=engine) # Seed default settings from registry from sqlalchemy.orm import Session from app import crud from app.core.settings_registry import get_database_defaults with Session(engine) as db: # Get all database settings with defaults from registry default_settings = get_database_defaults() for key, value in default_settings.items(): if crud.settings.get_setting(db, key) is None: logger.info(f"Seeding default setting: {key}={value}") crud.settings.update_setting(db, key, value) # Shutdown event @app.on_event("shutdown") async def shutdown_event(): """Run on application shutdown.""" logger.info(f"Shutting down {settings.APP_NAME}") # Serve static files (frontend) - must be after API routes if STATIC_DIR.exists(): # Mount static assets (JS, CSS, images) app.mount("/assets", StaticFiles(directory=STATIC_DIR / "assets"), name="assets") # SPA catch-all: serve index.html for all non-API routes @app.get("/{full_path:path}", include_in_schema=False) async def serve_spa(request: Request, full_path: str): """Serve the SPA for all non-API routes.""" # Skip API routes if full_path.startswith("api/") or full_path in ["docs", "redoc", "health", "openapi.json"]: return JSONResponse(status_code=404, content={"detail": "Not found"}) # Check if file exists in static dir file_path = STATIC_DIR / full_path if file_path.exists() and file_path.is_file(): return FileResponse(file_path) # Serve index.html for SPA routing index_path = STATIC_DIR / "index.html" if index_path.exists(): return FileResponse(index_path) return JSONResponse(status_code=404, content={"detail": "Not found"}) if __name__ == "__main__": import uvicorn uvicorn.run( "app.main:app", host="0.0.0.0", port=8000, reload=settings.DEBUG, log_level=settings.LOG_LEVEL.lower() )