""" 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. Seit dem Box-Wart-Umbau (09/2026) ist MC2 nur noch Updater, Wächter und Modell-Radar. Ideen, Wissen, Chronik, Skills, Verbinden und die Konsole sind raus; die Schnittstellen, die Lucy-Desktop, OpenChamber und Hermes brauchen (Sprache, /v1, MCP-Werkzeuge), bleiben. """ import asyncio import logging import os from collections.abc import AsyncGenerator, Awaitable, Callable from contextlib import asynccontextmanager from typing import Any import httpx from config import FRONTEND_DIST, V1_UPSTREAM, VERSION from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from routers import ( agent, boxwart, events, gateway_proxy, health, hermes_ui, maintenance, models, routing, system, voice, zeitmaschine, ) from routers import reminders as reminders_router from services import metrics_history, reminders, warmer from starlette.requests import Request # 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) -> AsyncGenerator[None, None]: """Hintergrund-Tasks an den App-Lebenszyklus binden. Der Wächter läuft NICHT hier, sondern im mc2-steward (eigener Prozess, steward.py) — ein totes MC2 könnte sich sonst nicht selbst melden.""" tasks: list[asyncio.Task[Any]] = [] 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, ) tasks.append(asyncio.create_task(reminders.reminders_loop())) # 24-h-Metrik-Verlauf (Cockpit-Zeitachse): 10-s-Sampler, Ringpuffer, persistiert. tasks.append(asyncio.create_task(metrics_history.sampler_loop())) # Geteilter HTTP-Client zur lokalen Engine: Keep-Alive/Connection-Pooling statt neuer Client # pro /v1-Anfrage (spart Sockets/TIME_WAIT unter parallelen Agent-Strömen von Zed/Kilo). app.state.gw_client = httpx.AsyncClient( timeout=httpx.Timeout(connect=10.0, read=None, write=None, pool=10.0), limits=httpx.Limits(max_keepalive_connections=100, max_connections=200), ) try: yield finally: for task in tasks: _ = task.cancel() await app.state.gw_client.aclose() 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: Callable[[Request], Awaitable[httpx.Response]] ) -> httpx.Response: 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(boxwart.router) # Cockpit: Start, Hinweise, Modell-Nutzung, Zeitplan app.include_router(models.router) app.include_router(routing.router) app.include_router(system.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 # /v1-Datenpfad: Nach dem Gateway-Auszug (UMBAU v3 P1) läuft der eigentliche Gateway als # eigener Prozess (mc2-gateway, Loopback :9010) — MC2 reicht /v1 dann nur roh durch, damit # LAN-Clients (IDE-Lane) weiter über :9001 kommen. Ohne MC_V1_UPSTREAM (vor dem ersten # Deploy der neuen Unit / nach Rollback) bedient MC2 /v1 wie bisher selbst. if V1_UPSTREAM: from routers import gateway_forward app.include_router(gateway_forward.router) # dünner Roh-Weiterleiter → mc2-gateway else: app.include_router(gateway_proxy.router) # OpenAI-kompatibler /v1-Gateway (model:auto) app.include_router(maintenance.router) app.include_router(events.router) # SSE-Strom /api/stream — Messwerte + Invalidation app.include_router(zeitmaschine.router) # Sicherungen ansehen + zurückspielen (detached) app.include_router( hermes_ui.router ) # Eingebaute Hermes-Web-GUI (hermes dashboard) same-origin unter /hermes-ui/ — VOR dem SPA-Catch-all # 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") _DIST_ROOT = FRONTEND_DIST.resolve() @app.get("/{full_path:path}") def spa(full_path: str): # Datei direkt aus FRONTEND_DIST ausliefern (manifest.webmanifest, favicon.ico, …) — aber # NUR innerhalb des dist-Ordners: Pfad auflösen + Traversal (../, absolute Pfade) hart raus. try: target = (FRONTEND_DIST / full_path).resolve() if target.is_relative_to(_DIST_ROOT) and target.is_file(): return FileResponse(target) except (ValueError, OSError): pass 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"}