"""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 label: str = "Jetzt updaten" @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" rueckweg_art: str | None = None # Kurzform für die Oberfläche: „snapshot“, „sicherung“ oder „keiner“ platte: dict | None = None # {"belegt", "gesamt"} in Bytes, wo bekannt (laufende Container) 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