Aufbau (Speicher, Taktgeber, Ausfuehrer-Faden, Schnittstellen, Waechter), Ablage und Groesse der Dateien, Aufbewahrung, Handgriffe zum Nachsehen und der Hinweis, dass der Ausfuehrer mit ausfuehrer-einrichten.sh kommt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
32 KiB
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.
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
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), 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(boxoderhomelab, Standardbox),MC_INSTANZ(Anzeigename, Standard „Box-Wart" bzw. „Homelab"),MC_DATEN_DIR(Zustandsdateien; Standard/srv/modelsauf der Box,/var/lib/mc2im Homelab),MC_PARTNER_URL(Basis-URL der anderen Instanz, leer = keine) undMC_PARTNER_NAME.backend/kern/zeit.py: die eine ZeitzoneMC_LOCAL_TZ(StandardEurope/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
boxhängt alle Box-Router ein. Rollehomelablä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.pybetreibt Re-Warm und Config-Watch nur in der Rollebox; die Prüfungen des Wächters im Homelab stehen im Abschnitt „Der Homelab-Teil“. /api/healthnenntrolleundinstanz;engine_reachable,gateway_reachableundbraingibt es nur auf der Box.GET /api/partnerliefert das Lebenszeichen der anderen Instanz./api/partner/<pfad>reicht an<partner>/api/<pfad>weiter (alle Methoden, nach der Herkunftsprüfung).partner/…undstreamwerden 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_ROLLEnochMC_PARTNER_URL: Die Box läuft in der Rolleboxund ohne Partner, bis die zweite Instanz eingerichtet ist.
Aufträge (seit 24.09., Phase 2b)
- Eigene Einheiten:
services/jobengine.pystartet jeden Auftrag (Update, Modell-Download) persystemd-runals Einheitmc2-job-<id>des Nutzer-Managers (als root: des System-Managers).RuntimeMaxSecist 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) undmodell: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,unbekanntmit 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 alsfestgehalten, gescheiterte Prüfungen alsunbekannt.GET /api/zieleliefert 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
<Datenordner>/mc2-update-verlauf.jsonl: je Baustein eine JSON-Zeile (lauf,anlass,ts,baustein,ergebnis,text). Es schreibenautoupdate.sh(Helferergebnis/verlauf; die Telegram-Texte bleiben Zeichen für Zeichen gleich) und die Update-Knöpfe (Abschluss-Hakenwartung:verlaufder Aufträge, bei „Alle aktualisieren" mit dem Teil, an dem die Kette scheiterte).services/update_verlauf.pyliest ab dem ersten strukturierten Eintrag nur noch ihn, ältere Läufe weiter aus~/mc2-notify.log. Den Hermes-Auftrag des Sonntags-Laufs startetautoupdate.shmit?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),visionundcoder-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 nachttlwieder 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 ancoder-bild, alles andere anvision). Ältere Bilder beschreibtvisioneinmal als Text (Cache je Bild), damit der Rest beim schnellen Modell mit Draft bleibt. model: autoist die Chat-Spur: normalfast, bei langen oder schweren Anfragenheavy(Schwellen in der Routing-Policymc2-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, sagtMC_WARMSETim Drop-inmc2-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 |
/srv/models/mc2-messwerte/box-JJJJ-MM-TT.jsonl |
nur der Steward (Box); im Homelab /var/lib/mc2/mc2-messwerte/<gerät>-… 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.
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.
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 (KopfzeileX-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 Etikettcommunity-scriptoderwatcherträgt und nichtwatcher-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/updateim 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, sonstsicherung_grund), und die eigenen Sicherungen je Gast. - Sicherung statt Snapshot (Ausführer, seit 24.09.): Wo kein Snapshot geht, sichert
sichernden Container pervzdump— auf den ersten lokalen Speicher, der Sicherungen annimmt (Artdir/btrfs, nicht geteilt; heutelocal=/var/lib/vz), oder aufsicherung_speicheraus/etc/mc2-ausfuehrer.json. Nie auf einen Speicher der Artpbs: 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).vzdumpläuft mit--mode snapshot(rootfs auf lvmthin: der Gast läuft durch), sonststop, dazu--remove 0(keine Aufräumregeln des Speichers) und der Notizmc2-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, bekommtvzdumpbzw.pct restoreerst 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“). Schnittstellenrouters/homelab.pyunter/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) →
updatedes 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 → älteremc2--Snapshots bzw.mc2-sicherung-Sicherungen dieses Gasts weg (die neueste bleibt) + Meldung. Läufe in/var/lib/mc2/homelab-laeufe.jsonund im strukturierten Update-Verlauf. Host: Pakete per Knopf mit Warnung, Neustart als eigener Knopf. - Wartezeit nach Skriptänderung (
karenz.py):updatelädtct/<kennung>.shungepinnt von GitHub (community-scripts/ProxmoxVE, Zweigmain) 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=…, überkern/github.py, 15 min gemerkt). Jünger alsMC_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.startenlehnt 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 Auftragsuchenan — 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, schreibtkanal.pyunterflock. - 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, KopfzeileX-API-Key). Docker-Updates laufen zuerst nur als Probelauf (dryRun); echt erst mitMC_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), die Platten der laufenden Container (ab 80 % gelb, ab 90 % rot — ext4 hält 5 % für root zurück; der Ausführer schicktplattemit), 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 incomponents/homelab/undviews/Homelab.tsx.
- Cockpit (
- Meldungen ohne Hermes:
notify.shim 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.
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.
flowchart LR
ST["mc2-steward (Box)<br/>jede Minute"] -->|box-…jsonl| MR[("mc2-messwerte/<br/>kern/messreihen.py")]
AF["Ausführer (pve)<br/>eigener Faden, jede Minute"] -->|POST …/ausfuehrer/messwerte| HL["Homelab-Teil<br/>services/homelab/messwerte.py"]
HL -->|pve-…, ct-…, vm-…jsonl| MR2[("mc2-messwerte/<br/>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<Datenordner>/mc2-messwerte/<quelle>-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 undflock; 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:1hin 60-s-Schritten,24hals 5-min-Mittel,7dals 30-min-Mittel (60, 288 bzw. 336 Werte, Schritte auf vollen Vielfachen, der letzte ist der laufende). Ein Schritt ohne Messung bleibtnull, 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=0schaltet ab, ein Trocken- oder Probelauf schreibt nie mit):cpu= Mittel der Minute (aus eigenenpsutil.cpu_times-Ständen gerechnet wiecpu_percent, denn psutil merkt sich den letzten Aufruf je Thread),ramundplatte(Modell-Laufwerk) beim Messen,gpu,temp_cpu,temp_gpuals Mittel von Proben alle 5 s (MC_MESSWERTE_PROBE_S),netz_rx/netz_txin Bytes/s über alle Schnittstellen außerlo,tokens= Prompt- plus Antwort-Tokens pro Minute (Differenz der Gesamtzähler ausservices/token_stats.py). Ein Zähler, der kleiner wird, ergibt in dieser Minutenull. - 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(statfür das CPU-Mittel der Minute,meminfo,loadavg,uptime) undstatvfs("/"), dennpvesh get /nodes/<node>/statusliefert in einem frischen pvesh-Prozess immercpu: 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;vmbr0sähe den Verkehr der Gäste nach draußen nicht), ohne physische Schnittstellevmbr0. CPU-Temperatur aus hwmon (k10tempbzw.zenpowermit Tdie vor Tctl,coretempmit „Package id 0“), sonstnull; auf dem Proxmox-PC gibt es nurk10temp/Tctl, keinthermal_zoneund kein lm-sensors. Die Gäste kommen aus einem einzigen Aufrufpvesh get /cluster/resources --type vm(cpuschon auf die eigenen Kerne gerechnet,mem,disk, …), ihre Netz-Zähler aber aus denselben/proc/net/dev-Zeilen (veth<vmid>iN,tap<vmid>iN; die Stände in/cluster/resourcessind 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 (Etikettmc2) wird wie im Inventar nicht erfasst. Quellenpve,ct-<vmid>,vm-<vmid>. - Schnittstellen:
GET /api/messwerte?zeitraum=1h|24h|7d(nur Rollebox) 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 ausapps.py). Jede Reihe ist eine Liste[t, wert](t = Beginn des Schritts);temp_cpugibt es als Reihe nur beim Host, inaktuellstehentemp_cpuundload1bei Gästen aufnull.aktuellist der letzte Minutenpunkt, solange er höchstens 3 Minuten alt ist, sonst lauternull. 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.