import asyncio import os from contextlib import asynccontextmanager from datetime import datetime, timezone from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from app.api.v1 import api_v1_router from app.core.config import get_settings from app.core.database import async_session_maker, engine from app.core.limiter import limiter from app.core.logging_middleware import DebugLoggingMiddleware from app.core.middleware import SecurityHeadersMiddleware from app.core.seed import seed_database from app.services.debug_service import DebugService settings = get_settings() @asynccontextmanager async def lifespan(app: FastAPI): # Seed database on startup (idempotent — skips if data exists) try: async with async_session_maker() as db: await seed_database(db) except Exception: pass # DB may not be ready yet, seed will run on next restart # Auto-cleanup logs on startup try: async with async_session_maker() as db: svc = DebugService(db) await svc.auto_cleanup_if_needed() except Exception: pass # Sync bot commands on startup try: async with async_session_maker() as db: from app.integrations.telegram.sync_service import TelegramBotSyncService from app.integrations.telegram.client import TelegramBotClient bot_client = TelegramBotClient(token=settings.telegram_bot_token) svc = TelegramBotSyncService(db, bot_client) await svc.full_sync() except Exception: pass # Auto-run QATesterAgent after fresh deploy (within 5 min) async def _auto_qa(): try: deploy_stamp = "/opt/voidea/.last_deploy" if not os.path.exists(deploy_stamp): return with open(deploy_stamp) as f: ts = int(f.read().strip()) if (datetime.now().timestamp() - ts) > 300: return # older than 5 min await asyncio.sleep(3) async with async_session_maker() as db: from app.agents.qa_tester_agent import QATesterAgent agent = QATesterAgent(session=db) result = await agent.run({"action": "api", "test_count": 2}) if not result.success: logger = __import__("logging").getLogger("voidea.qa_auto") logger.warning("Auto-QA after deploy: %s", result.message) except Exception: pass asyncio.create_task(_auto_qa()) yield await engine.dispose() app = FastAPI( title=settings.project_name, description="VoIdeaAI — идеи рождаются вслух, решения приходят мгновенно!", version=settings.project_version, docs_url="/docs", redoc_url="/redoc", openapi_url="/openapi.json", lifespan=lifespan, ) app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) app.include_router(api_v1_router) app.mount("/assets", StaticFiles(directory="webui/dist/assets"), name="assets") app.mount("/icons", StaticFiles(directory="webui/dist/icons"), name="icons") app.mount("/", StaticFiles(directory="webui/dist", html=True), name="static") app.add_middleware(SecurityHeadersMiddleware) app.add_middleware(DebugLoggingMiddleware) app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origins_list, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.get("/health") @limiter.limit("30/minute") async def health_check(request: Request): return { "status": "healthy", "version": settings.project_version, "environment": settings.project_env, } @app.get("/api/v1/health") @limiter.limit("30/minute") async def api_health(request: Request): return { "status": "healthy", "api_version": "v1", "database": "connected", "timestamp": datetime.now(timezone.utc).isoformat(), }