phase2d: gemeinsames Ziel-Modell, KI-Box als erster Adapter, strukturierter Update-Verlauf

- kern/ziele.py: Ziel -> Bausteine mit Stand (neu/aktuell/unbekannt/festgehalten/wird-geprueft),
  Versionen und dem Knopf, der das Update anstoesst; gleiches Modell fuer Box und Homelab
- services/box_updates.py: Update-Zwischenspeicher aus dem Router geholt, ki_box_ziel() als
  Box-Adapter; GET /api/ziele
- Update-Verlauf strukturiert: autoupdate.sh und die Update-Knoepfe schreiben je Baustein eine
  JSON-Zeile (mc2-update-verlauf.jsonl); die Meldungstexte bleiben gleich, aeltere Laeufe kommen
  weiter aus dem Meldeprotokoll. Updates per Knopf erscheinen jetzt auch im Verlauf.
- jobengine: Abschluss-Haken fuer jedes Ende (neben der Nacharbeit fuer den Erfolg)
- Sonntags-Lauf setzt seinen Anlass; Hermes-Job aus dem Lauf schreibt nicht doppelt
- Vertragstest Bash-Schreiber gegen Python-Leser (laeuft auf der Box mit jq)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-24 18:14:42 +02:00
co-authored by Claude Opus 5.5
parent 22bb8c672b
commit 589c14d5e9
12 changed files with 649 additions and 116 deletions
+43 -19
View File
@@ -31,6 +31,24 @@ notify_dringend(){ bash "$SRC_DIR/notify.sh" -d -s "[Box-Update]" "$1" || true;
SUMMARY=() # eine Zeile je Ebene für die Abschluss-Meldung
# ── Strukturierter Verlauf (seit 24.09.2026) ─────────────────────────────────
# Je Baustein eine JSON-Zeile; die Oberfläche liest den Update-Verlauf daraus statt aus den
# Telegram-Texten (services/update_verlauf.py). Die Meldungen selbst bleiben, wie sie sind.
VERLAUF="${MC_UPDATE_VERLAUF:-/srv/models/mc2-update-verlauf.jsonl}"
LAUF="$(date -Iseconds)"
ANLASS="${MC_UPDATE_ANLASS:-Update von Hand}"
verlauf(){ # $1 baustein (swap|engine|hermes|neustart|lauf) $2 ergebnis $3 text
jq -cn --arg lauf "$LAUF" --arg anlass "$ANLASS" --arg ts "$(date -Iseconds)" \
--arg b "$1" --arg e "$2" --arg t "$3" \
'{lauf: $lauf, anlass: $anlass, ts: $ts, baustein: $b, ergebnis: $e, text: $t}' \
>> "$VERLAUF" 2>/dev/null || true
}
# Ergebnis eines Bausteins: eine Zeile für die Abschlussmeldung und eine für den Verlauf.
ergebnis(){ # $1 baustein $2 anzeigename $3 ergebnis $4 text
SUMMARY+=("$2: $4")
verlauf "$1" "$3" "$4"
}
# ── Pin-Register ─────────────────────────────────────────────────────────────
pins_init(){ [ -f "$PINS" ] || echo '{}' > "$PINS"; }
is_pinned(){ jq -e --arg k "$1" '.[$k].pinned == true' "$PINS" >/dev/null 2>&1; }
@@ -55,12 +73,12 @@ run_root_update(){ # $1 komponente(swap|engine) $2 skript $3 anzeigename $4
say "$name: Update $installed → $latest wird eingespielt…"
sudo -n /usr/bin/bash "$SRC_DIR/$script"; rc=$?
case "$rc" in
0) SUMMARY+=("$name: $installed → $latest eingespielt, Stack-Check grün.")
0) ergebnis "$comp" "$name" eingespielt "$installed → $latest eingespielt, Stack-Check grün."
notify "$name aktualisiert: $installed → $latest. Stack-Check grün, alles läuft." ;;
1) set_pin "$comp" "$installed" "Update auf $latest zerschoss den Stack-Check; Rollback grün"
SUMMARY+=("$name: $latest FEHLGESCHLAGEN → zurückgerollt auf $installed + GEPINNT.")
ergebnis "$comp" "$name" zurueckgerollt "$latest FEHLGESCHLAGEN → zurückgerollt auf $installed + GEPINNT."
notify "$name-Update auf $latest fehlgeschlagen (Stack-Check rot). Automatisch zurückgerollt auf $installed — läuft wieder. Ebene ist jetzt GEPINNT, künftige Läufe überspringen sie." ;;
*) SUMMARY+=("$name: KRITISCH — Update UND Rollback fehlgeschlagen!")
*) ergebnis "$comp" "$name" fehler "KRITISCH — Update UND Rollback fehlgeschlagen!"
notify_dringend "KRITISCH: $name-Update auf $latest UND Rollback fehlgeschlagen — Stack möglicherweise kaputt. Bitte melden, ich brauche Hilfe (Backup liegt bereit, restore.sh existiert)." ;;
esac
}
@@ -69,25 +87,25 @@ check_layer_root(){ # $1 komponente $2 skript $3 anzeigename $4 details-kind
local comp="$1" script="$2" name="$3" kind="$4" det installed latest
if is_pinned "$comp"; then
say "$name: GEPINNT ($(pin_info "$comp")) — übersprungen."
SUMMARY+=("$name: gepinnt, übersprungen.")
ergebnis "$comp" "$name" festgehalten "gepinnt, übersprungen."
return
fi
det="$(details "$kind")" || { say "$name: Update-Check nicht erreichbar."; SUMMARY+=("$name: Check fehlgeschlagen."); return; }
det="$(details "$kind")" || { say "$name: Update-Check nicht erreichbar."; ergebnis "$comp" "$name" offen "Check fehlgeschlagen."; return; }
installed="$(jq -r '.installed_build // empty' <<<"$det")"
latest="$(jq -r '.latest_build // empty' <<<"$det")"
if [ -z "$installed" ] || [ -z "$latest" ]; then
say "$name: Versionen nicht ermittelbar (installed='$installed' latest='$latest')."
SUMMARY+=("$name: Versions-Check unklar, nichts getan.")
ergebnis "$comp" "$name" offen "Versions-Check unklar, nichts getan."
return
fi
if [ "$latest" -le "$installed" ] 2>/dev/null; then
say "$name: aktuell ($installed)."
SUMMARY+=("$name: aktuell ($installed).")
ergebnis "$comp" "$name" aktuell "aktuell ($installed)."
return
fi
if ! sudo_ok "$script"; then
say "$name: Update $installed → $latest verfügbar, aber sudo-Freischaltung fehlt."
SUMMARY+=("$name: Update $installed → $latest wartet — sudo-Freischaltung fehlt (einmalig deploy/sudoers-mc2-autonomie installieren).")
ergebnis "$comp" "$name" offen "Update $installed → $latest wartet — sudo-Freischaltung fehlt (einmalig deploy/sudoers-mc2-autonomie installieren)."
return
fi
run_root_update "$comp" "$script" "$name" "$installed" "$latest"
@@ -98,23 +116,23 @@ check_hermes(){
local name="Hermes-Agent" det behind old_head resp job_id state t
if is_pinned hermes; then
say "$name: GEPINNT ($(pin_info hermes)) — übersprungen."
SUMMARY+=("$name: gepinnt, übersprungen.")
ergebnis hermes "$name" festgehalten "gepinnt, übersprungen."
return
fi
det="$(details hermes)" || { SUMMARY+=("$name: Check fehlgeschlagen."); return; }
det="$(details hermes)" || { ergebnis hermes "$name" offen "Check fehlgeschlagen."; return; }
behind="$(jq -r '.behind // 0' <<<"$det")"
if [ "${behind:-0}" -eq 0 ] 2>/dev/null; then
say "$name: aktuell."
SUMMARY+=("$name: aktuell.")
ergebnis hermes "$name" aktuell "aktuell."
return
fi
old_head="$(git -C "$HERMES_DIR" rev-parse HEAD 2>/dev/null)"
say "$name: $behind Commits hinterher — Update-Job startet (Backup→update→doctor→Restart→Gehirn-Check)…"
resp="$(curl -sf --max-time 30 -X POST "$API/api/maintenance/hermes-update")" || { SUMMARY+=("$name: Job-Start fehlgeschlagen."); return; }
resp="$(curl -sf --max-time 30 -X POST "$API/api/maintenance/hermes-update?verlauf=0")" || { ergebnis hermes "$name" offen "Job-Start fehlgeschlagen."; return; }
job_id="$(jq -r '.job_id // empty' <<<"$resp")"
if [ -z "$job_id" ]; then
say "$name: kein Job gestartet: $resp"
SUMMARY+=("$name: Job-Start abgelehnt ($(jq -r '.error // .detail // "unbekannt"' <<<"$resp" 2>/dev/null)).")
ergebnis hermes "$name" offen "Job-Start abgelehnt ($(jq -r '.error // .detail // "unbekannt"' <<<"$resp" 2>/dev/null))."
return
fi
t=0
@@ -124,7 +142,7 @@ check_hermes(){
sleep 10; t=$((t + 10))
done
if [ "$state" = "done" ]; then
SUMMARY+=("$name: $behind Commits eingespielt, Gehirn-Check grün.")
ergebnis hermes "$name" eingespielt "$behind Commits eingespielt, Gehirn-Check grün."
notify "Hermes-Agent aktualisiert ($behind Commits). Gehirn-Check (Patches, Tools, Voice) grün — alles läuft."
return
fi
@@ -135,7 +153,7 @@ check_hermes(){
sr_out="$(bash "$SRC_DIR/self-repair.sh" 2>&1)"; sr_rc=$?
echo "$sr_out"
if [ "$sr_rc" -eq 0 ]; then
SUMMARY+=("$name: Config-Bruch nach Update SELBST repariert — neue Version läuft (kein Rollback).")
ergebnis hermes "$name" eingespielt "Config-Bruch nach Update SELBST repariert — neue Version läuft (kein Rollback)."
return
fi
sugg="$(echo "$sr_out" | sed -n 's/^SELFREPAIR_SUGGESTION=//p' | tail -1)"
@@ -156,14 +174,14 @@ check_hermes(){
systemctl --user restart hermes-gateway; sleep 6
if bash "$SRC_DIR/hermes-postcheck.sh" >/dev/null 2>&1; then
set_pin hermes "$old_head" "Update ($behind Commits) riss den Gehirn-Check; Rollback grün"
SUMMARY+=("$name: Update FEHLGESCHLAGEN → zurückgerollt + GEPINNT auf ${old_head:0:9}.")
ergebnis hermes "$name" zurueckgerollt "Update FEHLGESCHLAGEN → zurückgerollt + GEPINNT auf ${old_head:0:9}."
notify "Hermes-Update fehlgeschlagen (Gehirn-Check rot). Selbstreparatur der Config griff nicht, deshalb automatisch zurückgerollt auf ${old_head:0:9} — läuft wieder. Hermes ist jetzt GEPINNT.${sugg:+
Wahrscheinliche Ursache/Fix (Box-Diagnose): $sugg}"
return
fi
fi
SUMMARY+=("$name: KRITISCH — Update UND Rollback fehlgeschlagen!")
ergebnis hermes "$name" fehler "KRITISCH — Update UND Rollback fehlgeschlagen!"
notify_dringend "KRITISCH: Hermes-Update UND Rollback fehlgeschlagen — Gehirn-Check bleibt rot. Bitte melden. (Voll-Restore: deploy/restore.sh mit dem letzten Backup.)"
}
@@ -187,8 +205,7 @@ reboot_wenn_noetig(){
[ "${MC_AUTOUPDATE_REBOOT:-1}" = "1" ] || { say "Neustart per Env abgeschaltet."; return 0; }
[ -f /var/run/reboot-required ] || { say "Kein Neustart noetig."; return 0; }
local pakete; pakete="$(sort -u /var/run/reboot-required.pkgs 2>/dev/null | tr "
" " ")"
local pakete; pakete="$(sort -u /var/run/reboot-required.pkgs 2>/dev/null | tr '\n' ' ')"
# Sicherung 0 (24.09.2026): neu gestartet wird nur im Wartungsfenster, sonntags 04:00-06:59.
# Anlass: Ein nachgeholter Lauf (Persistent=true beim Einschalten des Timers) startete die Box
@@ -197,6 +214,7 @@ reboot_wenn_noetig(){
tag="$(date +%u)"; stunde="$((10#$(date +%H)))"
if [ "${MC_AUTOUPDATE_REBOOT_JETZT:-0}" != "1" ] && { [ "$tag" != "7" ] || [ "$stunde" -lt 4 ] || [ "$stunde" -ge 7 ]; }; then
notify "Neustart steht an (${pakete:-Kernel/libc}), passiert aber erst im Wartungsfenster am Sonntag frueh."
verlauf neustart offen "Neustart steht an (${pakete:-Kernel/libc}), erst im Wartungsfenster am Sonntag früh."
return 0
fi
@@ -206,6 +224,7 @@ reboot_wenn_noetig(){
local nutzer; nutzer="$(id -un)"
if [ "$(loginctl show-user "$nutzer" -p Linger --value 2>/dev/null)" != "yes" ]; then
notify "Neustart NICHT ausgefuehrt: die Box laeuft ohne linger — nach einem Neustart wuerden Steuerpult, Gateway und Waechter nicht von selbst starten. Erst 'loginctl enable-linger $nutzer' setzen, dann von Hand neustarten. Ausstehend: ${pakete:-Kernel/libc}"
verlauf neustart offen "Neustart nicht ausgeführt: Linger fehlt."
return 0
fi
@@ -214,6 +233,7 @@ reboot_wenn_noetig(){
laufend="$(curl -sf --max-time 20 "$API/api/jobs" | jq -r '[.jobs[]? | select(.state=="running")] | length' 2>/dev/null || echo 0)"
if [ "${laufend:-0}" -gt 0 ]; then
notify "Neustart verschoben: es laufen noch $laufend Job(s). Ausstehend: ${pakete:-Kernel/libc}. Naechster Versuch beim Lauf in einer Woche."
verlauf neustart offen "Neustart verschoben: es liefen noch $laufend Aufträge."
return 0
fi
@@ -224,14 +244,17 @@ reboot_wenn_noetig(){
done
if [ -n "$fehlend" ]; then
notify "Neustart NICHT ausgefuehrt: diese Dienste sind nicht fuer den Autostart eingetragen —$fehlend. Erst 'systemctl --user enable' nachholen. Ausstehend: ${pakete:-Kernel/libc}"
verlauf neustart offen "Neustart nicht ausgeführt: Dienste nicht im Autostart ($fehlend)."
return 0
fi
notify "Die Box startet jetzt neu — das OS verlangt es nach dem Update (${pakete:-Kernel/libc}). Sie ist ein paar Minuten weg und meldet sich, sobald alles wieder laeuft. Die Modelle muessen danach neu geladen werden, die erste Anfrage dauert also laenger."
say "Neustart wird ausgeloest (ausstehend: ${pakete:-Kernel/libc})."
verlauf neustart neustart "Die Box startet jetzt neu (${pakete:-Kernel/libc})."
sleep 20 # der Meldung Zeit lassen, rauszugehen
sudo -n systemctl reboot || {
notify "Neustart FEHLGESCHLAGEN: 'sudo systemctl reboot' wurde abgelehnt. Bitte von Hand neustarten. Ausstehend: ${pakete:-Kernel/libc}"
verlauf neustart fehler "Neustart fehlgeschlagen: sudo systemctl reboot wurde abgelehnt."
}
}
@@ -240,6 +263,7 @@ say "Auto-Update-Lauf startet ($(date '+%F %H:%M'))."
pins_init
if ! curl -sf --max-time 20 "$API/api/health" >/dev/null; then
notify "Auto-Update abgebrochen: MC2-API ($API) nicht erreichbar — bitte Box prüfen."
verlauf lauf fehler "Abgebrochen: MC2 war nicht erreichbar."
exit 1
fi
+2
View File
@@ -27,6 +27,8 @@ 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.
# Anlass für den strukturierten Update-Verlauf (seit 24.09.2026); von Hand heißt es „Update von Hand“.
export MC_UPDATE_ANLASS="Updates am Sonntag"
AUSGABE="$(bash "$AUTOUPDATE" 2>&1)"
CODE=$?