einstellungen: Zeitfenster, Radar-Schalter, Telegram-Test, Software-Stand und Altreste

Einstellungen, die etwas einstellen (services/einstellungen.py, /api/einstellungen):
- Update-Zeitfenster: Wochentag und Uhrzeit des Update-Laufs als Drop-in
  mc2-autoupdate.timer.d/zeitfenster.conf. Gegen die Nachhol-Falle von
  Persistent=true (24.09.): Timer vor dem daemon-reload stoppen, Stempel auf
  jetzt, dann starten. Bei Fehler zurück auf das alte Drop-in. Während eines
  Update-Laufs abgelehnt.
- Neustart nur im eingestellten Fenster (deploy/wartungsfenster.sh, drei
  Stunden ab der vollen Stunde des Beginns; Standard So 04:00-06:59).
- Radar an/aus: disable --now bzw. enable + Stempel + start (kein Nachholen).
  deploy.sh respektiert den Schalter und startet den Radar-Timer nur mit
  Stempel; das Zeitfenster-Drop-in fasst er nie an.
- Wächter: schläft der Timer eines Timer-Dienstes, ist ein alter Fehlschlag
  kein Befund mehr (der Dienst selbst ist static und schläft nie).
- Telegram-Test je Instanz (/api/einstellungen/telegram-test und
  /api/homelab/einstellungen/telegram-test) über notify.sh -d; Ergebnis und
  Grund aus dem Melde-Log. notify.sh schreibt jetzt auch den Grund des
  gescheiterten Zweitwegs in den FALLBACK-Eintrag.
- Einstellungsdatei mc2-einstellungen.json im Datenordner.

Software-Stand: deploy.sh und homelab/ausrollen.sh schreiben nach dem
Umschalten mc2-stand.json (deploy/stand-schreiben.py); /api/instanz liefert
den Stand, die Einstellungen zeigen beide Instanzen und warnen bei Abweichung.
Ohne Datei aus git, sonst unbekannt.

Aufräumen: Altreste neben dem Betrieb aus einer festen, auf der Box geprüften
Liste (Größe, Hinweis, nur was da und nicht in Gebrauch ist). Löschen nur per
Kennung und Klick mit Rückfrage; Worktrees mit git worktree prune, Alt-Units
mit daemon-reload, /opt/llamacpp notfalls mit sudo -n.

