Feat: Wartungs-Riegel — nur EIN System-Update gleichzeitig

Bisher kein Schutz: Doppelklick/zwei Tabs konnten zwei update-engine.sh parallel
starten → racende .bak-Sicherung + parallele llama-swap-Restarts + sich gegenseitig
als kaputt sehende Postchecks.

Backend: jobengine.start_job bekommt group-Tag + active_in_group(); os/engine/swap/
hermes-update sind group="maintenance" und lehnen einen Start ab, solange eines laeuft
({ok:false, status:"busy", running:<label>}). Schuetzt auch gegen parallele Sessions.

Frontend: laeuft ein Wartungs-Job, zeigt der Drawer ein Banner "Update laeuft: <label>"
und sperrt "Jetzt aktualisieren" + "Nach Updates suchen". Logs/Job-Fortschritt/Dienste
bleiben voll nutzbar (Dashboard nicht hart gesperrt). Busy-Antwort wird als Hinweis gezeigt.
Modell-Upgrades bleiben erlaubt (parallel unkritisch).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-28 11:44:17 +02:00
parent 4ee016d3a7
commit 71d1d99c44
7 changed files with 211 additions and 158 deletions
+12 -2
View File
@@ -147,12 +147,13 @@ def attach_download_progress(job_id: str, local_dir: str, total_bytes: int) -> N
threading.Thread(target=_watch, daemon=True).start()
def start_job(args: list[str], label: str, env: dict | None = None, on_done=None, sudo_password: str | None = None) -> str:
def start_job(args: list[str], label: str, env: dict | None = None, on_done=None,
sudo_password: str | None = None, group: str | None = None) -> str:
job_id = uuid.uuid4().hex[:12]
# Mask password in log if present in args
log_args = list(args)
JOBS[job_id] = {
"id": job_id, "label": label, "state": "queued",
"id": job_id, "label": label, "state": "queued", "group": group,
"log": ["$ " + " ".join(shlex.quote(a) for a in log_args)],
"returncode": None, "started_at": time.time(), "finished_at": None,
}
@@ -162,6 +163,15 @@ def start_job(args: list[str], label: str, env: dict | None = None, on_done=None
return job_id
def active_in_group(group: str) -> dict | None:
"""Erster laufender/wartender Job einer Gruppe (z.B. 'maintenance'), sonst None.
Basis für den Wartungs-Riegel: nur EIN System-Update gleichzeitig."""
for j in JOBS.values():
if j.get("group") == group and j.get("state") in ("running", "queued"):
return j
return None
def cancel_job(job_id: str) -> bool:
job = JOBS.get(job_id)
if not job or job["state"] in ("done", "failed", "canceled"):