""" Mission Control 2.0 — dünner FastAPI-Einstieg. Hängt die Router ein, liefert (in Prod) das gebaute React-Frontend aus und setzt eine no-cache-Middleware. Im Dev läuft das Frontend über den Vite-Dev- Server (proxyt /api hierher), daher CORS für localhost offen. """ import asyncio import logging import os from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from starlette.requests import Request from config import FRONTEND_DIST, VERSION from routers import agent, connect, gateway_proxy, health, maintenance, memory, models, reminders as reminders_router, routing, system, voice from services import reminders, sentry, warmer # Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration # für alle Module (logging.getLogger(__name__)). logging.basicConfig( level=os.environ.get("MC_LOG_LEVEL", "INFO").upper(), format="%(asctime)s %(levelname)-7s %(name)s: %(message)s", ) log = logging.getLogger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): """Hintergrund-Tasks an den App-Lebenszyklus binden: Re-Warm-Wächter fürs Agent-Hirn + Health-Wächter (meldet Ausfälle/Erholung in den Lucy-Briefkasten und auf Telegram).""" tasks = [] if warmer.ENABLED: tasks.append(asyncio.create_task(warmer.rewarm_loop())) log.info("Hirn-Re-Warm-Wächter aktiv (Intervall %ss, Hirn dynamisch aus Hermes-Config)", warmer.INTERVAL) if sentry.ENABLED: tasks.append(asyncio.create_task(sentry.sentry_loop())) tasks.append(asyncio.create_task(reminders.reminders_loop())) try: yield finally: for task in tasks: task.cancel() app = FastAPI(title="Mission Control 2.0", version=VERSION, lifespan=lifespan) # Dev: Vite-Dev-Server (5173) ruft das Backend per /api auf. app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"], allow_methods=["*"], allow_headers=["*"], ) @app.middleware("http") async def no_cache(request: Request, call_next): resp = await call_next(request) if request.url.path.startswith("/api"): resp.headers["Cache-Control"] = "no-cache" return resp app.include_router(health.router) app.include_router(models.router) app.include_router(routing.router) app.include_router(system.router) app.include_router(connect.router) app.include_router(memory.router) app.include_router(agent.router) app.include_router(voice.router) # Sprache: STT/TTS-Proxy + Hermes-Agent-Chat (Voice-Tab) app.include_router(reminders_router.router) # Erinnerungen/Routinen (A3) — feuern in den Briefkasten app.include_router(gateway_proxy.router) # OpenAI-kompatibler /v1-Gateway (model:auto) app.include_router(maintenance.router) # Prod: gebautes Frontend ausliefern (falls vorhanden). SPA-Fallback auf index.html. if FRONTEND_DIST.exists(): app.mount("/assets", StaticFiles(directory=FRONTEND_DIST / "assets"), name="assets") @app.get("/{full_path:path}") def spa(full_path: str): # Falls die Datei direkt in FRONTEND_DIST liegt (z.B. manifest.webmanifest, favicon.ico), liefere sie aus target = FRONTEND_DIST / full_path if target.is_file(): return FileResponse(target) index = FRONTEND_DIST / "index.html" if index.exists(): # index.html nie cachen → Browser zieht nach jedem Deploy das aktuelle (gehashte) Bundle. return FileResponse(index, headers={"Cache-Control": "no-cache, must-revalidate"}) return {"detail": "frontend not built"}