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
@@ -0,0 +1,3 @@
|
||||
"""Kern des Homelab Orchestrators (Phase 2, ab 24.09.2026): was beide Instanzen teilen —
|
||||
Einstellungen, Zeitzone, Partner-Instanz. Neue Module lesen ihre Einstellungen hier; ältere
|
||||
Module ziehen nach und nach um."""
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Zentrale Einstellungen einer Instanz des Homelab Orchestrators.
|
||||
|
||||
Zwei Rollen, derselbe Code (User-Entscheid 24.09.2026, „zwei Instanzen, eine Oberfläche“):
|
||||
box — auf der KI-Box: Box-Wart (Updates, Wächter, Modelle, Radar, Gateway für Lucy)
|
||||
homelab — auf dem Proxmox-PC: Homelab-Teil (Proxmox-Host, Container, Arcane)
|
||||
Beide kennen die jeweils andere Instanz als Partner: die Oberfläche zeigt beide Bereiche, und
|
||||
jede Instanz prüft, ob die andere noch antwortet.
|
||||
"""
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
ROLLEN = ("box", "homelab")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Einstellungen:
|
||||
rolle: str # "box" | "homelab"
|
||||
instanz: str # Anzeigename dieser Instanz
|
||||
daten_dir: Path # Zustandsdateien dieser Instanz
|
||||
partner_url: str # Basis-URL der anderen Instanz, leer = keine
|
||||
partner_name: str # Anzeigename der anderen Instanz
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def einstellungen() -> Einstellungen:
|
||||
rolle = os.environ.get("MC_ROLLE", "box").strip().lower() or "box"
|
||||
if rolle not in ROLLEN:
|
||||
raise ValueError(f"MC_ROLLE muss eine von {ROLLEN} sein, nicht {rolle!r}.")
|
||||
box = rolle == "box"
|
||||
standard_daten = os.environ.get("MC_MODELS_DIR", "/srv/models") if box else "/var/lib/mc2"
|
||||
return Einstellungen(
|
||||
rolle=rolle,
|
||||
instanz=os.environ.get("MC_INSTANZ", "Box-Wart" if box else "Homelab"),
|
||||
daten_dir=Path(os.environ.get("MC_DATEN_DIR", standard_daten)),
|
||||
partner_url=os.environ.get("MC_PARTNER_URL", "").strip().rstrip("/"),
|
||||
partner_name=os.environ.get("MC_PARTNER_NAME", "Homelab" if box else "Box-Wart"),
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Die Partner-Instanz (User-Entscheid 24.09.2026: zwei Instanzen, eine Oberfläche).
|
||||
|
||||
Box-Wart läuft auf der KI-Box, der Homelab-Teil in einem Container auf dem Proxmox-PC. Jede
|
||||
Instanz kennt die andere über MC_PARTNER_URL. Sie fragt deren Lebenszeichen ab (für den Wächter
|
||||
und die Kopfzeile der Oberfläche) und reicht Anfragen der Oberfläche unter /api/partner/… durch,
|
||||
damit eine Seite beide Bereiche zeigt — egal, auf welcher Instanz man sie öffnet.
|
||||
"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
import httpx
|
||||
|
||||
from kern.einstellungen import einstellungen
|
||||
|
||||
ZEITLIMIT_S = 5.0
|
||||
CACHE_S = 15.0
|
||||
|
||||
_lock = threading.Lock()
|
||||
_cache: dict = {"ts": 0.0, "daten": None}
|
||||
|
||||
|
||||
def _kurz(exc: Exception) -> str:
|
||||
if isinstance(exc, httpx.TimeoutException):
|
||||
return "antwortet nicht rechtzeitig"
|
||||
if isinstance(exc, httpx.ConnectError):
|
||||
return "nicht erreichbar"
|
||||
if isinstance(exc, httpx.HTTPStatusError):
|
||||
return f"antwortet mit HTTP {exc.response.status_code}"
|
||||
return f"{exc.__class__.__name__}: {exc}"[:160]
|
||||
|
||||
|
||||
def status(frisch: bool = False) -> dict:
|
||||
"""Lebenszeichen der Partner-Instanz. Ohne MC_PARTNER_URL: {"eingerichtet": False}."""
|
||||
e = einstellungen()
|
||||
if not e.partner_url:
|
||||
return {"eingerichtet": False}
|
||||
with _lock:
|
||||
if not frisch and _cache["daten"] and time.time() - _cache["ts"] < CACHE_S:
|
||||
return dict(_cache["daten"])
|
||||
daten: dict = {"eingerichtet": True, "name": e.partner_name, "url": e.partner_url,
|
||||
"erreichbar": False, "rolle": None, "instanz": None, "version": None, "fehler": None}
|
||||
try:
|
||||
r = httpx.get(f"{e.partner_url}/api/health", timeout=ZEITLIMIT_S)
|
||||
r.raise_for_status()
|
||||
h = r.json()
|
||||
daten.update(erreichbar=True, rolle=h.get("rolle"), instanz=h.get("instanz"), version=h.get("version"))
|
||||
except Exception as exc:
|
||||
daten["fehler"] = _kurz(exc)
|
||||
daten["geprueft"] = time.time()
|
||||
with _lock:
|
||||
_cache.update(ts=time.time(), daten=daten)
|
||||
return dict(daten)
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Die eine Zeitzone für alles, was Zeiten anzeigt oder naive Zeiten deutet (Projektregel:
|
||||
nie datetime.now() ohne Zone). Vorher stand dieselbe Zeile in sieben Modulen."""
|
||||
|
||||
import os
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
LOCAL_TZ = ZoneInfo(os.environ.get("MC_LOCAL_TZ", "Europe/Berlin"))
|
||||
Reference in New Issue
Block a user