# 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 als Container 107 auf dem Proxmox-PC (Rolle `homelab`, seit 24.09.). Der Homelab-Teil hat unten einen eigenen Abschnitt; der Rest beschreibt vor allem die Rolle `box`. In der Oberfläche sind beide Bereiche getrennt (seit 24.09.): Links steht die Menüführung mit einem Block je Bereich — **KI-Box** (Cockpit `/`, Updates `/updates`, Modelle `/modelle`, dazu Dienste und das Hermes-Dashboard) und **Homelab** (Cockpit `/homelab`, Updates `/homelab/updates`) —, jeder mit eigener Verbindungsanzeige und der Zahl offener Updates. Keine Seite zeigt etwas aus dem anderen Bereich. Am Handy steckt dieselbe Leiste hinter dem Menü-Knopf (`frontend/src/app/Seitenleiste.tsx`, `lib/navigation.ts`). 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
Oberfläche, /api"] GW["mc2-gateway :9010
/v1, Bild-Weiche"] ST["mc2-steward
Wächter, Re-Warm"] RD["mc2-radar
00:30"] HE["Hermes :8642
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-` (`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), Messwerte (ein Punkt je Minute, seit 24.09.) | `mc2-waechter.json` (einziger Schreiber), `mc2-messwerte/box-*.jsonl` | | `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 `/api/health`, `/api/partner`, die Homelab-Router (`/api/homelab/…`) und einen eigenen Live-Strom; alles andere reicht die Oberfläche über `/api/partner/…` an die Box weiter. `steward.py` betreibt Re-Warm und Config-Watch nur in der Rolle `box`; die Prüfungen des Wächters im Homelab stehen im Abschnitt „Der Homelab-Teil“. - **`/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/`** reicht an `/api/` 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 „ 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-` 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:** `/mc2-jobs/.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 `.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 unter `/api/homelab/ziele`. Die Oberfläche zeigt beide getrennt, jedes in seinem Bereich. - **Strukturierter Update-Verlauf** `/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-`; 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 | | `/srv/models/mc2-messwerte/box-JJJJ-MM-TT.jsonl` | nur der Steward (Box); im Homelab `/var/lib/mc2/mc2-messwerte/-…` nur MC2 | nur anhängen unter `flock`, eine Zeile je Minute; Tage älter als 8 löscht der Schreiber selbst (siehe „Monitoring“) | | `~/.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` | | Messwerte (`routers/messwerte.py`, seit 24.09.) | `GET /messwerte?zeitraum=1h\|24h\|7d` | | 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, live seit 24.09.) 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
Proxmox-Host, root"] -->|holt Aufträge, Bericht alle 10 min| HL["Homelab-Teil
Container, :9001"] HL -->|Ziele, Läufe| UI["Oberfläche
Seite Homelab"] BOX["KI-Box :9001"] <-->|prüfen sich, /api/partner| HL HL -->|Webprüfung| G["Gäste
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. Seit 24.09. schickt er außerdem jede Minute Messwerte (eigener Faden, siehe „Monitoring“). - **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/.`, `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 --force 1 --storage `, 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), `messwerte.py` (Minutenpunkte des Ausführers, siehe „Monitoring“). 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/.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`. - **„Alle aktualisieren“** (`sammellauf.py`, seit 24.09.): alle Updates mit Knopf nacheinander, jeder Schritt ein gewöhnlicher Lauf von „Jetzt updaten“ (`updates.starten`), auf dessen Ende der nächste wartet. Dabei ist nur, was „neu“ ist und eine Aktion hat; was in der Wartezeit steht, fehlt. Docker über Arcane nur, wenn es echt läuft (ein Probelauf ändert nichts). Reihenfolge: die Gäste nach VMID außer NPMplus und AdGuard, dann NPMplus, dann AdGuard (Nadelöhre für Proxy und DNS), ganz zuletzt die Pakete des Proxmox-Hosts; der Neustart des Hosts ist nie dabei. `GET /api/homelab/alle/plan` zeigt diese Reihenfolge mit dem Rückweg je Schritt und einem Hinweis (was keinen Rückweg hat, dass der Host zuletzt kommt). `POST /api/homelab/alle` startet, wenn es etwas zu tun gibt, der Ausführer verbunden ist (und nicht im Nur-Lesen-Modus) und gerade kein einzelnes Update läuft. - Endet ein Schritt mit `zurueckgerollt` oder `fehler` (auch „Update nicht begonnen“), hört der Sammellauf auf: die übrigen Schritte `uebersprungen`, Status `abgebrochen`, dringende Meldung. Lehnt `updates.starten` einen Schritt ab (etwa weil die Wartezeit inzwischen greift), wird nur dieser übersprungen. Läuft alles durch, kommt eine Sammelmeldung („Homelab: 5 Updates eingespielt, alle Prüfungen grün.“); die Erfolgsmeldungen der Schritte hält `updates._ende` so lange zurück (`gemeldet: false` am Lauf), Fehlermeldungen nicht. Im Update-Verlauf stehen die Schritte als ein Lauf mit dem Anlass „Alle aktualisieren“. - Höchstens ein Sammellauf; solange er läuft, lehnt „Jetzt updaten“ ab („Es läuft gerade ‚Alle aktualisieren‘.“). Prüfen und Anlegen geschehen unter `updates.START_SPERRE`. Stand in `/var/lib/mc2/homelab-sammellauf.json` (die letzten 20; `GET /api/homelab/sammellauf` liefert den laufenden oder den letzten). Startet der Homelab-Teil mitten im Lauf neu, macht `updates.unterbrochene_abschliessen()` beim Start daraus `abgebrochen` mit dem Text „unterbrochen (Neustart)“ und meldet es dringend. - **Protokoll** (`protokoll.py`, seit 24.09.): `GET /api/homelab/protokoll?grenze=200&berichte=0` führt zusammen, was geschah, neueste zuerst: Aufträge an den Ausführer (`kanal.py` hebt jetzt die letzten 500 erledigten auf; die jüngsten 60 mit ganzer Ausgabe, ältere mit den letzten 4000 Zeichen), Update-Läufe mit ihren Schritten, Sammelläufe, den Verlauf der Hinweise des Wächters (der Verlauf in `mc2-waechter.json` trägt seit 24.09. die Stufe mit), die Zeilen des Melde-Logs (`MC_NOTIFY_LOG`, im Container `/var/lib/mc2/notify.log`) und die Ereignisse des wöchentlichen Suchens (`homelab-pflege.json`, Liste `ereignisse`). Berichte nur mit `berichte=1`: die Aufträge `bericht` der Läufe und die Berichte, die der Ausführer von sich aus schickt (ihr Eingang steht in `/var/lib/mc2/ausfuehrer-berichte.json`, die letzten 150). Den Betreff einer Meldung schreibt `notify.sh` nicht ins Log; er folgt aus dem Absender (Lauf, Sammellauf, Wächter, Telegram-Test, Morgenmeldung). Nichts im Protokoll enthält Geheimnisse: Die Schlüssel dieser Instanz (Arcane, Ausführer) und alles, was wie ein Token aussieht (Bot-Token, Bearer, `name=wert` mit verräterischem Namen, Zugangsdaten in Adressen), wird zu `***`; Steuerzeichen der Konsole fallen weg. - **Einstellungen des Homelab-Teils** (`einstellungen.py`, seit 24.09.): `GET /api/homelab/einstellungen` (Arcane: Adresse aus dem Bericht, Herkunft des Schlüssels `hinterlegt`/`fehlt`/`umgebung`, echt und woher; Wartezeit in Stunden; Ausführer). `POST …/arcane-schluessel` prüft den Schlüssel zuerst bei Arcane mit denselben Aufrufen, die die Übersicht braucht (`/api/environments`, dann deren Images); nur wenn Arcane ihn annimmt, landet er in `/var/lib/mc2/arcane.key` (0600). Er steht nie in einer Antwort, einem Protokoll oder einer Fehlermeldung; den Body liest der Dienst selbst, damit keine 422-Antwort ihn wiederholt. `DELETE …/arcane-schluessel` löscht die Datei, `POST …/arcane-echt` (`{"an": bool}`) schaltet echte Docker-Updates in `/var/lib/mc2/homelab-einstellungen.json`. Was nicht geht, beantworten diese Endpunkte wie „Alle aktualisieren“ mit `{"ok": false, "detail": …}` und HTTP 200. - **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 (Kopfzeile `X-API-Key`): `MC_ARCANE_KEY` in `/etc/mc2/homelab.env`, sonst der Schlüssel aus den Einstellungen (`arcane.key`); beides wird bei jedem Zugriff gelesen, ein neuer Schlüssel gilt ohne Neustart. Docker-Updates laufen zuerst nur als Probelauf (`dryRun`); echt, wenn der Schalter in den Einstellungen an ist. Setzt die Umgebung `MC_ARCANE_ECHT`, gewinnt sie (`1` = echt). 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), die Platten der laufenden Container (ab 80 % gelb, ab 90 % rot — ext4 hält 5 % für root zurück; der Ausführer schickt `platte` mit), das wöchentliche Suchen (gelb, wenn es wiederholt scheitert) und seit 24.09. den Proxmox-Host selbst aus den Messwerten (gelb, wenn er 10 Minuten lang über 95 % RAM oder 90 °C CPU liegt). - **Oberfläche** (Bereich „Homelab“, nur Daten aus `/api/homelab/…`): - **Cockpit** (`/homelab`): oben die Lage wie das Warnpanel der KI-Box — die große Leuchte (Störung vor Hinweis vor laufendem Update vor offenen Updates) und eine Lampe je Gerät —, darunter die Hinweise des Homelab-Wächters und die Geräteliste (App-Version, Paketstand, Erreichbarkeit; „Nach Updates suchen“ nur, wo der Stand unklar ist). - **Updates** (`/homelab/updates`): nur die offenen Updates, eine Zeile je Update mit alter und neuer Version, Rückweg in Kurzform (`rueckweg_art`) und Knopf samt Rückfrage aus dem Ziel-Modell; darunter der Verlauf der Läufe. - Die Logik (Lage, Lampen, Reihenfolge) liegt in `frontend/src/lib/homelab.ts`, die Ansichten in `components/homelab/` und `views/Homelab.tsx`. - **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 ` → `ausfuehrer-einrichten.sh ` → `box-partner.sh `; Details in [BETRIEB.md](BETRIEB.md). ## Monitoring: Messwerte mit Verlauf (seit 24.09.) Der Live-Strom der KI-Box (`/api/stream`, jede Sekunde ein Messpunkt) zeigt nur den Augenblick. Für den Verlauf schreiben beide Bereiche jede Minute einen Punkt, gelesen wird er für die letzte Stunde, den letzten Tag oder die letzte Woche. ```mermaid flowchart LR ST["mc2-steward (Box)
jede Minute"] -->|box-…jsonl| MR[("mc2-messwerte/
kern/messreihen.py")] AF["Ausführer (pve)
eigener Faden, jede Minute"] -->|POST …/ausfuehrer/messwerte| HL["Homelab-Teil
services/homelab/messwerte.py"] HL -->|pve-…, ct-…, vm-…jsonl| MR2[("mc2-messwerte/
im Container")] MR -->|GET /api/messwerte| UI["Oberfläche"] MR2 -->|GET /api/homelab/messwerte| UI MR2 -->|RAM, Temperatur| W["Wächter (homelab)"] ``` - **Speicher** `backend/kern/messreihen.py` (beide Rollen): je Quelle und Tag eine Datei `/mc2-messwerte/-JJJJ-MM-TT.jsonl` (Tag nach Berliner Zeit), eine JSON-Zeile je Minute (`{"t": Unix-Sekunden, "cpu": …, …}`, `null` = nicht gemessen). Schreiben hängt eine Zeile an, unter Thread-Sperre und `flock`; fehlt am Dateiende der Zeilenumbruch (Absturz), beginnt die neue Zeile auf einer eigenen. Das erste Schreiben eines neuen Tages löscht Dateien, deren Tag mehr als 8 Tage zurückliegt. Lesen verdichtet: `1h` in 60-s-Schritten, `24h` als 5-min-Mittel, `7d` als 30-min-Mittel (60, 288 bzw. 336 Werte, Schritte auf vollen Vielfachen, der letzte ist der laufende). Ein Schritt ohne Messung bleibt `null`, nichts wird aufgefüllt; kaputte Zeilen werden übersprungen. Die Summen vergangener Tage merkt sich der Prozess je Datei, solange sie sich nicht ändert. Gemessen wird zur halben Minute, damit jeder 60-s-Schritt genau einen Punkt bekommt. - **KI-Box** (`services/messwerte.py`, Taktgeber im Steward; `MC_MESSWERTE_ENABLED=0` schaltet ab, ein Trocken- oder Probelauf schreibt nie mit): `cpu` = Mittel der Minute (aus eigenen `psutil.cpu_times`-Ständen gerechnet wie `cpu_percent`, denn psutil merkt sich den letzten Aufruf je Thread), `ram` und `platte` (Modell-Laufwerk) beim Messen, `gpu`, `temp_cpu`, `temp_gpu` als Mittel von Proben alle 5 s (`MC_MESSWERTE_PROBE_S`), `netz_rx`/`netz_tx` in Bytes/s über alle Schnittstellen außer `lo`, `tokens` = Prompt- plus Antwort-Tokens pro Minute (Differenz der Gesamtzähler aus `services/token_stats.py`). Ein Zähler, der kleiner wird, ergibt in dieser Minute `null`. - **Homelab:** Der Ausführer schickt in einem eigenen Faden jede Minute `POST /api/homelab/ausfuehrer/messwerte` (gleiche Anmeldung wie seine anderen Endpunkte), unabhängig vom 10-min-Bericht und von Aufträgen, die bis zu einer Stunde laufen. Host-Werte liest er selbst aus `/proc` (`stat` für das CPU-Mittel der Minute, `meminfo`, `loadavg`, `uptime`) und `statvfs("/")`, denn `pvesh get /nodes//status` liefert in einem frischen pvesh-Prozess immer `cpu: 0` (nachgesehen am 24.09.); belegt wie bei Proxmox: RAM = gesamt − verfügbar, rootfs = gesamt − frei. Netz des Hosts = Summe der physischen Schnittstellen aus `/proc/net/dev` (wie die Knoten-Anzeige in Proxmox; `vmbr0` sähe den Verkehr der Gäste nach draußen nicht), ohne physische Schnittstelle `vmbr0`. CPU-Temperatur aus hwmon (`k10temp` bzw. `zenpower` mit Tdie vor Tctl, `coretemp` mit „Package id 0“), sonst `null`; auf dem Proxmox-PC gibt es nur `k10temp`/Tctl, kein `thermal_zone` und kein lm-sensors. Die Gäste kommen aus einem einzigen Aufruf `pvesh get /cluster/resources --type vm` (`cpu` schon auf die eigenen Kerne gerechnet, `mem`, `disk`, …), ihre Netz-Zähler aber aus denselben `/proc/net/dev`-Zeilen (`vethiN`, `tapiN`; die Stände in `/cluster/resources` sind bis zu 10 s alt und verzerrten die Minutenrate). Fehler bleiben still; ein Fehler steht einmal im Journal, bis wieder ein Punkt durchgeht. Von Hand: `python3 ausfuehrer.py --messwerte`. - **Homelab-Teil** (`services/homelab/messwerte.py`): rechnet die kumulativen Zähler in Bytes/s um. Keine Rate (`null`) gibt es, wenn der Zähler oder die Laufzeit kleiner wurde (Neustart von Gast oder Host), der vorige Stand fehlt (Neustart des Homelab-Teils; die Stände liegen nur im Speicher) oder mehr als 5 Minuten zurückliegt, oder der Sprung über 5 GB/s liegt. Gestoppte Gäste haben keine Messwerte (`null`, keine Nullen); die Platte einer VM sieht Proxmox nicht (`null`). Der eigene Container (Etikett `mc2`) wird wie im Inventar nicht erfasst. Quellen `pve`, `ct-`, `vm-`. - **Schnittstellen:** `GET /api/messwerte?zeitraum=1h|24h|7d` (nur Rolle `box`) liefert `{zeitraum, schritt_s, reihen: {cpu, ram, gpu, temp_cpu, temp_gpu, platte, netz_rx, netz_tx, tokens}, aktuell}`; `GET /api/homelab/messwerte?zeitraum=…` liefert `{zeitraum, schritt_s, geraete: [{id, name, art, reihen, aktuell}]}` mit den Geräten wie im Inventar (Host, dann die Gäste nach Nummer; Namen aus `apps.py`). Jede Reihe ist eine Liste `[t, wert]` (t = Beginn des Schritts); `temp_cpu` gibt es als Reihe nur beim Host, in `aktuell` stehen `temp_cpu` und `load1` bei Gästen auf `null`. `aktuell` ist der letzte Minutenpunkt, solange er höchstens 3 Minuten alt ist, sonst lauter `null`. Ein unbekannter Zeitraum ergibt 400. Neue Punkte stößt der Live-Strom nicht an: Die Oberfläche fragt die Messwerte selbst nach (einmal je Minute genügt). - **Wächter** (Rolle `homelab`, `pruefe_host`): gelb, wenn der Proxmox-Host in den letzten 10 Minuten (mindestens 8 Minutenpunkte) durchgehend über 95 % RAM (`MC_WAECHTER_HOST_RAM`) oder über 90 °C CPU (`MC_WAECHTER_HOST_TEMP`) lag.