#!/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 --- # ---------------------------------------------------------------- Curator --- # Hierher gefaltet am 22.08.2026 aus dem Repo `curator-staleness-check`. # Das war ein eigener Wachhund mit eigenem systemd-Timer und eigener CI-Ampel — # und er ueberwachte einen Cron-Job ueber eine FEST VERDRAHTETE ID # (a47910e2ef05), den es laengst nicht mehr gibt. Er haette bei jedem Lauf # Alarm geschlagen fuer etwas, das gar nicht existiert. # Hier steht dieselbe Frage in vier Zeilen, ohne Repo, Timer und Ampel. echo "CURATOR" AN=$(grep -A3 '^curator:' "$HOME/.hermes/config.yaml" 2>/dev/null | grep -m1 'enabled:' | awk '{print $2}') if [ "${AN:-false}" != "true" ]; then ok "Curator ist abgeschaltet — nichts zu ueberwachen" else # %Ts liefert GANZE Sekunden. %T@ waere ein Float — bash rechnet damit nicht # und bricht mit "invalid arithmetic operator" ab. JUENGSTES=$(find "$HOME/.hermes/logs/curator" -type f -name '*.log' -printf '%Ts\n' 2>/dev/null | sort -rn | head -1) if [ -z "$JUENGSTES" ]; then bad "Curator ist AN, hat aber noch nie etwas protokolliert" else TAGE=$(( ( $(date +%s) - JUENGSTES ) / 86400 )) if [ "$TAGE" -le 7 ]; then ok "Curator war vor $TAGE Tagen zuletzt aktiv" elif [ "$TAGE" -le 21 ]; then warn "Curator seit $TAGE Tagen still — laeuft er noch?" else bad "Curator seit $TAGE Tagen still, obwohl eingeschaltet" fi fi fi echo 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"