phase2a: Kern fuer zwei Instanzen (Rolle, Partner, Telegram-Zweitweg)
- kern/einstellungen.py: MC_ROLLE (box|homelab), Instanzname, Datenordner, Partner-URL - kern/zeit.py: eine Zeitzone statt sechs Kopien - app.py/steward.py: Box-Router und Warm-Set nur in der Rolle box; der Homelab-Teil laedt nichts von der Box - /api/health nennt Rolle und Instanz; Engine/Gateway/Hirn nur auf der Box - /api/partner (Lebenszeichen) und /api/partner/<pfad> (Durchreiche, ohne Schleifen und SSE) - Waechter prueft die andere Instanz (rot erst nach 5 Takten, ein Neustart ist kein Alarm) - notify.sh: Zweitweg direkt an die Telegram-Bot-API, wenn hermes send scheitert oder fehlt; Token nicht in der Prozessliste, Tests greifen nie auf die echte .env - update_verlauf erkennt "OK telegram direkt" Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
99ba6a78ab
commit
75611be9a9
+68
-54
@@ -1,13 +1,16 @@
|
||||
"""
|
||||
Mission Control 2.0 — dünner FastAPI-Einstieg.
|
||||
Homelab Orchestrator (vormals 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.
|
||||
Zwei Rollen, derselbe Code (MC_ROLLE, kern/einstellungen.py, seit 24.09.2026):
|
||||
box — die KI-Box: Box-Wart (Updater, Wächter, Modell-Radar) plus die Schnittstellen,
|
||||
die Lucy-Desktop, OpenChamber und Hermes brauchen (Sprache, /v1, MCP-Werkzeuge)
|
||||
homelab — der Proxmox-PC: der Homelab-Teil
|
||||
Beide liefern dieselbe Oberfläche aus und reichen den Bereich der anderen Instanz unter
|
||||
/api/partner/… durch.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -23,25 +26,13 @@ from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from routers import (
|
||||
boxwart,
|
||||
events,
|
||||
gateway_proxy,
|
||||
health,
|
||||
hermes_ui,
|
||||
maintenance,
|
||||
models,
|
||||
radar,
|
||||
routing,
|
||||
system,
|
||||
voice,
|
||||
zeitmaschine,
|
||||
)
|
||||
from routers import reminders as reminders_router
|
||||
from services import reminders, warmer
|
||||
from kern.einstellungen import einstellungen
|
||||
from routers import health, partner
|
||||
from services.herkunft import erlaubt
|
||||
from starlette.requests import Request
|
||||
|
||||
ROLLE = einstellungen().rolle
|
||||
|
||||
# Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration
|
||||
# für alle Module (logging.getLogger(__name__)).
|
||||
logging.basicConfig(
|
||||
@@ -51,11 +42,11 @@ logging.basicConfig(
|
||||
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."""
|
||||
def _box_hintergrund() -> list[asyncio.Task[Any]]:
|
||||
"""Hintergrund-Tasks der KI-Box. 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."""
|
||||
from services import reminders, warmer
|
||||
|
||||
tasks: list[asyncio.Task[Any]] = []
|
||||
if warmer.ENABLED:
|
||||
tasks.append(asyncio.create_task(warmer.rewarm_loop()))
|
||||
@@ -69,6 +60,13 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
tasks.append(asyncio.create_task(reminders.reminders_loop()))
|
||||
else:
|
||||
log.info("Probelauf: Erinnerungen bleiben beim echten MC2.")
|
||||
return tasks
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
"""Hintergrund-Tasks an den App-Lebenszyklus binden."""
|
||||
tasks = _box_hintergrund() if ROLLE == "box" else []
|
||||
# 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(
|
||||
@@ -83,7 +81,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
await app.state.gw_client.aclose()
|
||||
|
||||
|
||||
app = FastAPI(title="Mission Control 2.0", version=VERSION, lifespan=lifespan)
|
||||
app = FastAPI(title="Homelab Orchestrator", version=VERSION, lifespan=lifespan)
|
||||
|
||||
# Dev: Vite-Dev-Server (5173) ruft das Backend per /api auf.
|
||||
app.add_middleware(
|
||||
@@ -99,7 +97,7 @@ app.add_middleware(
|
||||
@app.middleware("http")
|
||||
async def herkunft(request: Request, call_next: Callable[[Request], Awaitable[httpx.Response]]):
|
||||
if not erlaubt(request.method, request.url.path, request.headers.get("origin"), request.headers.get("host")):
|
||||
return JSONResponse({"detail": "Diese Aktion geht nur von der Box-Wart-Seite selbst aus."}, status_code=403)
|
||||
return JSONResponse({"detail": "Diese Aktion geht nur von der Orchestrator-Seite selbst aus."}, status_code=403)
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
@@ -113,34 +111,50 @@ async def 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)
|
||||
def _box_router(app: FastAPI) -> None:
|
||||
"""Die Schnittstellen der KI-Box (Box-Wart und alles, was Lucy, OpenChamber und Hermes brauchen)."""
|
||||
from routers import (
|
||||
boxwart,
|
||||
events,
|
||||
gateway_proxy,
|
||||
hermes_ui,
|
||||
maintenance,
|
||||
models,
|
||||
radar,
|
||||
routing,
|
||||
system,
|
||||
voice,
|
||||
zeitmaschine,
|
||||
)
|
||||
from routers import reminders as reminders_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
|
||||
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(voice.router) # Sprache: STT/TTS-Proxy + Hermes-Agent-Chat
|
||||
app.include_router(reminders_router.router) # Erinnerungen/Routinen — 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)
|
||||
# Eingebaute Hermes-Web-GUI (hermes dashboard) same-origin unter /hermes-ui/ — VOR dem SPA-Catch-all
|
||||
app.include_router(hermes_ui.router)
|
||||
|
||||
|
||||
app.include_router(health.router)
|
||||
app.include_router(partner.router) # zweite Instanz: Lebenszeichen + Durchreiche
|
||||
if ROLLE == "box":
|
||||
_box_router(app)
|
||||
|
||||
|
||||
# Prod: gebautes Frontend ausliefern (falls vorhanden). SPA-Fallback auf index.html.
|
||||
|
||||
Reference in New Issue
Block a user