Files
mission-control-v2/deploy/jobs/stack-ist.sh
T
HitonabiandClaude Opus 5 2210bf5c40
Ampel / ampel (push) Successful in 22s
feat(radar): Curator-Pruefung in den Stack-Radar gefaltet
Das Repo curator-staleness-check war ein eigener Wachhund mit eigenem
systemd-Timer und eigener CI-Ampel - und 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 nicht existiert.

Dieselbe Frage steht jetzt in vier Zeilen im Radar. Repo archiviert.
Falle dabei: find -printf %T@ liefert einen Float, bash bricht damit ab - %Ts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 13:06:25 +02:00

205 lines
8.8 KiB
Bash

#!/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"