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:
co-authored by
Claude Opus 5.5
parent
22bb8c672b
commit
589c14d5e9
+43
-19
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user