Tests: pytest mit einem systemd-Abbild samt Nachhol-Regel, echtem notify.sh
(Ersatz-Hermes, Tmp-Zugang, lokale Bot-API), Shell-Funktionen des
Wartungsfensters und von deploy.sh; vitest für die Rückfragen der Schublade.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-24 20:37:30 +02:00
co-authored by Claude Opus 5.5
parent 40d2840b16
commit 3d8df1c6fd
24 changed files with 2640 additions and 85 deletions
+20
View File
@@ -10,6 +10,8 @@ Box-Wart-Schnittstellen (Umbau 09/2026): alles, was der Cockpit-Startbildschirm
GET /api/modelle/aufraeumen Was auf der Modell-Platte ungenutzt Platz belegt
GET /api/ziele Die KI-Box als Ziel mit Bausteinen (gemeinsames Modell, Phase 2)
POST /api/modelle/aufraeumen/loeschen Einen Kandidaten löschen (nur auf Knopfdruck)
GET /api/aufraeumen/altreste Altreste neben dem Betrieb (feste Liste, nur was da ist)
POST /api/aufraeumen/altreste/loeschen Einen Altrest löschen — per Kennung, nie per Pfad
Dünn: die Logik liegt in services/waechter.py, modell_nutzung.py, zeitplan.py, update_verlauf.py.
"""
@@ -197,6 +199,24 @@ def aufraeumen_loeschen(auftrag: LoeschAuftrag) -> dict:
return ergebnis
class AltrestAuftrag(BaseModel):
id: str # Kennung aus der festen Liste (services/aufraeumen.ALTRESTE), nie ein Pfad
@router.get("/aufraeumen/altreste")
def altreste_liste() -> dict:
"""Altreste neben dem Betrieb (Test-Ordner, alte Umgebungen, Worktrees …), nur was da und nicht in Gebrauch ist."""
return {"altreste": aufraeumen.altreste()}
@router.post("/aufraeumen/altreste/loeschen")
def altrest_loeschen(auftrag: AltrestAuftrag) -> dict:
ergebnis = aufraeumen.altrest_loeschen(auftrag.id)
if not ergebnis.get("ok"):
raise HTTPException(status_code=409, detail=ergebnis.get("detail") or "Löschen ging nicht.")
return ergebnis
@router.get("/ziele")
def ziele() -> dict:
"""Die KI-Box im gemeinsamen Ziel-Modell (kern/ziele.py). Der Homelab-Teil liefert seine Ziele ab
+62
View File
@@ -0,0 +1,62 @@
"""Einstellungen der Oberfläche (seit 24.09.2026).
KI-Box (Rolle box):
GET /api/einstellungen Update-Zeitfenster und Radar-Schalter samt Zustand laut systemd
POST /api/einstellungen/update-fenster {tag: 1–7, uhrzeit: "HH:MM"}
POST /api/einstellungen/radar {an: true|false}
POST /api/einstellungen/telegram-test Testmeldung über notify.sh, Ergebnis aus dem Melde-Log
Homelab-Teil (Rolle homelab):
POST /api/homelab/einstellungen/telegram-test dasselbe für den Meldeweg des Homelab-Teils
Die Oberfläche schickt alles unter /api/homelab/… an den Homelab-Teil und den Rest an die KI-Box (lib/instanz.ts) —
so erreicht jeder Test-Knopf die richtige Instanz, egal welche die Seite ausliefert. Dünn: die Logik liegt in
services/einstellungen.py.
"""
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from services import einstellungen
router = APIRouter(prefix="/api/einstellungen", tags=["einstellungen"])
homelab_router = APIRouter(prefix="/api/homelab/einstellungen", tags=["einstellungen"])
class Zeitfenster(BaseModel):
tag: int
uhrzeit: str
class Schalter(BaseModel):
an: bool
def _antwort(ergebnis: dict) -> dict:
if not ergebnis.get("ok"):
raise HTTPException(status_code=409, detail=ergebnis.get("detail") or "Das ging gerade nicht.")
return ergebnis
@router.get("")
def stand() -> dict:
return einstellungen.stand()
@router.post("/update-fenster")
def update_fenster(fenster: Zeitfenster) -> dict:
return _antwort(einstellungen.update_fenster_setzen(fenster.tag, fenster.uhrzeit))
@router.post("/radar")
def radar(schalter: Schalter) -> dict:
return _antwort(einstellungen.radar_schalten(schalter.an))
@router.post("/telegram-test")
def telegram_test() -> dict:
"""Gesendet oder nicht, beides ist eine Antwort (200) — der Grund steht im Ergebnis."""
return einstellungen.telegram_test()
@homelab_router.post("/telegram-test")
def telegram_test_homelab() -> dict:
return einstellungen.telegram_test()
+5 -2
View File
@@ -9,6 +9,7 @@ import httpx
from fastapi import APIRouter, HTTPException, Request, Response
from kern import partner
from kern.einstellungen import einstellungen
from services import software_stand
router = APIRouter(prefix="/api", tags=["partner"])
@@ -21,10 +22,12 @@ _ZEITLIMIT = httpx.Timeout(30.0, connect=5.0)
@router.get("/instanz")
def instanz() -> dict:
"""Wer liefert die Seite aus? Ohne Netzabfragen, damit die Oberfläche sofort weiß, welche Anfragen
sie selbst beantwortet und welche über /api/partner/… an die andere Instanz gehen."""
sie selbst beantwortet und welche über /api/partner/… an die andere Instanz gehen. Seit 24.09.2026 mit dem
Software-Stand (welcher Commit seit wann live ist); den der anderen Instanz liefert /api/partner/instanz."""
e = einstellungen()
return {"rolle": e.rolle, "instanz": e.instanz,
"partner": {"eingerichtet": bool(e.partner_url), "name": e.partner_name}}
"partner": {"eingerichtet": bool(e.partner_url), "name": e.partner_name},
"stand": software_stand.stand()}
@router.get("/partner")