"""FluentGerman.ai — FastAPI application entry point.""" import logging import time from contextlib import asynccontextmanager from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from sqlalchemy import select from app.auth import hash_password from app.config import get_settings from app.database import Base, engine, async_session from app.models import User from app.routers import auth, chat, instructions, users, voice # ── Logging ────────────────────────────────────────────────────────── import os os.makedirs("logs", exist_ok=True) logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", handlers=[ logging.StreamHandler(), logging.FileHandler("logs/app.log", mode="a"), ], ) logger = logging.getLogger("fluentgerman") @asynccontextmanager async def lifespan(app: FastAPI): """Create tables and bootstrap admin user on startup.""" async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) # Bootstrap admin if not exists settings = get_settings() async with async_session() as db: result = await db.execute(select(User).where(User.is_admin == True)) # noqa: E712 if not result.scalar_one_or_none(): admin = User( username=settings.admin_username, email=settings.admin_email, hashed_password=hash_password(settings.admin_password), is_admin=True, ) db.add(admin) await db.commit() logger.info("Admin user created: %s", settings.admin_username) logger.info("FluentGerman.ai started — LLM: %s/%s, Voice: %s", settings.llm_provider, settings.llm_model, settings.voice_mode) yield logger.info("FluentGerman.ai shutting down") app = FastAPI( title="FluentGerman.ai", description="Personalized LLM-powered German language learning platform", version="1.0.0", lifespan=lifespan, ) # ── Request logging middleware ──────────────────────────────────────── @app.middleware("http") async def log_requests(request: Request, call_next): start = time.time() response = await call_next(request) duration = round((time.time() - start) * 1000) # Skip logging static file requests to keep logs clean path = request.url.path if path.startswith("/api/"): logger.info("%s %s → %s (%dms)", request.method, path, response.status_code, duration) return response # CORS — restrict in production app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # API routers app.include_router(auth.router) app.include_router(users.router) app.include_router(instructions.router) app.include_router(chat.router) app.include_router(voice.router) # Serve frontend static files app.mount("/", StaticFiles(directory="../frontend", html=True), name="frontend")