Files
mission-control-v2/docs/ARCHITEKTUR.md
T

287 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Architektur des Homelab Orchestrators (Teil Box-Wart)
_Stand 24.09.2026, Code in `main` = `75611be`. Für Agenten und Techniker. Versionen, Ports und Modelle mit
Messwerten: [wissen/STACK.md](wissen/STACK.md)._
Der Homelab Orchestrator (vormals Mission Control 2, kurz „MC2") hat zwei Bereiche: „Box-Wart" für die KI-Box und
„Homelab" für den Proxmox-PC. Zwei Instanzen, eine Oberfläche: Dieselbe Codebasis läuft auf der KI-Box (Rolle `box`)
und später als Container auf dem Proxmox-PC (Rolle `homelab`). Die Homelab-Instanz ist noch nicht eingerichtet
(Phase 3, braucht User-OK); dieses Dokument beschreibt deshalb vor allem die Rolle `box`.
Der Box-Wart hält die KI-Box aktuell, passt auf sie auf und sucht bessere Modelle. Er besteht aus vier eigenen
Python-Prozessen, einer Bash-Schicht für Updates, Sicherung und Meldungen und aus Zustandsdateien unter
`/srv/models`. Er steuert zwei fremde Programme: llama-swap mit llama.cpp (die Modelle) und Hermes (der Agent „Lucy"
mit Telegram).
## Überblick
```mermaid
flowchart LR
subgraph Netz["Heimnetz"]
BR["Browser (PC, Handy)"]
OC["OpenChamber (PC)"]
LD["Lucy-Desktop (PC)"]
NQ["NerdQuiz (Arcane)"]
end
subgraph Box["KI-Box"]
MC2["MC2 :9001<br/>Oberfläche, /api"]
GW["mc2-gateway :9010<br/>/v1, Bild-Weiche"]
ST["mc2-steward<br/>Wächter, Re-Warm"]
RD["mc2-radar<br/>00:30"]
HE["Hermes :8642<br/>Lucy, Telegram"]
LS["llama-swap :8080"]
end
BR --> MC2
OC -->|/v1| MC2
LD -->|/api, /v1| MC2
MC2 -->|/v1 roh| GW
HE -->|/v1| GW
GW --> LS
NQ -->|/v1, Alias fast| LS
RD -->|Baseline| LS
ST -.->|prüft| MC2
```
Alle Modell-Anfragen enden bei llama-swap, das je Modell einen `llama-server` startet. NerdQuiz geht als einziger
Client an MC2 vorbei direkt auf `:8080`. Nur den Radar-Kandidaten startet der Radar-Lauf nachts als eigenen
`llama-server` auf `:5899`, neben llama-swap.
## Prozesse
| Prozess (Unit) | Einstieg | Aufgabe | Eigener Zustand |
|---|---|---|---|
| `mission-control-2` (`:9001`) | `backend/app.py` | Oberfläche aus `frontend/dist`; 62 `/api`-Routen; reicht `/v1` roh an den Gateway weiter (`MC_V1_UPSTREAM`); reicht das Hermes-Dashboard unter `/hermes-ui/` durch (HTTP und WebSocket); Erinnerungen; startet Update- und Download-Aufträge als eigene systemd-Einheiten `mc2-job-<id>` (`services/jobengine.py`) | Briefkasten, Erinnerungen, Routing-Policy, Hugging-Face-Zugang, ausgeblendete Hinweise |
| `mc2-gateway` (`127.0.0.1:9010`) | `backend/gateway_app.py` | `/v1` mit `model: auto` und Bild-Weiche, Kontext-Warnung, `/gw/health` | Token-Zähler (`~/.hermes/token_stats.json`) |
| `mc2-steward` | `backend/steward.py` | Re-Warm (alle 90 s, lädt das Hirn nach, wenn nichts geladen ist), Config-Watch (5 s), Wächter (jede Minute) | `mc2-waechter.json` (einziger Schreiber) |
| `mc2-radar` (Timer 00:30) | `backend/radar_lauf.py` | Suche und Nachttest neuer Modelle | `mc2-radar.json`, Baseline, `/srv/models/radar/` |
| Bash-Schicht | `deploy/*.sh` | Updates (`autoupdate.sh` mit `update-swap.sh`, `update-engine.sh`, Postchecks, `self-repair.sh`), Sicherung (`backup.sh`, `restore.sh`), Meldungen (`notify.sh`, `morgenmeldung.sh`), Vorwärmen (`warmup.sh`), `projekte-sync.sh`, Deploy (`deploy.sh`, `pruefen.sh`) | Pins, Meldeprotokoll, Nacht-Warteschlange, Deploy-Log |
Alle vier Python-Prozesse teilen die flachen Pakete `backend/services/` und `backend/kern/` (Import über
`PYTHONPATH` und `WorkingDirectory` der Units). Router bleiben dünn, die Logik liegt in `backend/services/`
(siehe `AGENTS.md`).
**Warum getrennte Prozesse** (Umbau v3, 15.07.): Der Gateway ist klein, wird kaum angefasst und startet sich
selbst neu (`Restart=always`, `TimeoutStopSec=5`). MC2 darf deshalb neu starten, ohne dass Anfragen von Lucy und
OpenChamber abreißen. Der Steward überlebt MC2-Neustarts und kann MC2 selbst überwachen. Rückweg ist jeweils eine
Zeile in `deploy/mission-control-2.service` (`MC_V1_UPSTREAM` bzw. `MC_REWARM_ENABLED=0`/`MC_SENTRY_ENABLED=0`
entfernen).
Fremde Dienste, die der Box-Wart nur steuert oder überwacht: `llama-swap` (System-Unit, `--watch-config`),
`hermes-gateway`, `hermes-builtin-ui`, `lucy-stimme`, `voice-service` und `box-console` (beide schlafen seit 24.09.).
## Rollen und Partner-Instanz (seit 24.09., Phase 2a)
- **`backend/kern/einstellungen.py`:** `MC_ROLLE` (`box` oder `homelab`, Standard `box`), `MC_INSTANZ` (Anzeigename,
Standard „Box-Wart" bzw. „Homelab"), `MC_DATEN_DIR` (Zustandsdateien; Standard `/srv/models` auf der Box,
`/var/lib/mc2` im Homelab), `MC_PARTNER_URL` (Basis-URL der anderen Instanz, leer = keine) und `MC_PARTNER_NAME`.
- **`backend/kern/zeit.py`:** die eine Zeitzone `MC_LOCAL_TZ` (Standard `Europe/Berlin`) statt einer Kopie je Modul.
- **`backend/kern/partner.py`:** Lebenszeichen der anderen Instanz über deren `/api/health` (5 s Zeitlimit, 15 s
zwischengespeichert).
- **Rolle `box`** hängt alle Box-Router ein. **Rolle `homelab`** lädt nur `/api/health` und `/api/partner`; die
Homelab-Router folgen in Phase 3. `steward.py` betreibt Re-Warm und Config-Watch nur in der Rolle `box`; der
Wächter prüft im Homelab nur Platte und Partner.
- **`/api/health`** nennt `rolle` und `instanz`; `engine_reachable`, `gateway_reachable` und `brain` gibt es nur auf
der Box.
- **`GET /api/partner`** liefert das Lebenszeichen der anderen Instanz. **`/api/partner/<pfad>`** reicht an
`<partner>/api/<pfad>` weiter (alle Methoden, nach der Herkunftsprüfung). `partner/…` und `stream` werden nicht
weitergereicht: Zwei Instanzen sollen sich keine Anfrage endlos zureichen, und der Live-Strom würde offen bleiben.
- **Wächter:** Antwortet die andere Instanz nicht, entsteht der Hinweis „<Name> antwortet nicht", rot erst nach
`MC_WAECHTER_PARTNER_TAKTE` = 5 Takten, damit ein Neustart kein Alarm ist.
- Die Units setzen weder `MC_ROLLE` noch `MC_PARTNER_URL`: Die Box läuft in der Rolle `box` und ohne Partner, bis die
zweite Instanz eingerichtet ist.
## Aufträge (seit 24.09., Phase 2b)
- **Eigene Einheiten:** `services/jobengine.py` startet jeden Auftrag (Update, Modell-Download) per `systemd-run`
als Einheit `mc2-job-<id>` des Nutzer-Managers (als root: des System-Managers). `RuntimeMaxSec` ist das
Zeitlimit, „Abbrechen" stoppt die Einheit samt Kindern. Ein Neustart von MC2 (Deploy, Absturz) würgt den
Auftrag nicht mehr ab.
- **Akten:** `<Datenordner>/mc2-jobs/<id>.json` (Zustand), `.log` (Ausgabe), `.exit` (Exit-Code, atomar von einer
Bash-Hülle geschrieben). MC2 liest die Akten beim Start (`wiederaufnehmen()` im Lebenszyklus der App) und
beobachtet laufende Aufträge weiter; endet eine Einheit ohne Exit-Code, gilt der Auftrag als gescheitert bzw.
abgebrochen oder als Zeitlimit. Beendete Aufträge verschwinden nach einem Tag bzw. ab 40 Stück.
- **Geheimnisse** (z. B. `HF_TOKEN`) gehen über eine nur für den Nutzer lesbare Umgebungsdatei `<id>.env`, die der
Auftrag beim Start liest und löscht — nicht über Befehlszeile oder Einheit (auf der Box nachgeprüft).
- **Nacharbeiten** sind benannt (`@jobengine.nacharbeit`): `wartung:nach_update` (Zwischenspeicher leeren) und
`modell:rolle` (Rolle nach dem Download setzen). Sie stehen mit ihren Daten in der Akte, laufen also auch nach
einem Neustart, und zwar vor dem Endzustand: Wer „done" sieht, sieht auch ihre Wirkung.
- **Ohne systemd** (PC, Tests, `MC_JOBS_ART=prozess`) läuft der Auftrag als Kindprozess und überlebt keinen
Neustart. Ein Probelauf daneben (`MC_PROBELAUF=1`) nimmt keine Aufträge auf.
## Ziele, Bausteine und Update-Verlauf (seit 24.09., Phase 2d)
- **Gemeinsames Modell** `backend/kern/ziele.py`: Ein Ziel ist ein Gerät (KI-Box, später Proxmox-Host, Container,
Arcane-VM) mit Bausteinen. Jeder Baustein hat einen Stand (`neu`, `aktuell`, `unbekannt` mit Grund,
`festgehalten`, `wird-geprueft`), Versionen und den Knopf, der das Update anstößt (Methode, Pfad, Rückfrage).
- **Box-Adapter** `services/box_updates.py` (`ki_box_ziel()`): Betriebssystem, Motor, llama-swap und Hermes aus dem
Zwischenspeicher der Update-Prüfung (10 Minuten, nach jedem Update sofort neu), Pins als `festgehalten`,
gescheiterte Prüfungen als `unbekannt`. `GET /api/ziele` liefert die KI-Box; der Homelab-Teil liefert seine
Ziele ab Phase 3 unter `/api/homelab/ziele`, die Oberfläche legt beide zu einer Liste zusammen.
- **Strukturierter Update-Verlauf** `<Datenordner>/mc2-update-verlauf.jsonl`: je Baustein eine JSON-Zeile
(`lauf`, `anlass`, `ts`, `baustein`, `ergebnis`, `text`). Es schreiben `autoupdate.sh` (Helfer `ergebnis`/`verlauf`;
die Telegram-Texte bleiben Zeichen für Zeichen gleich) und die Update-Knöpfe (Abschluss-Haken
`wartung:verlauf` der Aufträge, bei „Alle aktualisieren" mit dem Teil, an dem die Kette scheiterte).
`services/update_verlauf.py` liest ab dem ersten strukturierten Eintrag nur noch ihn, ältere Läufe weiter aus
`~/mc2-notify.log`. Den Hermes-Auftrag des Sonntags-Laufs startet `autoupdate.sh` mit `?verlauf=0`, damit er
nicht doppelt erscheint.
- **Sammel-Schreiben der llama-swap-Config** `llamaswap.sammeln()`: Alle Änderungen darin lesen dieselbe Config aus
dem Speicher; geschrieben wird einmal am Ende, bei einem Fehler gar nicht. Ein Radar-Tausch (Eintragen, Befehl,
Zwilling, Alias, Gruppe, ttl) und die Hirn-Umstellung sind damit je ein Schreibvorgang statt bis zu sieben;
jeder Schreibvorgang lässt llama-swap alle Modelle entladen. Hermes wird erst danach umgestellt.
## Modelle und Modell-Rollen
- **Rollen-Aliase statt Namen:** Clients fragen `hermes`/`fast` (Hirn), `coder`/`heavy` (Coder), `vision` und
`coder-bild` (Bild-Zwillinge), `embed`, `reranker`. Die Eintragsnamen ändern sich beim Tausch, die Aliase nicht.
- **Gruppen:** `brains` (`swap: false`, `persistent: true`, `exclusive: false`) enthält nur das Hirn, es bleibt
geladen. `bild` (`swap: true`, `exclusive: false`) enthält die zwei Zwillinge, höchstens einer ist geladen.
Alles andere lädt bei Bedarf und geht nach `ttl` wieder raus.
- **Bild-Weiche** (`routers/gateway_proxy.py`, `services/router_logic.py`): Hat der aktuelle Schritt ein Bild, geht
die Anfrage an den Zwilling der Rolle (Coder-Ziele an `coder-bild`, alles andere an `vision`). Ältere Bilder
beschreibt `vision` einmal als Text (Cache je Bild), damit der Rest beim schnellen Modell mit Draft bleibt.
- **`model: auto`** ist die Chat-Spur: normal `fast`, bei langen oder schweren Anfragen `heavy` (Schwellen in der
Routing-Policy `mc2-routing.json`, ohne Datei gelten die Env-Defaults).
- **Warm halten:** `warmup.sh` (Root-Kopie in `/usr/local/bin/llama-swap-warmup.sh`) läuft nach jedem Start von
llama-swap. Danach übernimmt der Re-Warm im Steward; welches Modell warm bleibt, sagt `MC_WARMSET` im Drop-in
`mc2-steward.service.d/warmset.conf` (heute nur das Hirn).
- **Speicherregel:** Warm-Set plus größtes Bedarfsmodell ≤ ~115 GB, sonst droht ein Kernel-OOM.
## Wer schreibt was
| Datei | Schreiber | Regel |
|---|---|---|
| `/etc/llama-swap/config.yaml` | MC2 (Modelle, Rollen, Aufräumen, Hirn-Umstellung), Radar („Übernehmen"), `deploy.sh`, `restore.sh` | Die lebende Datei ist die Wahrheit. MC2 schreibt nur unter `llamaswap.config_sperre()` (Thread-Sperre plus `flock` auf `.mc2-config.lock`) und atomar. `deploy.sh` überschreibt nur, wenn sich `deploy/llama-swap.config.yaml` im selben Deploy geändert hat. Jede Änderung lädt llama-swap neu und entlädt alle Modelle. |
| `~/.hermes/config.yaml` | Hermes; MC2 nur bei der Hirn-Umstellung; `autoupdate.sh` und `restore.sh` beim Rückweg; `self-repair.sh` | MC2 ändert nur `model.default`/`model.model`, atomar, vorher `config.yaml.bak-hirn-<Zeit>`; bei einem Lesefehler schreibt es nichts. |
| `mc2-steward.service.d/warmset.conf` | `deploy.sh`; das Radar beim Übernehmen eines neuen Hirns | Das Radar tauscht den Namen in `MC_WARMSET` und startet den Steward neu. |
| `/srv/models/mc2-waechter.json` | nur der Steward | MC2 liest und führt Knöpfe aus; der nächste Takt sieht das Ergebnis. Der Ordner folgt `MC_DATEN_DIR`. |
| `/srv/models/mc2-quittiert.json` | nur MC2 (Knopf „Ausblenden bis zum nächsten Lauf") | Der Steward liest; ein ausgeblendeter Werkzeugfehler-Hinweis kommt wieder, wenn der nächste Lauf des Jobs erneut Fehler hat. |
| `/srv/models/mc2-radar.json` (+ `mc2-radar-baseline.json`) | Radar-Lauf und MC2 | jede Änderung unter `flock`, atomar |
| `/srv/models/mc2-jobs/` | nur MC2 (Akten) und die Hülle des Auftrags (`.log`, `.exit`) | Akten atomar; Protokoll und Exit-Code schreibt der Auftrag selbst, MC2 hängt nur `[mc]`-Zeilen an. |
| `/srv/models/mc2-pins.json` | `autoupdate.sh` hält fest; MC2 gibt frei | Format: Baustein → `pinned`, `version`, `grund`, `datum` |
| `/srv/models/mc2-announce.json` (Briefkasten, 200 Einträge) | nur MC2 | Steward und Gateway liefern per HTTP (`MC_ANNOUNCE_HTTP`), `notify.sh` per `POST /api/voice/announce` |
| `/srv/models/mc2-reminders.json` | nur MC2 | Erinnerungs-Schleife und `/api/reminders` im selben Prozess |
| `/srv/models/mc2-geheimnisse.json` | MC2 (Einstellungen) | Hugging-Face-Zugang, Rechte 0600 |
| `/srv/models/mc2-discover.json` | MC2, Radar | Cache der Hugging-Face-Entdeckung (12 h) |
| `/srv/models/mc2-deploy.log` | `deploy.sh` | eine Zeile je Deploy, auch gescheiterte |
| `~/mc2-notify.log` | `notify.sh` (anhängen), `morgenmeldung.sh` (über 3 MB auf 2 MB kürzen) | Quelle des Update-Verlaufs für Läufe vor dem 24.09.; der Wortlaut „QUEUED für Morgen-Digest" muss bleiben |
| `/srv/models/mc2-update-verlauf.jsonl` | `autoupdate.sh`, MC2 (Update-Knöpfe) | nur anhängen, eine JSON-Zeile je Baustein |
| `~/.hermes/night-queue.txt` | `notify.sh` (anhängen), `morgenmeldung.sh` (übernehmen, senden) | nicht gesendeter Stapel bleibt als `.senden` liegen |
## Meldeweg
`deploy/notify.sh` ist der eine Meldeweg: Die Meldung geht in Lucys Briefkasten (`POST /api/voice/announce`) und an
Telegram. Zwischen 00:00 und 06:59 sammelt `notify.sh` alles Nicht-Dringende in der Nacht-Warteschlange;
`morgenmeldung.sh` (Timer 07:00) schickt es als eine Nachricht. Dringend ist `-d`, ein Betreff mit „Alarm" oder
„Notfall" oder ein Text, der mit „KRITISCH" beginnt. Lucy-Desktop fragt den Briefkasten ab
(`/api/voice/announcements`) und spricht neue Einträge.
Telegram hat zwei Wege: zuerst `hermes send --to telegram`; scheitert das oder fehlt Hermes (die Homelab-Instanz hat
keins), geht die Meldung direkt an die Telegram-Bot-API (seit 24.09., live bewiesen 15:41). Die Zugangsdaten liest
`notify.sh` aus der Datei in `MC_TELEGRAM_ENV` (Standard `~/.hermes/.env`: `TELEGRAM_BOT_TOKEN`,
`TELEGRAM_HOME_CHANNEL`, optional `TELEGRAM_HOME_CHANNEL_THREAD_ID`) und gibt das Token über stdin an `curl`, damit
es nicht in der Prozessliste steht. Erst wenn beide Wege scheitern, bleiben Protokoll und `wall`
(`MC_NOTIFY_OHNE_WALL=1` schaltet `wall` ab). Einzelheiten und Betreffzeilen: [BETRIEB.md](BETRIEB.md).
## Schnittstellen
In der Rolle `box` 62 Routen unter `/api`: 60 des Box-Warts (seit 24.09., vorher 95) und 2 für die
Partner-Instanz. In der Rolle `homelab` gibt es nur `/api/health` und die Partner-Routen.
| Gruppe | Routen |
|---|---|
| Cockpit (`routers/boxwart.py`) | `GET /start`, `GET /hinweise`, `POST /hinweise/{id}/aktion/{aktion}`, `GET /modelle/nutzung`, `GET /updates/verlauf`, `POST /updates/festgehalten/{baustein}/freigeben`, `GET /modelle/aufraeumen`, `POST /modelle/aufraeumen/loeschen` |
| Radar (`routers/radar.py`) | `GET /radar`, `POST /radar/suche`, `POST /radar/{id}/uebernehmen`, `POST /radar/{id}/verwerfen` |
| Modelle (`routers/models.py`) | `GET /models`, `GET /discover`, `POST /models/register`, `GET /hf/search`, `GET /hf/quants`, `POST /models/install`, `GET /jobs`, `POST /jobs/{id}/cancel`, `POST /models/{id}/role`, `POST /models/unload`, `POST /models/{id}/unload`, `POST /models/{id}/load`, `DELETE /models/{id}` |
| Wartung (`routers/maintenance.py`) | `GET /maintenance/updates`, `GET /maintenance/update-details`, `POST` `check-updates`, `os-update`, `engine-update`, `swap-update`, `hermes-update`, `update-all`, `restart`; `GET /maintenance/logs`; `GET`/`POST /maintenance/geheimnisse` |
| System (`routers/system.py`) | `GET /system/status`, `GET /system/services`, `POST /system/backup`, `GET /system/backups`, `POST /system/restart`, `GET /system/token-stats` |
| Lucy und Sprache (`routers/voice.py`) | `POST /alarm`, `POST /voice/announce`, `GET /voice/announcements`, `POST /voice/stt`, `POST /voice/turn`, `POST /voice/chat`, `GET /lucy/stimme/health`, `POST /lucy/stimme/tts`, `POST /lucy/stimme/tts/stream` |
| Sonstiges | `GET /health`, `GET /stream` (Live-Strom, SSE), `GET /routing`, `GET`/`POST /reminders`, `DELETE /reminders/{id}`, `GET /zeitmaschine`, `POST /zeitmaschine/restore` |
| Partner (`routers/partner.py`) | `GET /partner` (Lebenszeichen), `/partner/{pfad}` (Durchreiche, alle Methoden) |
Dazu `/v1/*` (Weiterleitung an den Gateway), `/hermes-ui/` und die Auslieferung der Oberfläche. Unbekannte
`/api`-Pfade liefern einen Fehler (bei `GET` 404) statt der Startseite. Die MCP-Server in `mcp/` sprechen dieselben
Routen.
**Herkunftsprüfung** (`backend/services/herkunft.py`, statt Anmeldung): Schreibende Aufrufe (`POST`, `PUT`, `PATCH`,
`DELETE`) auf `/api/` mit dem `Origin` einer fremden Webseite lehnt MC2 mit 403 ab. Erlaubt bleiben Aufrufe ohne
`Origin` (Skripte, `curl`), `Origin: null` bzw. `file://` (Lucy-Desktop), dieselbe Adresse wie die Oberfläche und die
Entwicklungs-Ports 5173, 5180, 5181. `/v1` ist nicht betroffen.
## Der Homelab-Teil (Phasen 3 und 4, Code seit 24.09.; Einrichtung wartet auf das User-OK)
Derselbe Code in der Rolle `homelab`, in einem eigenen Container auf dem Proxmox-PC (`deploy/homelab/`).
Er braucht **keinen Proxmox-Schlüssel**: Alles, was vom Host kommt, liefert der Ausführer.
```mermaid
flowchart LR
AF["Ausführer<br/>Proxmox-Host, root"] -->|holt Aufträge, Bericht alle 10 min| HL["Homelab-Teil<br/>Container, :9001"]
HL -->|Ziele, Läufe| UI["Oberfläche<br/>Seite Homelab"]
BOX["KI-Box :9001"] <-->|prüfen sich, /api/partner| HL
HL -->|Webprüfung| G["Gäste<br/>AdGuard, Gitea …"]
```
- **Ausführer** `deploy/homelab/ausfuehrer.py` (root auf dem Host, nur Standardbibliothek, `mc2-ausfuehrer.service`):
holt sich Arbeit beim Homelab-Teil ab („Pull“, kein offener Port auf dem Host) und weist sich mit einem
gemeinsamen Geheimnis aus (Kopfzeile `X-MC2-Ausfuehrer`; erzeugt der Homelab-Teil in
`/var/lib/mc2/ausfuehrer.token`, liegt auf dem Host in `/etc/mc2-ausfuehrer.json`, beides 0600). Er führt nur eine
feste Liste von Aktionen aus (`bericht`, `snapshot`, `update`, `os_update`, `suchen`, `zurueck`,
`snapshot_loeschen`, `sichern`, `sicherung_zurueck`, `sicherung_loeschen`, `host_update`, `host_neustart`) und
prüft selbst, ob ein Gast das Etikett `community-script` oder `watcher` trägt und nicht `watcher-aus` — dem Server
vertraut er dabei nicht.
- **Bericht** (nur lesend, auch von Hand: `python3 ausfuehrer.py --bericht`): Host-Version und Paket-Updates, je
Gast Status, IP, Etiketten, Snapshots, ob ein Snapshot geht (Bind-Mounts wie beim PBS verhindern ihn), die
Community-Script-Kennung (aus `/usr/bin/update` im Gast), die App-Version (je App eigener Weg: `/root/.<app>`,
`AdGuardHome --version`, `netbird version`, `dpkg-query`, Docker-Image-Datum) und die Paket-Updates im Gast samt
Alter der Paketlisten. Dazu der Sicherungsspeicher (`host.sicherung`: Name und frei, oder was fehlt), je Container
ohne Snapshot, ob eine Sicherung geht (`sicherung_moeglich`, sonst `sicherung_grund`), und die eigenen
Sicherungen je Gast.
- **Sicherung statt Snapshot** (Ausführer, seit 24.09.): Wo kein Snapshot geht, sichert `sichern` den Container per
`vzdump` — auf den ersten lokalen Speicher, der Sicherungen annimmt (Art `dir`/`btrfs`, nicht geteilt; heute
`local` = `/var/lib/vz`), oder auf `sicherung_speicher` aus `/etc/mc2-ausfuehrer.json`. Nie auf einen Speicher der
Art `pbs`: Der PBS würde sich selbst sichern. Nimmt kein lokaler Speicher Sicherungen an, lehnt er ab und sagt im
Bericht, was fehlt; die Speicher-Konfiguration ändert er nicht. Vorher prüft er den Platz (frei > belegt × 1,2;
belegt = rootfs, Bind-Mounts sichert vzdump nie mit). `vzdump` läuft mit `--mode snapshot` (rootfs auf lvmthin: der
Gast läuft durch), sonst `stop`, dazu `--remove 0` (keine Aufräumregeln des Speichers) und der Notiz
`mc2-sicherung: …`. Nur Sicherungen mit dieser Notiz spielt er zurück (`sicherung_zurueck`: Gast stoppen,
`pct restore <vmid> <archiv> --force 1 --storage <bisheriger rootfs-Speicher>`, starten; Bind-Mount-Daten bleiben
unberührt) oder löscht er (`sicherung_loeschen`, `pvesm free`). Zeitlimit 30 min; läuft es ab, bekommt `vzdump`
bzw. `pct restore` erst SIGTERM, damit Sperre und Snapshot aufgeräumt werden.
- **Homelab-Teil** `backend/services/homelab/`: `kanal.py` (Geheimnis, Auftragsliste unter Dateisperre, Bericht),
`apps.py` (App-Katalog: Name, GitHub-Quelle, Weboberfläche, Update-Weg), `inventar.py` (Bericht + neueste Versionen
von GitHub + eigene Webprüfung → Ziele im gemeinsamen Modell; Paketlisten älter als 14 Tage = „unklar“ mit Knopf
„Nach Updates suchen“), `karenz.py` (Wartezeit nach einer Skriptänderung), `updates.py` („Jetzt updaten“),
`pflege.py` (wöchentliches Suchen). Schnittstellen `routers/homelab.py` unter `/api/homelab/…`.
- **„Jetzt updaten“** (nur per Knopf): Snapshot, wo keiner geht Sicherung (wo auch die nicht geht: ohne Rückweg, mit
Warnung in der Rückfrage; scheitert der Schritt, beginnt das Update nicht) → `update` des Community-Scripts
(`PHS_SILENT=1`) bzw. Pakete → 20 s warten → frischer Bericht → Prüfung (Gast läuft, Weboberfläche antwortet,
App-Version neu). Rot → zurück auf den Snapshot bzw. die Sicherung zurückspielen + dringende Meldung; grün → ältere
`mc2-`-Snapshots bzw. `mc2-sicherung`-Sicherungen dieses Gasts weg (die neueste bleibt) + Meldung. Läufe in
`/var/lib/mc2/homelab-laeufe.json` und im strukturierten Update-Verlauf. Host: Pakete per Knopf mit Warnung,
Neustart als eigener Knopf.
- **Wartezeit nach Skriptänderung** (`karenz.py`): `update` lädt `ct/<kennung>.sh` ungepinnt von GitHub
(community-scripts/ProxmoxVE, Zweig `main`) und führt es als root aus. Für Apps mit dem Weg „skript“ fragt der
Homelab-Teil deshalb, wann das Skript zuletzt geändert wurde (`/repos/community-scripts/ProxmoxVE/commits?path=…`,
über `kern/github.py`, 15 min gemerkt). Jünger als `MC_HOMELAB_KARENZ_H` (Standard 48 h): der Baustein bleibt
„neu“, aber ohne Knopf, mit „Das Update-Skript wurde am TT.MM. geändert; zur Sicherheit erst ab TT.MM. HH:MM.“
(Berliner Zeit); `updates.starten` lehnt mit demselben Satz ab. Antwortet GitHub nicht, blockiert nichts; die
Rückfrage sagt dann „Ob das Skript kürzlich geändert wurde, ließ sich nicht prüfen.“
- **Wöchentliches Suchen** (`pflege.py`, im Wächter-Takt des Stewards): Sind die Paketlisten eines freigegebenen,
laufenden Containers älter als 7 Tage, legt der Homelab-Teil selbst den Auftrag `suchen` an — höchstens einmal je
Gast und Tag, nie während eines Update-Laufs oder neben einem offenen Auftrag für diesen Gast, nur wenn der Ausführer
gerade berichtet, bevorzugt nachts 02:00–05:00 (war die Instanz letzte Nacht aus, eben gleich). Keine Meldungen;
scheitert die Suche für einen Gast zweimal hintereinander, wird es ein gelber Hinweis des Wächters. Zustand in
`/var/lib/mc2/homelab-pflege.json`. Weil damit zwei Prozesse Aufträge anlegen, schreibt `kanal.py` unter `flock`.
- **Arcane und Docker** (`services/homelab/arcane.py`): Arcanes eigene Version kommt öffentlich über
`/api/app-version`; die Docker-Images mit neuerem Stand und der Updater brauchen einen Arcane-API-Schlüssel
(`MC_ARCANE_KEY`, Kopfzeile `X-API-Key`). Docker-Updates laufen zuerst nur als Probelauf (`dryRun`); echt erst
mit `MC_ARCANE_ECHT=1`. Die Arcane-VM braucht dafür kein Etikett, weil der Ausführer nicht beteiligt ist.
- **Wächter** in der Rolle `homelab`: Platte, Partner (die KI-Box), Ausführer (kein Bericht seit 30 min = rot),
jede Weboberfläche der freigegebenen Gäste (außer mitten in ihrem Update-Lauf; das Ergebnis meldet der Lauf) und das
wöchentliche Suchen (gelb, wenn es wiederholt scheitert).
- **Oberfläche**: Die Seite „Homelab“ zeigt alle Geräte als Karten — die KI-Box (`/api/ziele`) und alles aus
`/api/homelab/ziele` — mit Stand je Baustein, Rückweg und Knopf samt Rückfrage.
- **Meldungen** ohne Hermes: `notify.sh` im Container nimmt den Zweitweg direkt an die Bot-API
(`/etc/mc2/telegram.env`), nachts sammelt eine eigene Morgenmeldung („[Morgenmeldung Homelab]“).
- **Einrichtung** (Reihenfolge, jeder Schritt braucht das User-OK): `container-anlegen.sh` → `ausrollen.sh <ip>` →
`ausfuehrer-einrichten.sh <ip>` → `box-partner.sh <ip>`; Details in [BETRIEB.md](BETRIEB.md).