homelab: eigener Reiter „Snapshots“ mit Verwaltung (anlegen, löschen, zurücksetzen)
Die Seite /homelab/snapshots (in der Seitenleiste nach „Updates“: Snapshots sind der Rückweg der Updates) zeigt je Gerät die Snapshots mit Zeitpunkt, Alter, Herkunft (vor Update / per Knopf / von Hand) und Beschreibung, beim PBS die Sicherungen des Orchestrators mit Größe. Knöpfe „Snapshot anlegen“, „Zurücksetzen“ und „Löschen“ gibt es nur für freigegebene Geräte und nur für mc2-Snapshots bzw. eigene Sicherungen; von Hand angelegte bleiben reine Ansicht. Die Rückfragen kommen vom Homelab-Teil (wie im Ziel-Modell); Zurücksetzen sagt vorher, was verloren geht und dass das Gerät neu startet. Am Handy (320–400 px) untereinander, ab md Knöpfe rechts, ab xl fünf Spalten. Entscheide: - Ausführer: kein neuer Befehl. Der Bericht bringt zusätzlich snapshot_details (snaptime, Beschreibung, eigen) und sicherung_details (ctime, Größe); snapshots und sicherungen bleiben unverändert. Keine Belegung je Snapshot — die kennt Proxmox bei LVM-Thin nicht, die Seite sagt das so. „snapshot“ nimmt einen Anlass (update oder knopf) und wählt damit eine feste Beschreibung, damit ein Snapshot auf Knopfdruck nicht „Vor einem Update“ heißt. Bis der neue Ausführer eingespielt ist, liest der Homelab-Teil den Zeitpunkt aus dem Namen. - Aktionen sind Läufe in homelab-laeufe.json (Mechanik aus updates.py): gleiche Sperren (START_SPERRE, je Gerät einer, SAMMELLAUF_SPERRE), Protokoll-Art „snapshot“ mit eigenen Titeln, der Wächter lässt ein Gerät im Zurücksetzen in Ruhe. Zusätzlich keine Aktion, solange irgendwo ein Update läuft: Der Ausführer arbeitet einen Auftrag nach dem anderen ab — hinter einem Update liefe die Aktion in ihr Zeitlimit und käme danach unbeobachtet doch noch dran. - Zurücksetzen prüft danach wie ein Update und meldet immer ([Homelab-Snapshot], bei Rot als Alarm); anlegen und löschen melden nur ihr Scheitern, das Ergebnis steht beim Gerät. Nicht im Update-Verlauf, nicht unter „Zuletzt eingespielt“. - Per Knopf angelegte Snapshots räumt kein grünes Update weg (updates._aufraeumen fragt snapshots.knopf_namen). - speicher.py: alle Snapshots in einem Vorschlag mit Verweis auf die Seite statt einer je Snapshot. - Nebenbei: Der Update-Verlauf stürzte bei einem Ergebnis ohne Eintrag ab (etwa „offen“ nach einem Docker-Probelauf); die Protokoll-Seite stürzte im Entwicklungsmodus ab (Konstante „Symbol“ gegen das Symbol.for des React Compilers); Auftragstitel „Alter Snapshot gelöscht“ heißt jetzt „Snapshot gelöscht“ (gelöscht wird auch von Hand). Tests: pytest test_homelab_snapshots.py (Ausführer-Details, Liste, Läufe mit nachgespieltem Ausführer, Sperren, Aufräumen, Schnittstelle), test_homelab_speicher.py; Vitest SnapshotListe, lib/snapshots, Speicher. Prüftor grün. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2784c0ea32
commit
450c782ed3
@@ -5,6 +5,8 @@ Quellen, alle nur gelesen:
|
||||
Berichte nur auf Wunsch (berichte=1): die Aufträge „bericht“ der Update-Läufe und die Berichte, die
|
||||
der Ausführer von sich aus schickt (alle zehn Minuten; kanal.py vermerkt ihren Eingang).
|
||||
lauf „Jetzt updaten“-Läufe mit ihren Schritten (updates.py)
|
||||
snapshot Aktionen der Seite Snapshots (snapshots.py, seit 25.09.2026): anlegen, löschen, zurücksetzen, Sicherung
|
||||
löschen — Läufe wie die Updates, mit eigenen Titeln
|
||||
sammellauf „Alle aktualisieren“ (sammellauf.py)
|
||||
hinweis Hinweise des Wächters: erschienen, erledigt, Selbstreparatur (der Verlauf in mc2-waechter.json;
|
||||
ein Hinweis, der dort schon herausgefallen ist, erscheint mit seinem Beginn)
|
||||
@@ -28,7 +30,7 @@ from kern.einstellungen import einstellungen
|
||||
from kern.zeit import LOCAL_TZ
|
||||
|
||||
from services.einstellungen import melde_log
|
||||
from services.homelab import apps, arcane, kanal, pflege, sammellauf, updates
|
||||
from services.homelab import apps, arcane, kanal, pflege, sammellauf, snapshots, updates
|
||||
|
||||
GRENZE_MAX = 1000
|
||||
DETAILS_MAX = 4000
|
||||
@@ -52,12 +54,13 @@ _AKTION = {
|
||||
"Paketlisten erneuern"),
|
||||
"zurueck": ("Auf den Snapshot zurückgesetzt", "Zurücksetzen gescheitert", "Wird auf den Snapshot zurückgesetzt",
|
||||
"Zurücksetzen"),
|
||||
"snapshot_loeschen": ("Alter Snapshot gelöscht", "Snapshot löschen gescheitert", "Alter Snapshot wird gelöscht",
|
||||
# Seit 25.09.2026 nicht mehr „Alter …“: Gelöscht wird auch auf der Seite Snapshots, nicht nur nach einem Update.
|
||||
"snapshot_loeschen": ("Snapshot gelöscht", "Snapshot löschen gescheitert", "Snapshot wird gelöscht",
|
||||
"Snapshot löschen"),
|
||||
"sichern": ("Sicherung angelegt", "Sicherung gescheitert", "Sicherung läuft", "Sicherung"),
|
||||
"sicherung_zurueck": ("Sicherung zurückgespielt", "Zurückspielen gescheitert", "Sicherung wird zurückgespielt",
|
||||
"Zurückspielen"),
|
||||
"sicherung_loeschen": ("Alte Sicherung gelöscht", "Sicherung löschen gescheitert", "Alte Sicherung wird gelöscht",
|
||||
"sicherung_loeschen": ("Sicherung gelöscht", "Sicherung löschen gescheitert", "Sicherung wird gelöscht",
|
||||
"Sicherung löschen"),
|
||||
"host_update": ("Host-Pakete eingespielt", "Host-Pakete gescheitert", "Host-Pakete werden eingespielt",
|
||||
"Host-Pakete"),
|
||||
@@ -287,7 +290,46 @@ def _anzahl_pakete(n: object, wort: str) -> str:
|
||||
return f"{n} {wort}{'' if n == 1 else 'e'} eingespielt" if isinstance(n, int) and n > 0 else f"{wort}e eingespielt"
|
||||
|
||||
|
||||
# Läufe der Seite Snapshots: (Baustein, Ergebnis) → (Titel, Stufe). Gescheitert heißt dort meist: Am Gerät hat sich
|
||||
# nichts geändert (warn) — nur ein gescheitertes Zurücksetzen ist rot.
|
||||
_SNAPSHOT_TITEL = {
|
||||
("snapshot-anlegen", "angelegt"): ("Snapshot angelegt", "ok"),
|
||||
("snapshot-anlegen", "fehler"): ("Snapshot anlegen gescheitert", "warn"),
|
||||
("snapshot-loeschen", "geloescht"): ("Snapshot gelöscht", "ok"),
|
||||
("snapshot-loeschen", "fehler"): ("Snapshot löschen gescheitert", "warn"),
|
||||
("snapshot-zurueck", "zurueckgesetzt"): ("Zurückgesetzt, Prüfung grün", "ok"),
|
||||
("snapshot-zurueck", "fehler"): ("Zurücksetzen gescheitert", "fehler"),
|
||||
("sicherung-loeschen", "geloescht"): ("Sicherung gelöscht", "ok"),
|
||||
("sicherung-loeschen", "fehler"): ("Sicherung löschen gescheitert", "warn"),
|
||||
}
|
||||
|
||||
|
||||
def _snapshot_lauf(x: dict, schw: Callable) -> dict | None:
|
||||
"""Eine Aktion der Seite Snapshots (snapshots.py): eigene Art „snapshot“, eigene Titel."""
|
||||
name = str(x.get("name") or x.get("ziel") or "?")
|
||||
baustein, status, text = str(x.get("baustein")), x.get("status"), x.get("text")
|
||||
was = snapshots.WAS.get(baustein, "Snapshot-Aktion")
|
||||
if status == "laeuft":
|
||||
titel, stufe = f"{snapshots.LAEUFT.get(baustein, was)} · {name}", "info"
|
||||
elif status == "unterbrochen":
|
||||
titel, stufe = f"{was} unterbrochen · {name}", "fehler"
|
||||
text = text or ("Der Homelab-Teil wurde währenddessen neu gestartet; was am Gerät geschah, zeigt der nächste "
|
||||
"Bericht.")
|
||||
elif baustein == "snapshot-zurueck" and x.get("ergebnis") == "fehler" and x.get("zurueckgesetzt"):
|
||||
titel, stufe = f"Zurückgesetzt, Prüfung rot · {name}", "fehler"
|
||||
else:
|
||||
wort, stufe = _SNAPSHOT_TITEL.get((baustein, str(x.get("ergebnis"))), (f"{was} beendet", "info"))
|
||||
titel = f"{wort} · {name}"
|
||||
zeilen = [str(s) for s in x.get("schritte") or []]
|
||||
if status == "fertig" and x.get("gemeldet") is False:
|
||||
zeilen.append("Ohne Meldung: Das Ergebnis steht auf der Seite Snapshots.")
|
||||
return _eintrag(x.get("ende") or x.get("start"), "snapshot", titel, text, stufe, x.get("ziel"),
|
||||
"\n".join(zeilen) or None, schw)
|
||||
|
||||
|
||||
def _lauf(x: dict, schw: Callable) -> dict | None:
|
||||
if str(x.get("baustein")) in snapshots.LAEUFT:
|
||||
return _snapshot_lauf(x, schw)
|
||||
name = str(x.get("name") or x.get("ziel") or "?")
|
||||
baustein, ergebnis, status, text = x.get("baustein"), x.get("ergebnis"), x.get("status"), x.get("text")
|
||||
was = _LAUF_WAS.get(str(baustein), "Update")
|
||||
@@ -431,6 +473,9 @@ def _betreffe(laeufe: list[dict], sammellaeufe: list[dict]) -> dict[str, str]:
|
||||
bekannt = {}
|
||||
for x in laeufe:
|
||||
if x.get("text") and x.get("gemeldet") is not False:
|
||||
if x.get("betreff"): # die Läufe der Seite Snapshots merken sich ihren Betreff selbst
|
||||
bekannt[x["text"]] = x["betreff"]
|
||||
continue
|
||||
dringend = x.get("dringend")
|
||||
if dringend is None: # Läufe von vor dem 24.09.2026 abends: wie updates._ende es entschied
|
||||
dringend = x.get("ergebnis") in ("zurueckgerollt", "fehler") and "nicht begonnen" not in x["text"]
|
||||
|
||||
Reference in New Issue
Block a user