- 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>
60 lines
2.7 KiB
Python
60 lines
2.7 KiB
Python
"""Ziele und Bausteine (Phase 2, 24.09.2026): ein Modell für alles, was Updates bekommt.
|
|
|
|
Ein Ziel ist ein Gerät: die KI-Box, der Proxmox-Host, ein Container, die Arcane-VM. Es besteht aus
|
|
Bausteinen (Betriebssystem, Motor, die App im Container, Docker-Images …). Jeder Baustein sagt, wie
|
|
sein Stand ist — neu, aktuell, unbekannt (mit Grund) oder festgehalten — und wie man ihn einspielt.
|
|
|
|
Die KI-Box meldet ihre Bausteine über den Box-Adapter (services/box_updates.py), der Homelab-Teil
|
|
ab Phase 3 über Proxmox- und Arcane-Adapter. So wird aus beiden Instanzen EINE Liste offener Updates
|
|
(User-Entscheid 24.09.: eine Anlaufstelle für Updates im ganzen Homelab).
|
|
"""
|
|
|
|
from dataclasses import asdict, dataclass, field
|
|
from typing import Literal
|
|
|
|
Zustand = Literal["neu", "aktuell", "unbekannt", "festgehalten", "wird-geprueft"]
|
|
# Reihenfolge für die Anzeige: was Aufmerksamkeit braucht, zuerst.
|
|
_RANG = {"unbekannt": 0, "festgehalten": 1, "neu": 2, "wird-geprueft": 3, "aktuell": 4}
|
|
|
|
|
|
@dataclass
|
|
class Aktion:
|
|
"""Wie ein Update angestoßen wird: eine Schnittstelle der Instanz, die das Ziel pflegt."""
|
|
methode: str # "POST"
|
|
pfad: str # "/api/maintenance/engine-update"
|
|
frage: str # Rückfrage vor dem Klick
|
|
|
|
|
|
@dataclass
|
|
class BausteinStand:
|
|
id: str # "engine"
|
|
name: str # "Motor (llama.cpp)"
|
|
zustand: Zustand
|
|
installiert: str | None = None
|
|
verfuegbar: str | None = None
|
|
kurz: str = "" # „10 Pakete", „b11160 → b11172", „743 Änderungen"
|
|
grund: str | None = None # bei „unbekannt" und „festgehalten": warum
|
|
aktion: Aktion | None = None # None = kein Knopf (aktuell, festgehalten, unbekannt)
|
|
|
|
|
|
@dataclass
|
|
class ZielStand:
|
|
id: str # "ki-box"
|
|
name: str # "KI-Box"
|
|
art: str # "ki-box" | "proxmox-host" | "container" | "vm"
|
|
instanz: str # welche Instanz es pflegt: "box" | "homelab"
|
|
erreichbar: bool | None = None # None = nicht geprüft
|
|
bausteine: list[BausteinStand] = field(default_factory=list)
|
|
geprueft: float | None = None # Unix-Sekunden der letzten Prüfung
|
|
rueckweg: str | None = None # „Sicherung vor jedem Lauf", „Snapshot", „nur Backup"
|
|
|
|
def als_dict(self) -> dict:
|
|
daten = asdict(self)
|
|
daten["bausteine"].sort(key=lambda b: _RANG.get(b["zustand"], 9))
|
|
zaehlung: dict[str, int] = {}
|
|
for b in self.bausteine:
|
|
zaehlung[b.zustand] = zaehlung.get(b.zustand, 0) + 1
|
|
daten["zaehlung"] = zaehlung
|
|
daten["offen"] = zaehlung.get("neu", 0)
|
|
return daten
|