phase2d: gemeinsames Ziel-Modell, KI-Box als erster Adapter, strukturierter Update-Verlauf
- kern/ziele.py: Ziel -> Bausteine mit Stand (neu/aktuell/unbekannt/festgehalten/wird-geprueft), Versionen und dem Knopf, der das Update anstoesst; gleiches Modell fuer Box und Homelab - services/box_updates.py: Update-Zwischenspeicher aus dem Router geholt, ki_box_ziel() als Box-Adapter; GET /api/ziele - Update-Verlauf strukturiert: autoupdate.sh und die Update-Knoepfe schreiben je Baustein eine JSON-Zeile (mc2-update-verlauf.jsonl); die Meldungstexte bleiben gleich, aeltere Laeufe kommen weiter aus dem Meldeprotokoll. Updates per Knopf erscheinen jetzt auch im Verlauf. - jobengine: Abschluss-Haken fuer jedes Ende (neben der Nacharbeit fuer den Erfolg) - Sonntags-Lauf setzt seinen Anlass; Hermes-Job aus dem Lauf schreibt nicht doppelt - Vertragstest Bash-Schreiber gegen Python-Leser (laeuft auf der Box mit jq) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
22bb8c672b
commit
589c14d5e9
@@ -0,0 +1,156 @@
|
||||
"""Updates der KI-Box: Zwischenspeicher für den Start-Bildschirm und der Box-Adapter (Phase 2).
|
||||
|
||||
maintenance.updates() fragt GitHub, apt und git und braucht nach einem Neustart gern zehn Sekunden
|
||||
und mehr. Der Start-Bildschirm wartet darauf nie: Er bekommt den letzten Stand (oder zunächst
|
||||
nichts), frisch geholt wird im Hintergrund. Seit 24.09.2026 liegt das hier statt im Router, weil
|
||||
auch der Box-Adapter (ki_box_ziel) davon liest.
|
||||
|
||||
ki_box_ziel() beschreibt die KI-Box im gemeinsamen Modell aus kern/ziele.py: vier Bausteine mit
|
||||
Stand, Versionen und dem Knopf, der das jeweilige Update anstößt.
|
||||
"""
|
||||
|
||||
import platform
|
||||
import threading
|
||||
import time
|
||||
|
||||
from kern.ziele import Aktion, BausteinStand, ZielStand
|
||||
|
||||
from services import maintenance, system, update_verlauf
|
||||
|
||||
_updates_lock = threading.Lock()
|
||||
_updates_cache: dict = {"ts": 0.0, "daten": None, "laeuft": False, "stand": -1}
|
||||
UPDATES_CACHE_S = 600
|
||||
|
||||
NAMEN = {"os": "Betriebssystem", "engine": "Motor (llama.cpp)", "swap": "llama-swap", "hermes": "Hermes"}
|
||||
NAMEN_KURZ = {"os": "Betriebssystem", "engine": "Motor", "swap": "llama-swap", "hermes": "Hermes"}
|
||||
|
||||
AKTIONEN = {
|
||||
"os": Aktion("POST", "/api/maintenance/os-update",
|
||||
"Betriebssystem-Updates jetzt einspielen? Danach wird der Stack geprüft."),
|
||||
"engine": Aktion("POST", "/api/maintenance/engine-update",
|
||||
"Den Motor jetzt aktualisieren? Die Modelle sind dabei kurz weg; scheitert die Prüfung, "
|
||||
"geht es zurück."),
|
||||
"swap": Aktion("POST", "/api/maintenance/swap-update",
|
||||
"llama-swap jetzt aktualisieren? Laufende Antworten brechen ab; scheitert die Prüfung, "
|
||||
"geht es zurück."),
|
||||
"hermes": Aktion("POST", "/api/maintenance/hermes-update",
|
||||
"Hermes jetzt aktualisieren? Lucy ist dabei ein paar Minuten weg."),
|
||||
}
|
||||
|
||||
|
||||
def _holen() -> None:
|
||||
stand = maintenance.update_stand
|
||||
try:
|
||||
daten = maintenance.updates()
|
||||
except Exception:
|
||||
daten = None
|
||||
with _updates_lock:
|
||||
if daten is not None or _updates_cache["daten"] is None:
|
||||
_updates_cache["daten"] = daten or {}
|
||||
_updates_cache["ts"] = time.time()
|
||||
_updates_cache["stand"] = stand
|
||||
_updates_cache["laeuft"] = False
|
||||
|
||||
|
||||
def gecacht() -> dict:
|
||||
"""Letzter Stand der Update-Prüfung; ist er veraltet, wird im Hintergrund neu geholt."""
|
||||
with _updates_lock:
|
||||
# Nach jedem abgeschlossenen Update zählt maintenance.update_stand hoch — dann sofort neu holen,
|
||||
# statt bis zu 10 Minuten „Aktualisieren" für schon eingespielte Updates zu zeigen (24.09.2026).
|
||||
veraltet = (_updates_cache["daten"] is None or time.time() - _updates_cache["ts"] > UPDATES_CACHE_S
|
||||
or _updates_cache["stand"] != maintenance.update_stand)
|
||||
if veraltet and not _updates_cache["laeuft"]:
|
||||
_updates_cache["laeuft"] = True
|
||||
threading.Thread(target=_holen, name="updates-holen", daemon=True).start()
|
||||
return _updates_cache["daten"] or {}
|
||||
|
||||
|
||||
def gelesen() -> bool:
|
||||
"""Liegt schon ein Ergebnis vor (nach einem Neustart dauert die erste Prüfung)?"""
|
||||
return _updates_cache["daten"] is not None
|
||||
|
||||
|
||||
def bausteine(u: dict) -> list[dict]:
|
||||
"""Welche Bausteine haben Neues? In der Reihenfolge, in der auch das Update läuft."""
|
||||
liste = []
|
||||
if u.get("os"):
|
||||
liste.append({"id": "os", "name": "Betriebssystem", "neu": f"{u['os']} Pakete"})
|
||||
if u.get("engine"):
|
||||
liste.append({"id": "engine", "name": "Motor", "neu": "neuer Build"})
|
||||
if u.get("swap"):
|
||||
liste.append({"id": "swap", "name": "llama-swap", "neu": "neue Version"})
|
||||
for c in u.get("components") or []:
|
||||
if c.get("key") == "hermes_agent" and c.get("update"):
|
||||
liste.append({"id": "hermes", "name": "Hermes", "neu": f"{c.get('behind', '?')} Änderungen"})
|
||||
return liste
|
||||
|
||||
|
||||
def unklar(u: dict) -> list[dict]:
|
||||
"""Bausteine, deren Update-Prüfung scheiterte — ehrlich „unbekannt" statt „aktuell"."""
|
||||
return [{"id": k, "name": NAMEN_KURZ.get(k, k), "grund": grund}
|
||||
for k, grund in (u.get("pruef_fehler") or {}).items() if grund]
|
||||
|
||||
|
||||
# --- Box-Adapter ------------------------------------------------------------------------
|
||||
|
||||
def _ubuntu() -> str | None:
|
||||
try:
|
||||
info = platform.freedesktop_os_release()
|
||||
except OSError:
|
||||
return None
|
||||
return " ".join(x for x in (info.get("NAME"), info.get("VERSION_ID")) if x) or None
|
||||
|
||||
|
||||
def _versionen(u: dict) -> dict[str, tuple[str | None, str | None]]:
|
||||
"""(installiert, verfügbar) je Baustein. „Verfügbar" nur, wenn es Neues gibt — die Abfrage
|
||||
dafür ist bei GitHub zwischengespeichert (maintenance._github_json, 15 Minuten)."""
|
||||
engine = maintenance._installed_engine_build()
|
||||
swap = maintenance._installed_swap_version()
|
||||
werte: dict[str, tuple[str | None, str | None]] = {
|
||||
"os": (_ubuntu(), f"{u['os']} Pakete" if u.get("os") else None),
|
||||
"engine": (f"b{engine}" if engine else None, None),
|
||||
"swap": (f"v{swap}" if swap else None, None),
|
||||
}
|
||||
try:
|
||||
if u.get("engine") and (rel := maintenance._latest_engine_asset_release()):
|
||||
werte["engine"] = (werte["engine"][0], str(rel.get("tag_name") or "") or None)
|
||||
if u.get("swap"):
|
||||
rel = maintenance._github_json(f"repos/{maintenance.SWAP_REPO}/releases/latest")
|
||||
tag = str(rel.get("tag_name") or "") if isinstance(rel, dict) else ""
|
||||
werte["swap"] = (werte["swap"][0], tag or None)
|
||||
except Exception:
|
||||
pass
|
||||
hermes = next((c for c in u.get("components") or [] if c.get("key") == "hermes_agent"), {})
|
||||
git = system.find_hermes_agent_git() or {}
|
||||
version = maintenance._hermes_version(git["path"]) if git.get("path") else None
|
||||
werte["hermes"] = (version or hermes.get("current"),
|
||||
f"+{hermes.get('behind')} Änderungen" if hermes.get("update") else None)
|
||||
return werte
|
||||
|
||||
|
||||
def ki_box_ziel() -> ZielStand:
|
||||
"""Die KI-Box als Ziel mit ihren vier Bausteinen."""
|
||||
u = gecacht()
|
||||
ziel = ZielStand(id="ki-box", name="KI-Box", art="ki-box", instanz="box", erreichbar=True,
|
||||
geprueft=_updates_cache["ts"] or None,
|
||||
rueckweg="Sicherung vor jedem Sonntags-Lauf; Motor, llama-swap und Hermes rollen "
|
||||
"bei rotem Check selbst zurück, das Betriebssystem nicht.")
|
||||
if not gelesen():
|
||||
ziel.bausteine = [BausteinStand(id=k, name=n, zustand="wird-geprueft") for k, n in NAMEN.items()]
|
||||
return ziel
|
||||
offen = {b["id"]: b["neu"] for b in bausteine(u)}
|
||||
fehler = {b["id"]: b["grund"] for b in unklar(u)}
|
||||
fest = {p["baustein"]: p for p in update_verlauf.festgehalten()}
|
||||
versionen = _versionen(u)
|
||||
for k, name in NAMEN.items():
|
||||
installiert, verfuegbar = versionen.get(k, (None, None))
|
||||
stand = BausteinStand(id=k, name=name, zustand="aktuell", installiert=installiert, verfuegbar=verfuegbar)
|
||||
if k in fest:
|
||||
p = fest[k]
|
||||
stand.zustand, stand.grund = "festgehalten", f"Seit {p['seit']} auf {p['version']}: {p['grund']}"
|
||||
elif k in fehler:
|
||||
stand.zustand, stand.grund = "unbekannt", fehler[k]
|
||||
elif k in offen:
|
||||
stand.zustand, stand.kurz, stand.aktion = "neu", offen[k], AKTIONEN[k]
|
||||
ziel.bausteine.append(stand)
|
||||
return ziel
|
||||
Reference in New Issue
Block a user