Ampel / ampel (push) Failing after 21s
Suche aus Merkliste (deploy/radar-watchlist.json) und Hugging-Face-Entdeckung; nur Kandidaten mit Bild-Projektor, die neben das Warm-Set passen (<= 115 GB inkl. KV-Cache). Nachtlauf mc2-radar.timer 00:30, Tests nur bis 02:30 (um 03:00 kommt NerdQuiz), hoechstens ein neuer Kandidat pro Woche, Notbremse 02:35, RuntimeMaxSec als letzte Sicherung. Pruefstand als Modul (deploy/bench/pruefstand.py): Tempo, Werkzeuge, Deutsch/JSON bzw. Programmieraufgaben und Bild-Probe gegen das heutige Modell der Rolle. Durchgefallene werden geloescht, Bestandene gemeldet und erst nach "Uebernehmen" getauscht. Beim Uebernehmen wandern die Zweitrollen mit (fast beim Hirn, heavy beim Coder), und das Warm-Set des Stewards wird selbst umgestellt statt als Handgriff zu bleiben. Modell-Code im Pruefstand darf keine Prozesse starten (RLIMIT_NPROC=0). Radar steht im Flugplan und unter Waechter-Aufsicht; deploy.sh spielt seine Units ein. Oberflaeche: Vergleichswerte je Kandidat, Rueckfrage vor Uebernehmen und Verwerfen, Status auf Deutsch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
168 lines
6.7 KiB
Python
168 lines
6.7 KiB
Python
"""
|
|
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, HTTPException
|
|
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,
|
|
radar,
|
|
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,
|
|
)
|
|
# Probelauf (neue Fassung neben der laufenden, Box-Wart-Umbau 09/2026): Erinnerungen und
|
|
# Messverlauf gehören dem echten MC2 — zwei Schreiber würden Erinnerungen doppelt feuern.
|
|
if os.environ.get("MC_PROBELAUF", "") != "1":
|
|
tasks.append(asyncio.create_task(reminders.reminders_loop()))
|
|
# 24-h-Metrik-Verlauf: 10-s-Sampler, Ringpuffer, persistiert.
|
|
tasks.append(asyncio.create_task(metrics_history.sampler_loop()))
|
|
else:
|
|
log.info("Probelauf: Erinnerungen und Messverlauf bleiben beim echten MC2.")
|
|
# 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(radar.router) # Modell-Radar: Kandidaten, Nachttests, Übernehmen/Verwerfen
|
|
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):
|
|
# Unbekannte /api-Pfade sind ein echter 404, keine Startseite: Sonst bekommt die
|
|
# Oberfläche HTML statt JSON und stürzt ab (erster Probelauf 23.09., /api/radar).
|
|
if full_path == "api" or full_path.startswith("api/"):
|
|
raise HTTPException(status_code=404, detail="Diese Schnittstelle gibt es nicht.")
|
|
# 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"}
|