diff --git a/deploy/jobs/README.md b/deploy/jobs/README.md new file mode 100644 index 0000000..c2dc3c7 --- /dev/null +++ b/deploy/jobs/README.md @@ -0,0 +1,72 @@ +# Die drei Jobs (KISS-Umbau, 21.08.2026) + +Vorher: **4 Hermes-Crons + 6 systemd-Timer**, verteilt auf zwei Mechanismen. +Deshalb fiel am 20.08. tagelang niemandem auf, dass ein Waechter fehlte — +niemand schaut an zwei Orten nach. + +Jetzt: **drei Jobs, ein Ort.** `hermes cron list` zeigt die gesamte Automatik. + +| Job | Wann | Art | Skript | +|---|---|---|---| +| **Daily News Report** | taeglich 07:00 | Agent + Websuche | — | +| **KI und Stack Radar** | samstags 08:00 | `--no-agent` | `stack-radar.sh` → `stack-ist.sh` | +| **Updates am Sonntag** | sonntags 04:30 | `--no-agent` | `sonntags-update.sh` → `autoupdate.sh` | + +Daneben laufen als stille Rohrleitung weiter: `mc2-backup` (03:33) und +`projekte-sync` (stuendlich). Die melden sich nie, die sichern und synchronisieren nur. + +## Der Entwurfsgrundsatz + +> **Fakten sammelt ein Skript, Prosa schreibt das Modell.** + +Der alte Selbsttest war am 20.08. gruen, waehrend vier Dinge kaputt waren: das +Kritiker-Gate zeigte seit einem Tag auf ein umbenanntes Modell (404), Lucys +`SOUL.md` war blockiert, das Dashboard stand ohne Passwort offen, die CI war rot. +Er hatte geprueft, ob Dienste **antworten** — nicht, ob sie **stimmen**. + +`stack-ist.sh` prueft deshalb Ergebnisse: loesen die Rollen-Aliase noch auf? Ist +der letzte Timer-Lauf gutgegangen? Ist eine Sicherung juenger als zwei Tage? +Liefert die oeffentliche Adresse Daten ohne Anmeldung? Beim ersten Lauf hat es +sofort zwei echte Befunde gefunden. + +## Kugelsicher heisst konkret + +- **Das Modell steht nicht im kritischen Pfad.** Antwortet es nicht, gehen die + Rohbefunde trotzdem raus. Nur die Prosa faellt weg, nie die Meldung. +- **Es meldet immer.** „Alles gruen" ist ein Ergebnis, kein Grund zu schweigen. +- **Kein `set -e`.** Ein einzelner fehlschlagender Test darf den Bericht nicht + abschneiden. +- **Ein Melderweg:** `deploy/notify.sh` erreicht Telegram **und** Lucys + Briefkasten (aus dem sie spricht) in einem Aufruf. Die Jobs laufen mit + `--deliver local`, damit Hermes nicht ein zweites Mal sendet. +- **Kein Fuellmaterial.** Die Prompts verbieten ausgedachte Vorschlaege + ausdruecklich — ein Bericht mit Fuellmaterial wird nicht gelesen, und dann + auch der echte Befund nicht. + +## Ausbringen + +Die Skripte muessen unter `~/.hermes/scripts/` liegen (Vorgabe von `hermes cron --script`). +Quelle ist dieses Verzeichnis: + +```bash +scp deploy/jobs/*.sh hitonabi@192.168.178.151:/home/hitonabi/.hermes/scripts/ +ssh hitonabi@192.168.178.151 'cd ~/.hermes/scripts && sed -i "s/\r$//" *.sh && chmod +x *.sh' +``` + +‼️ Das `sed` ist Pflicht: vom Windows-PC kopierte Dateien haben CRLF, und bash +scheitert daran mit unverstaendlichen Meldungen. + +## Abgeschaltet am 21.08. + +| Weg | Warum | +|---|---| +| Cron `morgen-digest` | geht im Daily News Report auf | +| Cron `tech-radar` | geht im Stack Radar auf | +| Cron `nacht-wartung` | pruefte Lebenszeichen, nicht Ergebnisse | +| Cron `wissens-sync` | **war nie gelaufen** — kein `last_run_at` | +| Timer `mc2-bagatell` | **15 Naechte hintereinander „0 eingespielt"** | +| Timer `mc2-selfsmoke` | geht in `stack-ist.sh` auf | +| Timer `pbs-backup` | doppelt zu `mc2-backup` und seit 21.08. rot (`/srv/models/mem0` gibt es nicht mehr) | +| Timer `mc2-autoupdate` | wird jetzt vom Cron „Updates am Sonntag" gestartet | + +Die Unit-Dateien liegen noch da, nur `disable`d — Rueckbau ist ein Befehl. diff --git a/deploy/jobs/sonntags-update.sh b/deploy/jobs/sonntags-update.sh new file mode 100644 index 0000000..a7a5e8e --- /dev/null +++ b/deploy/jobs/sonntags-update.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# sonntags-update.sh — Job 2: "Updates am Sonntag" (21.08.2026). +# +# Duenner Aufsatz auf deploy/autoupdate.sh. Bewusst KEIN Neubau: autoupdate.sh +# macht seit Juli genau das Richtige — Router -> Engine -> Hermes, jede Ebene +# mit Update-Pruefung, Postcheck, und bei Rot automatischem Rueckbau plus +# Selbst-Pinnung. Es meldet auch schon selbst ueber notify.sh, also Telegram +# UND Lucys Stimme. +# +# Dieser Aufsatz existiert nur, damit der Job unter `hermes cron list` steht +# statt in einem separaten systemd-Timer. Ein Ort fuer alle Automatik — genau +# deshalb ist am 20.08. tagelang niemandem aufgefallen, dass ein Waechter fehlte. +# +# Aufruf ueber Hermes-Cron: +# hermes cron create '30 4 * * 0' --name 'Updates am Sonntag' \ +# --no-agent --script sonntags-update.sh --deliver local +set -uo pipefail + +AUTOUPDATE="${AUTOUPDATE:-$HOME/mission-control-v2/deploy/autoupdate.sh}" +NOTIFY="${NOTIFY:-$HOME/mission-control-v2/deploy/notify.sh}" + +melde() { bash "$NOTIFY" -s "[Box-Update]" "$1" 2>/dev/null || echo "$1" >&2; } + +if [ ! -f "$AUTOUPDATE" ]; then + melde "Sonntags-Update ausgefallen: $AUTOUPDATE gibt es nicht. Nichts wurde aktualisiert." + exit 1 +fi + +# autoupdate.sh meldet waehrend des Laufs selbst (inkl. der KRITISCH-Faelle, in +# denen ein Bericht am Ende zu spaet kaeme). Wir fangen hier nur den Fall ab, +# dass es gar nicht erst durchlaeuft — sonst waere das Schweigen zweideutig. +AUSGABE="$(bash "$AUTOUPDATE" 2>&1)" +CODE=$? + +if [ "$CODE" -ne 0 ]; then + melde "Sonntags-Update mit Fehler beendet (exit $CODE). Letzte Zeilen: + +$(printf '%s\n' "$AUSGABE" | tail -15)" +fi + +# Erfolgsfall: autoupdate.sh hat bereits gemeldet. Leere Ausgabe -> Hermes +# schweigt, also genau eine Nachricht statt zwei. +exit 0 diff --git a/deploy/jobs/stack-ist.sh b/deploy/jobs/stack-ist.sh new file mode 100644 index 0000000..eb7566b --- /dev/null +++ b/deploy/jobs/stack-ist.sh @@ -0,0 +1,177 @@ +#!/usr/bin/env bash +# stack-ist.sh — der IST-Zustand der Box, in Fakten (21.08.2026). +# +# Wird von Hermes-Cron "stack-radar" ueber --script eingespeist: die Ausgabe hier +# landet im Prompt des Agenten, der daraus den Bericht schreibt. +# +# GRUNDSATZ: Dieses Skript prueft ERGEBNISSE, nicht Lebenszeichen. +# Der alte Selbsttest war am 20.08. gruen, waehrend vier Dinge kaputt waren: +# das Kritiker-Gate zeigte auf ein umbenanntes Modell (404), Lucys SOUL.md war +# blockiert, das Dashboard stand ohne Passwort offen, die CI war rot. +# "Dienst antwortet" haette das nie gefunden. Deshalb wird hier nachgesehen, +# ob Dinge STIMMEN — nicht ob sie laufen. +# +# Nie mit set -e: ein einzelner fehlschlagender Test darf den Bericht nicht +# abschneiden. Fehler werden gemeldet, nicht verschwiegen. +set -uo pipefail + +MC2="${MC2_URL:-http://127.0.0.1:9001}" +SWAP="${SWAP_URL:-http://127.0.0.1:8080}" +GITEA="${GITEA_URL:-http://192.168.178.153:3000}" +OEFFENTLICH="${OEFFENTLICH_URL:-https://mc.tobisniceshomelab.ddnsfree.com}" + +ok() { printf ' OK %s\n' "$*"; } +warn() { printf ' ACHTUNG %s\n' "$*"; } +bad() { printf ' ROT %s\n' "$*"; } + +echo "IST-ZUSTAND DER BOX — $(date '+%d.%m.%Y %H:%M')" +echo + +# ---------------------------------------------------------------- Dienste --- +echo "DIENSTE" +# systemd stellt gescheiterten Units ein "●" voran — das muss weg, sonst meldet +# der Bericht lauter Aufzaehlungspunkte als kaputte Dienste. +# EGAL: Units, die dauerhaft scheitern und niemanden stoeren. Ein Bericht, der +# jeden Tag denselben harmlosen Fehler zeigt, erzieht zum Wegsehen. +EGAL="at-spi-dbus-bus.service" +fehl=$(systemctl --user list-units --type=service --state=failed --no-legend --no-pager 2>/dev/null \ + | sed 's/^[^a-zA-Z]*//' | awk '{print $1}') +gefunden=0 +for d in $fehl; do + case " $EGAL " in *" $d "*) continue ;; esac + bad "Dienst gescheitert: $d"; gefunden=1 +done +[ "$gefunden" = "0" ] && ok "kein Dienst im Fehlerzustand" + +for d in mc2-gateway mission-control-2 hermes-gateway voice-service; do + systemctl --user is-active --quiet "$d.service" 2>/dev/null \ + && ok "$d laeuft" || bad "$d laeuft NICHT" +done +systemctl is-active --quiet llama-swap.service 2>/dev/null \ + && ok "llama-swap laeuft" || bad "llama-swap laeuft NICHT" +echo + +# ------------------------------------------------------------------ Timer --- +# Nicht "ist der Timer aktiv", sondern "ist der letzte Lauf gutgegangen". +echo "TIMER — letzter Lauf" +for t in $(systemctl --user list-timers --no-legend --no-pager 2>/dev/null | awk '{print $NF}'); do + dienst="${t%.service}.service" + case "$dienst" in launchpadlib-*) continue ;; esac # Ubuntu-Eigenes, nicht unseres + code=$(systemctl --user show -p ExecMainStatus --value "$dienst" 2>/dev/null) + wann=$(systemctl --user show -p ExecMainExitTimestamp --value "$dienst" 2>/dev/null | cut -d' ' -f2,3) + if [ -z "$wann" ]; then warn "$dienst ist eingerichtet, aber NIE gelaufen" + elif [ "${code:-0}" = "0" ]; then ok "$dienst zuletzt $wann" + else bad "$dienst zuletzt GESCHEITERT (exit $code) am $wann"; fi +done +echo + +# --------------------------------------------------------- Modell-Aliase --- +# Der Fund vom 19./20.08.: Skripte und Configs nennen Modelle beim Eigennamen, +# das Modell wird umbenannt, und alles laeuft still in 404. Hier wird geprueft, +# ob jeder Alias, den irgendwer benutzt, heute noch antwortet. +echo "MODELL-ALIASE (Rollen muessen aufloesen)" +liste=$(curl -s -m 10 "$MC2/v1/models" 2>/dev/null \ + | python3 -c 'import sys,json; print(" ".join(m["id"] for m in json.load(sys.stdin)["data"]))' 2>/dev/null) +if [ -z "$liste" ]; then bad "Modell-Liste nicht abrufbar ueber $MC2/v1" +else + ok "verfuegbar: $liste" + for rolle in hermes fast heavy; do + case " $liste " in + *" $rolle "*) ok "Rolle '$rolle' loest auf" ;; + *) bad "Rolle '$rolle' FEHLT — Skripte, die sie nennen, laufen in 404" ;; + esac + done +fi +echo + +# ------------------------------------------------------------- Erreichbar --- +echo "ERREICHBARKEIT" +for paar in "MC2:$MC2/api/health" "llama-swap:$SWAP/v1/models" "Gitea:$GITEA/api/v1/version"; do + name="${paar%%:*}"; url="${paar#*:}" + code=$(curl -s -m 10 -o /dev/null -w '%{http_code}' "$url" 2>/dev/null) + [ "$code" = "200" ] && ok "$name antwortet (200)" || bad "$name antwortet mit '$code'" +done +echo + +# ------------------------------------------------ Offen im Internet? -------- +# Der Fund vom 21.08.: MC2 hing ueber den Nginx Proxy Manager ungeschuetzt im +# Netz. ufw sah nichts davon, weil der Proxy im LAN steht. Also von aussen +# nachsehen — und zwar auf einem Endpunkt, der ECHTE Daten liefert. +echo "OEFFENTLICHE ERREICHBARKEIT" +code=$(curl -s -m 15 -o /tmp/.ist-oeff -w '%{http_code}' "$OEFFENTLICH/api/system/status" 2>/dev/null) +if [ "$code" = "200" ] && head -c1 /tmp/.ist-oeff 2>/dev/null | grep -q '{'; then + bad "MC2 liefert Systemdaten OHNE Anmeldung ins Internet ($OEFFENTLICH)" +elif [ "$code" = "401" ] || [ "$code" = "403" ]; then + ok "oeffentlicher Zugang verlangt Anmeldung ($code)" +elif [ "$code" = "000" ]; then + ok "von aussen nicht erreichbar" +else + warn "oeffentlicher Zugang antwortet mit '$code' — nachsehen" +fi +rm -f /tmp/.ist-oeff +echo + +# ----------------------------------------------------------------- Ports --- +echo "OFFENE PORTS (ufw)" +sudo -n ufw status 2>/dev/null | grep ALLOW | sed 's/^/ /' || warn "ufw-Status nicht lesbar" +echo + +# --------------------------------------------------------------- CI-Ampel --- +echo "CI-AMPEL (Gitea)" +T=$(tr -d '[:space:]' < "$HOME/.config/gitea/create-token" 2>/dev/null) +if [ -z "$T" ]; then warn "kein Gitea-Token — CI nicht pruefbar" +else + curl -s -m 20 -H "Authorization: token $T" "$GITEA/api/v1/user/repos?limit=100" 2>/dev/null \ + | python3 -c ' +import sys, json +try: + repos = json.load(sys.stdin) +except Exception as e: + print(" ACHTUNG Repo-Liste nicht lesbar:", e); sys.exit(0) +print(f" OK {len(repos)} Repos in Gitea") +' 2>/dev/null || warn "Repo-Liste nicht lesbar" +fi +echo + +# ------------------------------------------------------------- Sicherung --- +echo "SICHERUNGEN" +# Pfad aus backup.sh: DEST_DIR="$MODELS_DIR/mc2-backups" +SICHER="${MC_BACKUP_DIR:-/srv/models/mc2-backups}" +neuestes=$(find "$SICHER" -maxdepth 2 -type f -newermt '-2 days' 2>/dev/null | head -1) +anzahl=$(find "$SICHER" -maxdepth 2 -type f 2>/dev/null | wc -l) +if [ -n "$neuestes" ]; then + ok "Sicherung juenger als 2 Tage vorhanden ($anzahl Staende in $SICHER)" +elif [ "$anzahl" -gt 0 ]; then + bad "juengste Sicherung ist AELTER als 2 Tage ($anzahl Staende in $SICHER)" +else + bad "gar keine Sicherung in $SICHER" +fi +echo + +# ------------------------------------------------------------------ Platz --- +echo "PLATZ UND LAST" +# Nur eindeutige Dateisysteme: / und /srv liegen hier auf demselben Geraet, +# doppelt gemeldet las sich das wie ein Befund. +df -h --output=source,size,used,avail,pcent,target / /srv 2>/dev/null \ + | awk '!gesehen[$1]++ {printf " %s\n", $0}' +echo " RAM: $(free -h | awk '/^Mem:/{print $3" von "$2" belegt"}')" +echo + +# ------------------------------------------------------------- Warm/Kalt --- +echo "MODELLE IM SPEICHER" +curl -s -m 10 "$SWAP/running" 2>/dev/null \ + | python3 -c ' +import sys, json +try: + d = json.load(sys.stdin) +except Exception: + print(" ACHTUNG llama-swap-Status nicht lesbar"); sys.exit(0) +lauf = d.get("running", []) +if not lauf: + print(" OK kein Modell geladen (alles kalt)") +for m in lauf: + print(" OK " + str(m.get("model")) + " - " + str(m.get("state"))) +' + +echo +echo "ENDE IST-ZUSTAND" diff --git a/deploy/jobs/stack-radar.sh b/deploy/jobs/stack-radar.sh new file mode 100644 index 0000000..90ed702 --- /dev/null +++ b/deploy/jobs/stack-radar.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# stack-radar.sh — Job 3: "KI & Stack Radar" (21.08.2026). +# +# Ablauf: IST-Zustand holen (Fakten) -> Modell bewerten lassen (Prosa) +# -> ueber notify.sh melden (Telegram + Lucys Stimme) +# +# KUGELSICHER heisst hier konkret: +# * Die Fakten kommen aus stack-ist.sh, nicht aus dem Modell. Faellt das Modell +# aus, werden die ROTEN Zeilen trotzdem gemeldet — roh statt gar nicht. +# * Es meldet IMMER. "Alles gruen" ist ein Ergebnis, kein Grund zu schweigen. +# * Kein set -e: ein einzelner Fehlschlag darf den Bericht nicht abschneiden. +# +# Aufruf ueber Hermes-Cron: +# hermes cron create '0 8 * * 6' --name 'KI & Stack Radar' \ +# --no-agent --script stack-radar.sh --deliver local +# (--no-agent, weil dieses Skript selbst meldet. Leere Ausgabe = Hermes schweigt.) +set -uo pipefail + +HIER="$(cd "$(dirname "$0")" && pwd)" +IST_SKRIPT="${IST_SKRIPT:-$HOME/.hermes/scripts/stack-ist.sh}" +NOTIFY="${NOTIFY:-$HOME/mission-control-v2/deploy/notify.sh}" +GATEWAY="${GATEWAY_URL:-http://127.0.0.1:9010/v1}" +MODELL="${RADAR_MODELL:-fast}" # bewusst NICHT heavy: das laedt 68 GB und der + # Radar meldet danach seinen eigenen Speicherverbrauch als Befund + +[ -x "$IST_SKRIPT" ] || IST_SKRIPT="$HIER/stack-ist.sh" + +melde() { + if [ -x "$NOTIFY" ] || [ -f "$NOTIFY" ]; then + bash "$NOTIFY" -s "[Stack-Radar]" "$1" + else + # Letzte Rueckfallebene: wenigstens ins Log, damit nichts still verschwindet. + echo "[stack-radar] MELDEWEG FEHLT — Bericht folgt roh:" >&2 + echo "$1" >&2 + fi +} + +# ------------------------------------------------------------- 1. Fakten --- +IST="$(bash "$IST_SKRIPT" 2>&1)" +if [ -z "$IST" ]; then + melde "Radar konnte den IST-Zustand nicht erheben — $IST_SKRIPT lieferte nichts. Das ist selbst ein Befund: bitte nachsehen." + exit 1 +fi + +ROT="$(printf '%s\n' "$IST" | grep -E '^\s+(ROT|ACHTUNG)' || true)" +ANZ_ROT="$(printf '%s\n' "$ROT" | grep -c 'ROT' || true)" + +# ------------------------------------------------------- 2. Bewertung ------ +# Das Modell bekommt NUR die Fakten und darf nichts dazuerfinden. Es soll +# einordnen, nicht ermitteln. +PROMPT="Du bist der Stack-Radar einer selbstgehosteten KI-Box. Unten steht der +gemessene IST-Zustand. Schreibe einen kurzen Bericht auf Deutsch, per Du. + +Regeln: +- Erfinde NICHTS. Nur was unten steht. +- Beginne mit einer Zeile Gesamturteil. +- Dann: was ist ROT oder ACHTUNG, und was waere jeweils der naechste Schritt. +- Verbesserungsvorschlaege NUR, wenn sich aus den Zahlen unten wirklich etwas + Konkretes ergibt. Gibt es nichts, lass den Abschnitt ersatzlos weg. Schreibe + lieber gar keinen Vorschlag als einen ausgedachten - ein Bericht mit + Fuellmaterial wird nicht gelesen. +- Was du an den Messwerten selbst merkwuerdig findest, gehoert unter ACHTUNG, + nicht unter Verbesserungen. +- Wenn alles gruen ist, sag das in einem Satz und hoere auf. +- Keine Ueberschriften-Deko, keine Emojis, hoechstens 200 Woerter. + +IST-ZUSTAND: +$IST" + +ANTWORT="" +if NUTZLAST="$(python3 -c ' +import json, sys +print(json.dumps({ + "model": sys.argv[1], + "messages": [{"role": "user", "content": sys.argv[2]}], + "max_tokens": 900, + "temperature": 0.3, +}))' "$MODELL" "$PROMPT" 2>/dev/null)"; then + ANTWORT="$(curl -s -m 300 -X POST "$GATEWAY/chat/completions" \ + -H 'Content-Type: application/json' -d "$NUTZLAST" 2>/dev/null \ + | python3 -c ' +import json, sys +try: + print(json.load(sys.stdin)["choices"][0]["message"]["content"].strip()) +except Exception: + pass' 2>/dev/null)" +fi + +# ------------------------------------------------------------- 3. Melden --- +if [ -n "$ANTWORT" ]; then + melde "$ANTWORT" +else + # Modell stumm oder kaputt -> die Fakten gehen TROTZDEM raus. Genau dafuer + # steht das Modell nicht im kritischen Pfad. + if [ -n "$ROT" ]; then + melde "Radar: Bewertung durch das Modell hat nicht geklappt — hier die Rohbefunde: + +$ROT" + else + melde "Radar: alles gruen (Bewertung durch das Modell hat nicht geklappt, die Messung schon)." + fi +fi + +# Hermes bekommt eine LEERE Ausgabe -> keine zweite Nachricht. Gemeldet wurde +# bereits ueber notify.sh, und das erreicht Telegram UND Lucys Stimme. +exit 0