Merge branch 'wartung/einstellungen-stand-altreste' into wartung/restarbeiten

# Conflicts:
#	deploy/deploy.sh
This commit is contained in:
Hitonabi
2026-09-24 20:47:53 +02:00
27 changed files with 2738 additions and 110 deletions
+13 -8
View File
@@ -24,6 +24,10 @@ BACKUP_DIR="${MC_BACKUP_DIR:-/srv/models/mc2-backups}"
JOB_TIMEOUT="${MC_AUTOUPDATE_JOB_TIMEOUT:-1200}" # Sekunden für den Hermes-Job
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
# Wartungsfenster für den Neustart (seit 24.09.2026 einstellbar): im_wartungsfenster, fenster_text.
# shellcheck source=deploy/wartungsfenster.sh
. "$SRC_DIR/wartungsfenster.sh"
say(){ echo "[autoupdate] $*"; }
notify(){ bash "$SRC_DIR/notify.sh" -s "[Box-Update]" "$1" || true; }
# Dringend = geht auch nachts sofort raus (sonst sammelt notify.sh bis zur Morgenmeldung um 07:00).
@@ -190,8 +194,9 @@ Wahrscheinliche Ursache/Fix (Box-Diagnose): $sugg}"
# laeuft die Box weiter auf dem alten Kernel. Das faellt niemandem auf — am
# 27.08.2026 lag die Box 7 Wochen mit drei ungenutzten Kerneln da.
#
# Der Neustart haengt bewusst am WOECHENTLICHEN Lauf (So 04:30) und nicht an
# einem eigenen Timer: ein Ort fuer alle Automatik (Lehre vom 20.08.).
# Der Neustart haengt bewusst am WOECHENTLICHEN Lauf (Standard So 04:30, in den
# Einstellungen der Oberflaeche verschiebbar) und nicht an einem eigenen Timer:
# ein Ort fuer alle Automatik (Lehre vom 20.08.).
# Er passiert NUR, wenn Ubuntu ihn selbst anfordert (/var/run/reboot-required).
#
# WICHTIG: Die Linger-Pruefung ist die entscheidende Zeile hier. Ohne Linger=yes
@@ -207,14 +212,14 @@ reboot_wenn_noetig(){
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.
# Sicherung 0 (24.09.2026): neu gestartet wird nur im Wartungsfenster — drei Stunden ab der vollen
# Stunde des eingestellten Update-Beginns, Standard Sonntag 04:00-06:59 (deploy/wartungsfenster.sh).
# Anlass: Ein nachgeholter Lauf (Persistent=true beim Einschalten des Timers) startete die Box
# an einem Donnerstagnachmittag neu. Laeufe ausserhalb des Fensters melden den Neustart nur an.
local tag stunde
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."
if [ "${MC_AUTOUPDATE_REBOOT_JETZT:-0}" != "1" ] && ! im_wartungsfenster; then
local fenster; fenster="$(fenster_text)"
notify "Neustart steht an (${pakete:-Kernel/libc}), passiert aber erst im Wartungsfenster ($fenster)."
verlauf neustart offen "Neustart steht an (${pakete:-Kernel/libc}), erst im Wartungsfenster ($fenster)."
return 0
fi
+49 -8
View File
@@ -8,8 +8,9 @@
# erst beim nächsten Lauf (die alte Falle: bash lief nach dem Pull mit der alten Datei).
# Stufe 2: Prüftor (deploy/pruefen.sh), Abhängigkeiten, llama-swap-Config nur, wenn sich der
# Abzug in DIESEM Deploy geändert hat, Units/Drop-ins/Cron-Skripte/Skills/Plugins
# ausspielen, Dienste neu starten, Nachprüfung. Scheitert irgendein Schritt: zurück auf
# den alten Stand, Dienste neu starten, dringende Meldung.
# ausspielen (den Radar-Schalter der Oberfläche respektieren), Dienste neu starten,
# Nachprüfung, Stand-Datei schreiben. Scheitert irgendein Schritt: zurück auf den alten
# Stand, Dienste neu starten, dringende Meldung.
#
# Aufruf auf der Box: bash ~/mission-control-v2/deploy/deploy.sh
# Schalter: MC_DEPLOY_TROTZDEM=1 (auch bei laufenden Jobs), MC_DEPLOY_OHNE_TESTS=1 (Notfall),
@@ -21,6 +22,9 @@ export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
API="http://127.0.0.1:9001"
LSWAP_LIVE="/etc/llama-swap/config.yaml"
DEPLOY_LOG="/srv/models/mc2-deploy.log"
# Einstellungen der Oberfläche (Radar-Schalter, Update-Zeitfenster) und der Software-Stand (seit 24.09.2026).
EINSTELLUNGEN="/srv/models/mc2-einstellungen.json"
STAND_DATEI="/srv/models/mc2-stand.json"
# Units, die ganz dem Repo gehören (deploy.sh spielt sie aus). Aktiviert wird nur, was laufen soll:
# Konsole und Spracherkennung schlafen seit 24.09.2026 (disable --now), ihre Units bleiben aber aktuell.
@@ -42,6 +46,27 @@ SKILLS_ALT="autonomie konzept-fliessband llm-wiki morning-report orchestrator pr
review trend-radar wartung konzept_fliessband llm_wiki morning_report projekt_start
trend_radar betrieb-playbook pc-pfad-cache"
# Radar-Schalter der Oberfläche (Einstellungen → Modell-Radar): Steht er auf aus, bleibt mc2-radar.timer aus.
# Fehlt die Datei oder ist sie unlesbar, gilt „an“ wie vor dem Schalter.
radar_aus() {
python3 - "$EINSTELLUNGEN" <<'PY' 2>/dev/null
import json
import sys
try:
with open(sys.argv[1], encoding="utf-8") as f:
an = (json.load(f).get("radar") or {}).get("an", True)
except (OSError, ValueError, AttributeError):
an = True
sys.exit(0 if an is False else 1)
PY
}
# Software-Stand für die Oberfläche (Einstellungen → Software-Stand): Commit, Betreff, Commit-Datum, Zeitpunkt.
stand_schreiben() {
git log -1 --format='%h%n%cI%n%s' HEAD | python3 deploy/stand-schreiben.py "$STAND_DATEI"
}
# Laufende Update-Aufträge (Gruppe maintenance). Seit Phase 2 überleben Aufträge den Neustart von
# MC2 (eigene systemd-Einheiten) — ein Download darf also weiterlaufen. Ein Update aber führt
# Skripte aus diesem Checkout aus; die darf der Deploy nicht mitten im Lauf austauschen.
@@ -119,22 +144,37 @@ stufe2() {
echo "Hinweis: Die lebende llama-swap-Config weicht vom Repo-Abzug ab (Änderung über die Oberfläche?) — nicht angefasst."
fi
# 4. Units und Drop-ins (alle Repo-Units, damit Box und Repo nicht auseinanderlaufen)
local ziel="$HOME/.config/systemd/user" u
# 4. Units und Drop-ins (alle Repo-Units, damit Box und Repo nicht auseinanderlaufen). Das Drop-in
# mc2-autoupdate.timer.d/zeitfenster.conf gehört der Oberfläche (Einstellungen → Update-Zeitfenster):
# Der Deploy überschreibt und löscht es nie.
local ziel="$HOME/.config/systemd/user" u aktiv="$AKTIV" radar_an=1
for u in $UNITS; do
install -m 644 "deploy/$u" "$ziel/$u"
done
install -D -m 644 deploy/mc2-steward.service.d-warmset.conf "$ziel/mc2-steward.service.d/warmset.conf"
install -D -m 644 deploy/mission-control-2.service.d-override.conf "$ziel/mission-control-2.service.d/override.conf"
systemctl --user daemon-reload
# Radar-Schalter der Oberfläche: Steht er auf aus, bleibt der Timer aus (vorher schaltete jeder Deploy ihn ein).
if radar_aus; then
radar_an=0
aktiv="${aktiv//mc2-radar.timer/}"
systemctl --user disable --now mc2-radar.timer >/dev/null 2>&1 || true
echo "Modell-Radar ist in den Einstellungen ausgeschaltet — es bleibt aus."
fi
# shellcheck disable=SC2086
systemctl --user enable $AKTIV >/dev/null 2>&1
systemctl --user enable $aktiv >/dev/null 2>&1
# shellcheck disable=SC2086
systemctl --user start mc2-radar.timer mc2-backup.timer mc2-morgenmeldung.timer projekte-sync.timer \
mc2-probe-wiederherstellung.timer
systemctl --user start mc2-backup.timer mc2-morgenmeldung.timer projekte-sync.timer mc2-probe-wiederherstellung.timer
# Den Radar-Timer nur ohne Nachholen starten (Persistent=true): Läuft er nicht, erst den Stempel auf jetzt
# setzen — sonst holt er den verpassten Lauf sofort nach, mitten am Tag.
if [ "$radar_an" = 1 ] && ! systemctl --user is-active --quiet mc2-radar.timer; then
mkdir -p "$HOME/.local/share/systemd/timers"
touch "$HOME/.local/share/systemd/timers/stamp-mc2-radar.timer"
systemctl --user start mc2-radar.timer
fi
# Den Update-Timer nur starten, wenn er nicht schon läuft: Mit Persistent=true holt ein frisch
# gestarteter Timer einen verpassten Sonntagslauf sofort nach (so am 24.09.2026 passiert). Neu
# gestartet wird die Box dabei ohnehin nur sonntags zwischen 04 und 07 Uhr (autoupdate.sh).
# gestartet wird die Box dabei ohnehin nur im Wartungsfenster (autoupdate.sh, wartungsfenster.sh).
systemctl --user is-active --quiet mc2-autoupdate.timer || systemctl --user start mc2-autoupdate.timer
# 5. Cron-Skripte für Hermes (~/.hermes/scripts ist die Vorgabe von `hermes cron --script`)
@@ -179,6 +219,7 @@ stufe2() {
trap - ERR
echo "$(date -Is) $neu (vorher ${vorher:0:7})" >> "$DEPLOY_LOG" 2>/dev/null || true
stand_schreiben || echo "Hinweis: Die Stand-Datei $STAND_DATEI ließ sich nicht schreiben."
echo "✅ Deploy $neu ist live (vorher ${vorher:0:7})."
homelab_mitziehen
}
+11 -3
View File
@@ -1,9 +1,10 @@
#!/usr/bin/env bash
# ausrollen.sh — den Homelab-Teil in seinen Container bringen (erstmals und bei jedem Update).
#
# Läuft am PC (Git-Bash, im Repo). Schickt den Stand von HEAD (backend, deploy, frontend/dist) per SSH in
# den Container, lässt einrichten.sh laufen und startet die Dienste neu. Der Container braucht so keinen
# Zugang zu Gitea. Vorher: bash deploy/pruefen.sh (und deploy/probelauf-box.sh).
# Läuft am PC (Git-Bash, im Repo) oder im Deploy der Box. Schickt den Stand von HEAD (backend, deploy,
# frontend/dist) per SSH in den Container, lässt einrichten.sh laufen und startet die Dienste neu. Danach hält
# es den Stand in /var/lib/mc2/mc2-stand.json fest. Der Container braucht so keinen Zugang zu Gitea.
# Vorher: bash deploy/pruefen.sh (und deploy/probelauf-box.sh).
#
# Nutzung: bash deploy/homelab/ausrollen.sh <ip-des-containers> [partner-url]
set -euo pipefail
@@ -17,3 +18,10 @@ git archive --format=tar HEAD backend deploy frontend/dist ruff.toml \
| ssh -o BatchMode=yes "$ZIEL" "mkdir -p /opt/mc2 && find /opt/mc2 -mindepth 1 -maxdepth 1 ! -name backend -exec rm -rf {} + \
&& find /opt/mc2/backend -mindepth 1 -maxdepth 1 ! -name .venv -exec rm -rf {} + 2>/dev/null; tar -x -C /opt/mc2"
ssh -o BatchMode=yes "$ZIEL" "bash /opt/mc2/deploy/homelab/einrichten.sh '$PARTNER'"
# Software-Stand für die Oberfläche (Einstellungen → Software-Stand, seit 24.09.2026): erst nach erfolgreichem
# Einrichten, also wenn dieser Stand wirklich läuft. Liegt im Datenordner, den das Ausrollen nicht leert. Scheitert
# nur das, gilt das Ausrollen trotzdem als gelungen.
git log -1 --format='%h%n%cI%n%s' HEAD \
| ssh -o BatchMode=yes "$ZIEL" "python3 /opt/mc2/deploy/stand-schreiben.py /var/lib/mc2/mc2-stand.json \
&& chown mc2:mc2 /var/lib/mc2/mc2-stand.json" \
|| echo "Hinweis: Die Stand-Datei im Container ließ sich nicht schreiben."
+24 -4
View File
@@ -102,13 +102,22 @@ env_wert() {
wert="${wert%\'}"; wert="${wert#\'}"
printf '%s' "$wert"
}
# Warum der Zweitweg scheiterte, steht seit 24.09.2026 mit im FALLBACK-Eintrag (der Telegram-Test der Oberfläche
# zeigt es an). Ohne Klammern im Text: services/update_verlauf.py trennt den Eintrag an „): “.
DIREKT_GRUND=""
telegram_direkt() {
local token chat thread text
[ -r "$TG_ENV" ] || return 1
local token chat thread text rc
if [ ! -r "$TG_ENV" ]; then
DIREKT_GRUND="Zugangsdatei $TG_ENV fehlt oder ist nicht lesbar"
return 1
fi
token="$(env_wert TELEGRAM_BOT_TOKEN)"
chat="$(env_wert TELEGRAM_HOME_CHANNEL)"
thread="$(env_wert TELEGRAM_HOME_CHANNEL_THREAD_ID)"
[ -n "$token" ] && [ -n "$chat" ] || return 1
if [ -z "$token" ] || [ -z "$chat" ]; then
DIREKT_GRUND="TELEGRAM_BOT_TOKEN oder TELEGRAM_HOME_CHANNEL fehlt in $TG_ENV"
return 1
fi
text="$MSG"
[ -n "$SUBJECT" ] && text="$SUBJECT
$MSG"
@@ -116,6 +125,15 @@ $MSG"
| curl -sf -m 15 -o /dev/null --config - \
--data-urlencode "chat_id=$chat" --data-urlencode "text=$text" \
${thread:+--data-urlencode "message_thread_id=$thread"}
rc=$?
case "$rc" in
0) return 0 ;;
22) DIREKT_GRUND="Telegram lehnt ab, HTTP-Fehler – Token oder Chat falsch?" ;;
6|7) DIREKT_GRUND="Telegram nicht erreichbar, keine Verbindung" ;;
28) DIREKT_GRUND="Telegram antwortet nicht rechtzeitig" ;;
*) DIREKT_GRUND="curl endete mit Code $rc" ;;
esac
return 1
}
if telegram_direkt; then
# „OK telegram direkt“: services/update_verlauf.py erkennt beide Wege.
@@ -124,7 +142,9 @@ if telegram_direkt; then
fi
# Letzter Ausweg: Logfile + wall — Meldung darf nie verloren gehen.
echo "$TS FALLBACK (telegram fehlgeschlagen: $SEND_OUT): $MSG" >> "$LOG"
GRUND="$SEND_OUT"
[ -n "$DIREKT_GRUND" ] && GRUND="${GRUND:+$GRUND; }Zweitweg: $DIREKT_GRUND"
echo "$TS FALLBACK (telegram fehlgeschlagen: $GRUND): $MSG" >> "$LOG"
if [ "${MC_NOTIFY_OHNE_WALL:-0}" != "1" ]; then
printf '%s\n' "MC2-Meldung: ${SUBJECT:+$SUBJECT }$MSG" | wall 2>/dev/null || true
fi
+58
View File
@@ -0,0 +1,58 @@
"""stand-schreiben.py — den Software-Stand einer Instanz festhalten (seit 24.09.2026).
deploy.sh (KI-Box) und homelab/ausrollen.sh (Container des Homelab-Teils) rufen es nach einem erfolgreichen
Umschalten auf. Es liest drei Zeilen von stdin, genau wie `git log -1 --format='%h%n%cI%n%s'` sie liefert:
Commit kurz, Commit-Datum (ISO), Betreff. Dazu kommt der Zeitpunkt des Deploys, und alles landet als JSON in der
angegebenen Datei (atomar). Die Oberfläche zeigt daraus „Software-Stand“ (backend/services/software_stand.py).
Nur Standardbibliothek: Es läuft mit dem python3 des Systems, auch im Container ohne git.
Nutzung: git log -1 --format='%h%n%cI%n%s' HEAD | python3 deploy/stand-schreiben.py /srv/models/mc2-stand.json
"""
import json
import os
import sys
import tempfile
from datetime import datetime
from pathlib import Path
def stand_aus(zeilen: list[str], jetzt: datetime) -> dict:
"""Die drei git-Zeilen und der Zeitpunkt des Deploys als Stand. Leere Felder werden None."""
commit, datum, betreff = ([z.strip() for z in zeilen] + ["", "", ""])[:3]
return {"commit": commit or None, "betreff": betreff or None, "commit_datum": datum or None,
"deploy_zeit": jetzt.isoformat(timespec="seconds")}
def schreiben(ziel: Path, stand: dict) -> None:
"""Atomar: erst eine Nachbardatei, dann umbenennen — ein Leser sieht nie eine halbe Datei."""
ziel.parent.mkdir(parents=True, exist_ok=True)
fd, tmp = tempfile.mkstemp(prefix=".mc2-stand-", suffix=".tmp", dir=ziel.parent)
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(stand, f, ensure_ascii=False, indent=1)
os.chmod(tmp, 0o644)
os.replace(tmp, ziel)
except BaseException:
Path(tmp).unlink(missing_ok=True)
raise
def main(argv: list[str]) -> int:
if len(argv) != 2:
print("Nutzung: git log -1 --format='%h%n%cI%n%s' | python3 stand-schreiben.py <datei>", file=sys.stderr)
return 2
# Bytes lesen und selbst als UTF-8 deuten: Im Container ist die Locale oft C/POSIX.
zeilen = sys.stdin.buffer.read().decode("utf-8", "replace").splitlines()
stand = stand_aus(zeilen, datetime.now().astimezone())
if not stand["commit"]:
print("Kein Commit auf stdin — Stand nicht geschrieben.", file=sys.stderr)
return 1
schreiben(Path(argv[1]), stand)
print(f"Stand {stand['commit']} festgehalten in {argv[1]}.")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# wartungsfenster.sh — wann die Box nach einem Update neu starten darf (zum Einbinden, seit 24.09.2026).
#
# autoupdate.sh bindet es ein (source). Das Fenster beginnt zur vollen Stunde des eingestellten Update-Beginns und
# dauert drei Stunden: Standard So 04:30 → Sonntag 04:00–06:59 (so stand es bis 24.09. fest in autoupdate.sh).
# Den Beginn stellt die Oberfläche ein (Einstellungen → Update-Zeitfenster); sie legt ihn in der Einstellungsdatei
# ab und stellt den Timer um (backend/services/einstellungen.py, dort steht dieselbe Regel als Text für die Anzeige).
#
# Test-Schalter: MC_WARTUNGSFENSTER="<Tag 1-7> <HH:MM>" statt der Datei, MC_FENSTER_JETZT="<Tag 1-7> <HH:MM>"
# (Zeit vortäuschen), MC_EINSTELLUNGEN (andere Datei).
WARTUNG_EINSTELLUNGEN="${MC_EINSTELLUNGEN:-/srv/models/mc2-einstellungen.json}"
WARTUNG_DAUER_MIN=180
WARTUNG_TAGE=(Montag Dienstag Mittwoch Donnerstag Freitag Samstag Sonntag)
# Setzt FENSTER_TAG (1 = Montag … 7 = Sonntag) und FENSTER_STUNDE. Fehlt die Datei oder steht Unsinn darin: So 04:30.
fenster_lesen() {
local roh="${MC_WARTUNGSFENSTER:-}" tag uhr
if [ -z "$roh" ] && [ -r "$WARTUNG_EINSTELLUNGEN" ] && command -v jq >/dev/null 2>&1; then
roh="$(jq -r '"\(.update_fenster.tag // 7) \(.update_fenster.uhrzeit // "04:30")"' "$WARTUNG_EINSTELLUNGEN" \
2>/dev/null)"
fi
tag="${roh%% *}"
uhr="${roh#* }"
if ! [[ "$tag" =~ ^[1-7]$ && "$uhr" =~ ^([01][0-9]|2[0-3]):[0-5][0-9]$ ]]; then
tag=7
uhr="04:30"
fi
FENSTER_TAG="$tag"
FENSTER_STUNDE="$((10#${uhr%%:*}))"
}
# Erfolg (0), wenn jetzt Neustart-Zeit ist. Rechnet in Minuten ab Montag 00:00, damit ein Fenster über Mitternacht
# (Samstag 23:00 – Sonntag 01:59) und über das Wochenende hinaus (Sonntag 23:00 – Montag 01:59) stimmt.
im_wartungsfenster() {
fenster_lesen
local jetzt="${MC_FENSTER_JETZT:-$(date '+%u %H:%M')}" tag uhr minute beginn
tag="${jetzt%% *}"
uhr="${jetzt#* }"
minute=$(( (tag - 1) * 1440 + 10#${uhr%%:*} * 60 + 10#${uhr##*:} ))
beginn=$(( (FENSTER_TAG - 1) * 1440 + FENSTER_STUNDE * 60 ))
[ $(( (minute - beginn + 10080) % 10080 )) -lt "$WARTUNG_DAUER_MIN" ]
}
# Das Fenster in Worten, etwa „Sonntag 04:00–06:59“ oder „Samstag 23:00 – Sonntag 01:59“.
fenster_text() {
fenster_lesen
local ende=$(( FENSTER_STUNDE * 60 + WARTUNG_DAUER_MIN - 1 )) tag_ende="$FENSTER_TAG" von bis
if [ "$ende" -ge 1440 ]; then
ende=$(( ende - 1440 ))
tag_ende=$(( FENSTER_TAG % 7 + 1 ))
fi
von="$(printf '%02d:00' "$FENSTER_STUNDE")"
bis="$(printf '%02d:%02d' $(( ende / 60 )) $(( ende % 60 )))"
if [ "$tag_ende" = "$FENSTER_TAG" ]; then
printf '%s %s–%s' "${WARTUNG_TAGE[FENSTER_TAG - 1]}" "$von" "$bis"
else
printf '%s %s – %s %s' "${WARTUNG_TAGE[FENSTER_TAG - 1]}" "$von" "${WARTUNG_TAGE[tag_ende - 1]}" "$bis"
fi
}