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
@@ -13,15 +13,14 @@ Box-Wart-Schnittstellen (Umbau 09/2026): alles, was der Cockpit-Startbildschirm
|
||||
Dünn: die Logik liegt in services/waechter.py, modell_nutzung.py, zeitplan.py, update_verlauf.py.
|
||||
"""
|
||||
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import psutil
|
||||
from config import MODELS_DIR
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from kern.zeit import LOCAL_TZ
|
||||
from pydantic import BaseModel
|
||||
from services import (
|
||||
aufraeumen,
|
||||
@@ -36,7 +35,6 @@ from services import (
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["boxwart"])
|
||||
LOCAL_TZ = ZoneInfo(os.environ.get("MC_LOCAL_TZ", "Europe/Berlin"))
|
||||
|
||||
_updates_lock = threading.Lock()
|
||||
_updates_cache: dict = {"ts": 0.0, "daten": None, "laeuft": False, "stand": -1}
|
||||
|
||||
+24
-15
@@ -1,9 +1,12 @@
|
||||
"""Health-/Status-Endpoint — schlanker Lebenszeichen-Check für MC 2.0."""
|
||||
"""Health-/Status-Endpoint — schlanker Lebenszeichen-Check einer Instanz.
|
||||
|
||||
Seit 24.09.2026 nennt er auch Rolle und Namen der Instanz: Die Partner-Instanz liest daran ab,
|
||||
wer ihr antwortet. Engine, Gateway und Hirn gibt es nur auf der KI-Box."""
|
||||
|
||||
from config import VERSION
|
||||
from fastapi import APIRouter
|
||||
from kern.einstellungen import einstellungen
|
||||
from pydantic import BaseModel
|
||||
from services import gateway, llamaswap
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
@@ -11,19 +14,25 @@ router = APIRouter(prefix="/api")
|
||||
class HealthResponse(BaseModel):
|
||||
status: str
|
||||
version: str
|
||||
engine_reachable: bool
|
||||
gateway_reachable: bool
|
||||
brain: dict
|
||||
rolle: str
|
||||
instanz: str
|
||||
engine_reachable: bool | None = None
|
||||
gateway_reachable: bool | None = None
|
||||
brain: dict | None = None
|
||||
|
||||
|
||||
@router.get("/health", response_model=HealthResponse)
|
||||
@router.get("/health", response_model=HealthResponse, response_model_exclude_none=True)
|
||||
def health() -> dict:
|
||||
return {
|
||||
"status": "ok",
|
||||
"version": VERSION,
|
||||
"engine_reachable": llamaswap.engine_reachable(),
|
||||
"gateway_reachable": gateway.gateway_reachable(),
|
||||
# Echte Hirn-Bereitschaft: Engine kann erreichbar sein, das Agent-Hirn ('fast') aber tot
|
||||
# (Crash/OOM nach Engine-Update). Das wäre sonst ein silent fail (App-Fehler statt Status).
|
||||
"brain": llamaswap.brain_status(),
|
||||
}
|
||||
e = einstellungen()
|
||||
antwort: dict = {"status": "ok", "version": VERSION, "rolle": e.rolle, "instanz": e.instanz}
|
||||
if e.rolle == "box":
|
||||
from services import gateway, llamaswap
|
||||
|
||||
antwort.update(
|
||||
engine_reachable=llamaswap.engine_reachable(),
|
||||
gateway_reachable=gateway.gateway_reachable(),
|
||||
# Echte Hirn-Bereitschaft: Engine kann erreichbar sein, das Agent-Hirn aber tot
|
||||
# (Crash/OOM nach Engine-Update). Das wäre sonst ein silent fail.
|
||||
brain=llamaswap.brain_status(),
|
||||
)
|
||||
return antwort
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Partner-Instanz: Lebenszeichen und Durchreiche (zwei Instanzen, eine Oberfläche, 24.09.2026).
|
||||
|
||||
Die Oberfläche fragt den Bereich der anderen Instanz über /api/partner/<pfad> ab; hier wird
|
||||
daraus <partner>/api/<pfad>. Die Herkunftsprüfung (services/herkunft.py) greift wie bei jeder
|
||||
anderen schreibenden Anfrage schon hier, bevor etwas weitergereicht wird.
|
||||
"""
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, HTTPException, Request, Response
|
||||
from kern import partner
|
||||
from kern.einstellungen import einstellungen
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["partner"])
|
||||
|
||||
# Nicht durchreichen: den Partner-Weg des Partners (sonst reichen sich zwei Instanzen eine Anfrage
|
||||
# endlos zu) und den Live-Strom (SSE hält die Verbindung offen; der Partner-Bereich fragt ab).
|
||||
_GESPERRT = ("partner", "stream")
|
||||
_ZEITLIMIT = httpx.Timeout(30.0, connect=5.0)
|
||||
|
||||
|
||||
@router.get("/partner")
|
||||
def partner_status() -> dict:
|
||||
return partner.status()
|
||||
|
||||
|
||||
@router.api_route("/partner/{pfad:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE"], include_in_schema=False)
|
||||
async def weiterreichen(pfad: str, request: Request) -> Response:
|
||||
e = einstellungen()
|
||||
if not e.partner_url:
|
||||
raise HTTPException(status_code=404, detail="Es ist keine zweite Instanz eingerichtet.")
|
||||
if pfad.split("/", 1)[0] in _GESPERRT:
|
||||
raise HTTPException(status_code=404, detail="Diese Schnittstelle wird nicht weitergereicht.")
|
||||
kopf = {"X-MC-Von": e.instanz}
|
||||
if request.headers.get("content-type"):
|
||||
kopf["Content-Type"] = request.headers["content-type"]
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=_ZEITLIMIT) as c:
|
||||
r = await c.request(request.method, f"{e.partner_url}/api/{pfad}",
|
||||
params=request.query_params, content=await request.body(), headers=kopf)
|
||||
except httpx.HTTPError:
|
||||
raise HTTPException(status_code=502, detail=f"{e.partner_name} ist gerade nicht erreichbar.") from None
|
||||
return Response(content=r.content, status_code=r.status_code,
|
||||
media_type=r.headers.get("content-type", "application/json"))
|
||||
Reference in New Issue
Block a user