diff --git a/backend/app.py b/backend/app.py index 4786e39..4c5d727 100644 --- a/backend/app.py +++ b/backend/app.py @@ -72,6 +72,9 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: from services import jobengine await asyncio.to_thread(jobengine.wiederaufnehmen) tasks = _box_hintergrund() if ROLLE == "box" else [] + if ROLLE == "homelab": + from services.homelab import updates + updates.unterbrochene_abschliessen() # Läufe des vorigen Prozesses gelten als unterbrochen # Geteilter HTTP-Client zur lokalen Engine: Keep-Alive/Connection-Pooling statt neuer Client # pro /v1-Anfrage (spart Sockets/TIME_WAIT unter parallelen Agent-Strömen von Zed/Kilo). app.state.gw_client = httpx.AsyncClient( @@ -160,6 +163,9 @@ app.include_router(health.router) app.include_router(partner.router) # zweite Instanz: Lebenszeichen + Durchreiche if ROLLE == "box": _box_router(app) +else: + from routers import homelab + app.include_router(homelab.router) # Proxmox-Host, Container, Arcane (Phase 3/4) # Prod: gebautes Frontend ausliefern (falls vorhanden). SPA-Fallback auf index.html. diff --git a/backend/kern/github.py b/backend/kern/github.py new file mode 100644 index 0000000..41283f6 --- /dev/null +++ b/backend/kern/github.py @@ -0,0 +1,44 @@ +"""GitHub-Abfragen mit Zwischenspeicher — für beide Rollen (Box: Motor und llama-swap, Homelab: die Apps). + +GitHub erlaubt ohne Anmeldung 60 Anfragen pro Stunde. Jeder Blick auf die Update-Details fragte früher +neu; jetzt gilt eine Antwort 15 Minuten. Fehler (auch das Anfragelimit) werden nicht gemerkt. +""" + +import os +import re +import threading +import time + +import httpx + +GITHUB_CACHE_S = int(os.environ.get("MC_GITHUB_CACHE_S", "900")) +_cache: dict[str, tuple[float, object]] = {} +_lock = threading.Lock() + + +def github_json(pfad: str, timeout: float = 8) -> object: + """GET https://api.github.com/, 15 Minuten gemerkt. Wirft bei HTTP-Fehlern.""" + jetzt = time.time() + with _lock: + if (eintrag := _cache.get(pfad)) and jetzt - eintrag[0] < GITHUB_CACHE_S: + return eintrag[1] + r = httpx.get(f"https://api.github.com/{pfad}", timeout=timeout, headers={"User-Agent": "MissionControl2"}) + r.raise_for_status() + daten = r.json() + with _lock: + _cache[pfad] = (jetzt, daten) + return daten + + +_VERSION = re.compile(r"^v?(\d+(?:\.\d+)+|\d{4}-\d{2}-\d{2}(?:-r\d+)?)", re.IGNORECASE) + + +def neueste_version(repo: str) -> dict | None: + """Neueste Veröffentlichung eines Repos: {"tag", "version", "datum"} oder None. Manche Projekte + veröffentlichen „untagged“ und tragen die Version nur im Namen (PVE Scripts Local, 09/2026).""" + daten = github_json(f"repos/{repo}/releases/latest") + if not isinstance(daten, dict) or not daten.get("tag_name"): + return None + tag, name = str(daten["tag_name"]), str(daten.get("name") or "") + roh = tag if _VERSION.match(tag) or not _VERSION.match(name) else name + return {"tag": roh, "version": roh.lstrip("vV"), "datum": str(daten.get("published_at") or "")[:10] or None} diff --git a/backend/kern/ziele.py b/backend/kern/ziele.py index f1493b0..c280c55 100644 --- a/backend/kern/ziele.py +++ b/backend/kern/ziele.py @@ -23,6 +23,7 @@ class Aktion: methode: str # "POST" pfad: str # "/api/maintenance/engine-update" frage: str # Rückfrage vor dem Klick + label: str = "Jetzt updaten" @dataclass diff --git a/backend/routers/homelab.py b/backend/routers/homelab.py new file mode 100644 index 0000000..a1fad27 --- /dev/null +++ b/backend/routers/homelab.py @@ -0,0 +1,96 @@ +"""Schnittstellen des Homelab-Teils (Rolle homelab, Phase 3/4, 24.09.2026). + + GET /api/homelab/ziele Proxmox-Host und Gäste im gemeinsamen Ziel-Modell + POST /api/homelab/ziele/{id}/update „Jetzt updaten“ (App bzw. Host-Pakete) + POST /api/homelab/ziele/{id}/os-update Pakete im Gast einspielen + POST /api/homelab/ziele/{id}/suchen Paketlisten im Gast erneuern + POST /api/homelab/ziele/pve/neustart Proxmox-Host neu starten (eigener Knopf) + GET /api/homelab/laeufe Die letzten Update-Läufe mit ihren Schritten + GET /api/homelab/ausfuehrer Lebenszeichen des Ausführers + GET /api/homelab/ausfuehrer/auftrag ┐ + POST /api/homelab/ausfuehrer/bericht ├ nur für den Ausführer (Kopfzeile X-MC2-Ausfuehrer) + POST /api/homelab/ausfuehrer/ergebnis/{id} ┘ + +Dünn: die Logik liegt in services/homelab/. +""" + +import time + +from fastapi import APIRouter, Depends, Header, HTTPException +from pydantic import BaseModel +from services.homelab import inventar, kanal, updates + +router = APIRouter(prefix="/api/homelab", tags=["homelab"]) + + +def _nur_ausfuehrer(x_mc2_ausfuehrer: str | None = Header(default=None)) -> None: + if not kanal.token_gueltig(x_mc2_ausfuehrer): + raise HTTPException(status_code=403, detail="Nur für den Ausführer auf dem Proxmox-Host.") + kanal.kontakt() + + +@router.get("/ziele") +def ziele() -> dict: + return inventar.ziele() + + +def _antwort(ergebnis: dict) -> dict: + if not ergebnis.get("ok"): + raise HTTPException(status_code=409, detail=ergebnis.get("detail") or "Das geht gerade nicht.") + return ergebnis + + +@router.post("/ziele/{ziel_id}/update") +def update(ziel_id: str) -> dict: + return _antwort(updates.starten(ziel_id, "pakete" if ziel_id == "pve" else "app")) + + +@router.post("/ziele/{ziel_id}/os-update") +def os_update(ziel_id: str) -> dict: + return _antwort(updates.starten(ziel_id, "os")) + + +@router.post("/ziele/pve/neustart") +def host_neustart() -> dict: + return _antwort(updates.starten("pve", "neustart")) + + +@router.post("/ziele/{ziel_id}/suchen") +def suchen(ziel_id: str) -> dict: + gast = inventar.gast(ziel_id) + if not gast or not gast.get("erlaubt"): + raise HTTPException(status_code=404, detail="Dieses Gerät steht nicht (freigegeben) im Bericht.") + return {"ok": True, "auftrag": kanal.anlegen("suchen", {"vmid": gast["vmid"]})} + + +@router.get("/laeufe") +def laeufe() -> dict: + return {"laeufe": updates.laeufe()} + + +@router.get("/ausfuehrer") +def ausfuehrer() -> dict: + zuletzt = kanal.zuletzt() + return {"zuletzt": zuletzt, "verbunden": bool(zuletzt and time.time() - zuletzt < inventar.BERICHT_ALT_S), + "auftraege": kanal.liste()[:20]} + + +@router.get("/ausfuehrer/auftrag", dependencies=[Depends(_nur_ausfuehrer)]) +def auftrag_abholen() -> dict: + return kanal.naechster() or {} + + +@router.post("/ausfuehrer/bericht", dependencies=[Depends(_nur_ausfuehrer)]) +def bericht(daten: dict) -> dict: + kanal.bericht_speichern(daten) + return {"ok": True} + + +class Ergebnis(BaseModel): + code: int + text: str = "" + + +@router.post("/ausfuehrer/ergebnis/{auftrag_id}", dependencies=[Depends(_nur_ausfuehrer)]) +def ergebnis(auftrag_id: str, e: Ergebnis) -> dict: + return {"ok": kanal.ergebnis(auftrag_id, e.code, e.text)} diff --git a/backend/services/announce.py b/backend/services/announce.py index ad046cb..24ccf05 100644 --- a/backend/services/announce.py +++ b/backend/services/announce.py @@ -20,13 +20,13 @@ import time from pathlib import Path import httpx -from config import MODELS_DIR +from kern.einstellungen import einstellungen log = logging.getLogger(__name__) NOTIFY_SH = str(Path(__file__).resolve().parent.parent.parent / "deploy" / "notify.sh") -STORE_PATH = Path(os.environ.get("MC_ANNOUNCE_STORE", str(MODELS_DIR / "mc2-announce.json"))) +STORE_PATH = Path(os.environ.get("MC_ANNOUNCE_STORE", str(einstellungen().daten_dir / "mc2-announce.json"))) MAX_ITEMS = int(os.environ.get("MC_ANNOUNCE_MAX", "200")) # Steward-Auszug (UMBAU v3 P2): Läuft der Absender AUSSERHALB des MC2-Prozesses diff --git a/backend/services/homelab/__init__.py b/backend/services/homelab/__init__.py new file mode 100644 index 0000000..2be09f0 --- /dev/null +++ b/backend/services/homelab/__init__.py @@ -0,0 +1,7 @@ +"""Der Homelab-Teil (Rolle homelab, Phase 3/4): Proxmox-Host, Container und Arcane. + + apps.py was in welchem Container steckt, wo seine Oberfläche liegt, woher die neueste Version kommt + kanal.py Aufträge und Berichte des Ausführers auf dem Proxmox-Host (gemeinsames Geheimnis) + inventar.py Bericht + neueste Versionen + Erreichbarkeit → Ziele im gemeinsamen Modell (kern/ziele.py) + updates.py „Jetzt updaten“: Snapshot → Update → Prüfung → bei Rot zurück, mit Meldung +""" diff --git a/backend/services/homelab/apps.py b/backend/services/homelab/apps.py new file mode 100644 index 0000000..a3342c1 --- /dev/null +++ b/backend/services/homelab/apps.py @@ -0,0 +1,47 @@ +"""Was in welchem Container steckt (Community-Scripts-Kennung aus /usr/bin/update im Gast). + +Stand der Bestandsaufnahme vom 24.09.2026: sechs Container, alle mit dem Etikett community-script. +Neue Container mit dem Etikett erscheinen von selbst; fehlt ihre Kennung hier, zeigt die Übersicht +sie ohne Versionsvergleich und ohne Webprüfung — nachtragen genügt. +""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class App: + kennung: str + name: str + quelle: str | None = None # GitHub owner/repo für die neueste Version; None = über die Pakete des Gasts + vergleich: str = "version" # „version“ oder „datum“ (Docker-Images ohne Versionsnummer) + port: int | None = None # Weboberfläche; None = keine (z. B. NetBird-Client) + https: bool = False + pfad: str = "/" + + +KATALOG: dict[str, App] = {a.kennung: a for a in ( + App("adguard", "AdGuard Home", "AdguardTeam/AdGuardHome", port=8080), + App("npmplus", "NPMplus", "ZoeyVid/NPMplus", vergleich="datum", port=81, https=True), + App("netbird", "NetBird", "netbirdio/netbird"), + App("pve-scripts-local", "PVE Scripts Local", "community-scripts/ProxmoxVE-Local", port=3000), + App("gitea", "Gitea", "go-gitea/gitea", port=3000), + App("proxmox-backup-server", "Proxmox Backup Server", port=8007, https=True), +)} + +# Gäste, die keine Community-Scripts sind, erkennt man am Namen. +NACH_NAME: dict[str, App] = { + "arcane": App("arcane", "Arcane (Docker)", port=3552), +} + + +def app_fuer(kennung: str | None, name: str | None) -> App | None: + if kennung and kennung in KATALOG: + return KATALOG[kennung] + return NACH_NAME.get(str(name or "").lower()) + + +def adresse(app: App | None, ip: str | None) -> str | None: + """Adresse der Weboberfläche, falls es eine gibt und die IP bekannt ist.""" + if not app or not app.port or not ip: + return None + return f"{'https' if app.https else 'http'}://{ip}:{app.port}{app.pfad}" diff --git a/backend/services/homelab/inventar.py b/backend/services/homelab/inventar.py new file mode 100644 index 0000000..ea9fa84 --- /dev/null +++ b/backend/services/homelab/inventar.py @@ -0,0 +1,209 @@ +"""Vom Bericht des Ausführers zu Zielen im gemeinsamen Modell (kern/ziele.py) — Phase 3, 24.09.2026. + +Je Gerät ein Ziel: der Proxmox-Host (Pakete, Neustart) und jeder freigegebene Gast (Etikett +community-script oder watcher, nicht watcher-aus) mit seinen Bausteinen „App“ (Community-Script, +Vergleich mit der neuesten Veröffentlichung auf GitHub) und „Pakete“ (Betriebssystem im Gast). +Dazu prüft dieser Teil selbst, ob die Weboberfläche jedes Gasts antwortet. + +Wie ehrlich der Stand ist, steht dabei: Sind die Paketlisten im Gast älter als zwei Wochen, heißt es +„unbekannt“ (mit Knopf zum Suchen) statt „aktuell“; ist der Bericht älter als eine halbe Stunde, sagt +die Übersicht, dass der Ausführer schweigt. +""" + +import re +import threading +import time +from datetime import datetime + +import httpx +from kern.github import neueste_version +from kern.zeit import LOCAL_TZ +from kern.ziele import Aktion, BausteinStand, ZielStand + +from services.homelab import apps, kanal + +BERICHT_ALT_S = 30 * 60 +LISTEN_ALT_S = 14 * 24 * 3600 +ERREICHBAR_CACHE_S = 60 + +_erreichbar_cache: dict[str, tuple[float, bool]] = {} +_erreichbar_lock = threading.Lock() + + +def erreichbar(url: str) -> bool: + """Antwortet die Weboberfläche? Alles unter 500 zählt (eine Anmeldeseite ist eine Antwort).""" + jetzt = time.time() + with _erreichbar_lock: + if (eintrag := _erreichbar_cache.get(url)) and jetzt - eintrag[0] < ERREICHBAR_CACHE_S: + return eintrag[1] + try: + # Selbstsignierte Zertifikate sind im Heimnetz üblich (PBS, NPMplus); geprüft wird nur, ob jemand antwortet. + ok = httpx.get(url, timeout=5.0, verify=False, follow_redirects=False).status_code < 500 + except httpx.HTTPError: + ok = False + with _erreichbar_lock: + _erreichbar_cache[url] = (jetzt, ok) + return ok + + +def erreichbar_frisch(url: str) -> bool: + """Wie erreichbar(), aber ohne den Zwischenspeicher — für die Prüfung direkt nach einem Update.""" + with _erreichbar_lock: + _erreichbar_cache.pop(url, None) + return erreichbar(url) + + +def _teile(version: str) -> tuple[int, ...]: + return tuple(int(x) for x in re.findall(r"\d+", version.split("-")[0])[:4]) + + +def neuer(verfuegbar: str, installiert: str) -> bool | None: + """Ist verfuegbar neuer als installiert? None, wenn sich das nicht sagen lässt.""" + a, b = _teile(verfuegbar), _teile(installiert) + if not a or not b: + return None + return a > b + + +def _datum(ts: float | None) -> str: + return datetime.fromtimestamp(ts, LOCAL_TZ).strftime("%d.%m.%Y") if ts else "?" + + +def _app_baustein(gast: dict, app: apps.App, zid: str) -> BausteinStand: + installiert = (gast.get("app") or {}).get("version") + stand = BausteinStand(id="app", name=app.name, zustand="aktuell", installiert=installiert) + if not app.quelle: + return stand + try: + neu = neueste_version(app.quelle) + except Exception: + neu = None + if not neu: + stand.zustand, stand.grund = "unbekannt", "Die neueste Version ist gerade nicht abrufbar (GitHub)." + return stand + stand.verfuegbar = neu["version"] + if app.vergleich == "datum": + # Docker-Image ohne Versionsnummer: neuer, wenn die Veröffentlichung jünger ist als das Image. + bild = re.search(r"\d{4}-\d{2}-\d{2}", installiert or "") + ist_neuer = (neu["datum"] or "") > bild.group(0) if bild else None + else: + ist_neuer = neuer(neu["version"], installiert) if installiert else None + if ist_neuer is None: + stand.zustand, stand.grund = "unbekannt", "Die installierte Version ließ sich nicht lesen." + elif ist_neuer: + stand.zustand, stand.kurz = "neu", f"{installiert} → {neu['version']}" + rueckweg = ("Vorher wird ein Snapshot angelegt; ist die Prüfung danach rot, geht es von selbst zurück." + if gast.get("snapshot_moeglich") else + "Ein Snapshot ist hier nicht möglich — scheitert das Update, gibt es keinen automatischen Rückweg.") + stand.aktion = Aktion("POST", f"/api/homelab/ziele/{zid}/update", f"{app.name} jetzt aktualisieren? {rueckweg}") + return stand + + +def _os_baustein(gast: dict, zid: str, jetzt: float) -> BausteinStand: + os_stand = gast.get("os_updates") or {} + stand = BausteinStand(id="os", name="Pakete", zustand="aktuell") + anzahl, listen = os_stand.get("anzahl"), os_stand.get("listen_stand") + suchen = Aktion("POST", f"/api/homelab/ziele/{zid}/suchen", "Die Paketlisten im Gast jetzt erneuern?", + label="Nach Updates suchen") + if anzahl is None: + stand.zustand, stand.grund = "unbekannt", "Die Pakete ließen sich nicht lesen." + elif listen and jetzt - listen > LISTEN_ALT_S: + stand.zustand, stand.aktion = "unbekannt", suchen + stand.grund = f"Die Paketlisten sind vom {_datum(listen)}; was fehlt, weiß erst eine neue Suche." + elif anzahl > 0: + stand.zustand, stand.kurz = "neu", f"{anzahl} Pakete" + stand.aktion = Aktion("POST", f"/api/homelab/ziele/{zid}/os-update", + f"{anzahl} Pakete im Gast jetzt einspielen? Vorher wird, wo möglich, ein Snapshot angelegt.") + return stand + + +def _gast_ziel(gast: dict, jetzt: float) -> ZielStand: + art = "container" if gast["art"] == "lxc" else "vm" + zid = f"{'ct' if art == 'container' else 'vm'}-{gast['vmid']}" + app = apps.app_fuer((gast.get("app") or {}).get("kennung"), gast.get("name")) + url = apps.adresse(app, gast.get("ip")) + laeuft = gast.get("status") == "running" + ziel = ZielStand(id=zid, name=app.name if app else str(gast.get("name") or zid), art=art, instanz="homelab", + erreichbar=(erreichbar(url) if laeuft else False) if url else (laeuft or None), + rueckweg=("Snapshot vor jedem Update" if gast.get("snapshot_moeglich") + else gast.get("snapshot_grund") or "kein Snapshot möglich")) + if not laeuft: + ziel.bausteine.append(BausteinStand(id="status", name="Status", zustand="unbekannt", + grund=f"Der Gast ist {gast.get('status') or 'unbekannt'}.")) + return ziel + if not gast.get("erlaubt"): + ziel.bausteine.append(BausteinStand( + id="freigabe", name="Freigabe", zustand="unbekannt", + grund="Nicht freigegeben: Das Etikett „watcher“ fehlt (oder „watcher-aus“ ist gesetzt).")) + return ziel + if app and app.kennung in apps.KATALOG: + ziel.bausteine.append(_app_baustein(gast, app, zid)) + if gast["art"] == "lxc": + ziel.bausteine.append(_os_baustein(gast, zid, jetzt)) + return ziel + + +def _host_ziel(host: dict) -> ZielStand: + ziel = ZielStand(id="pve", name="Proxmox-Host", art="proxmox-host", instanz="homelab", erreichbar=True, + rueckweg="Kein Snapshot möglich; der Host bleibt beim Update in Betrieb, neu gestartet wird getrennt.") + updates = host.get("updates") + pakete = BausteinStand(id="pakete", name="Proxmox VE und Debian", zustand="aktuell", + installiert=host.get("version")) + if updates is None: + pakete.zustand, pakete.grund = "unbekannt", host.get("fehler") or "Die Update-Liste fehlt im Bericht." + elif updates: + pakete.zustand, pakete.kurz = "neu", f"{len(updates)} Pakete" + pakete.aktion = Aktion("POST", "/api/homelab/ziele/pve/update", + f"{len(updates)} Pakete auf dem Proxmox-Host einspielen? Alle Gäste laufen weiter. " + "Einen Neustart löst das nicht aus — der ist ein eigener Knopf.") + ziel.bausteine.append(pakete) + if host.get("neustart_noetig"): + ziel.bausteine.append(BausteinStand( + id="neustart", name="Neustart", zustand="neu", kurz="steht an", + aktion=Aktion("POST", "/api/homelab/ziele/pve/neustart", + "Den Proxmox-Host jetzt neu starten? ALLE Container und die Arcane-VM sind dann ein paar " + "Minuten weg. Die KI-Box prüft danach von außen, ob alles wiederkommt.", + label="Jetzt neu starten"))) + return ziel + + +def ziele() -> dict: + """Alle Ziele des Homelabs aus dem letzten Bericht, dazu wie frisch er ist.""" + eingang = kanal.bericht() + jetzt = time.time() + zuletzt = kanal.zuletzt() + ausfuehrer = {"verbunden": bool(zuletzt and jetzt - zuletzt < BERICHT_ALT_S), "zuletzt": zuletzt} + if not eingang: + return {"ziele": [], "bericht": None, "ausfuehrer": ausfuehrer} + b = eingang["bericht"] + liste = [_host_ziel(b.get("host") or {})] + liste += [_gast_ziel(g, jetzt) for g in b.get("gaeste") or []] + for z in liste: + z.geprueft = eingang["empfangen"] + return {"ziele": [z.als_dict() for z in liste], + "bericht": {"empfangen": eingang["empfangen"], "veraltet": jetzt - eingang["empfangen"] > BERICHT_ALT_S}, + "ausfuehrer": ausfuehrer} + + +def erreichbarkeit() -> list[dict]: + """Für den Wächter: jede Weboberfläche laufender, freigegebener Gäste — ohne GitHub-Abfragen.""" + eingang = kanal.bericht() + ergebnis = [] + for g in (eingang or {}).get("bericht", {}).get("gaeste") or []: + app = apps.app_fuer((g.get("app") or {}).get("kennung"), g.get("name")) + url = apps.adresse(app, g.get("ip")) + if url and g.get("erlaubt"): + zid = f"{'ct' if g['art'] == 'lxc' else 'vm'}-{g['vmid']}" + ergebnis.append({"id": zid, "name": app.name if app else g.get("name"), "url": url, + "laeuft": g.get("status") == "running", + "ok": g.get("status") == "running" and erreichbar(url)}) + return ergebnis + + +def gast(zid: str) -> dict | None: + """Der Gast zu einer Ziel-ID aus dem letzten Bericht (ct-104 → vmid 104).""" + m = re.fullmatch(r"(ct|vm)-(\d+)", zid) + eingang = kanal.bericht() + if not m or not eingang: + return None + return next((g for g in eingang["bericht"].get("gaeste") or [] if g.get("vmid") == int(m.group(2))), None) diff --git a/backend/services/homelab/kanal.py b/backend/services/homelab/kanal.py new file mode 100644 index 0000000..5c41185 --- /dev/null +++ b/backend/services/homelab/kanal.py @@ -0,0 +1,170 @@ +"""Kanal zum Ausführer auf dem Proxmox-Host (Phase 3/4, 24.09.2026). + +Der Ausführer (deploy/homelab/ausfuehrer.py) holt sich Aufträge hier ab, liefert die Ergebnisse und +alle zehn Minuten einen Bericht. Er weist sich mit einem gemeinsamen Geheimnis aus (Kopfzeile +X-MC2-Ausfuehrer). Das erzeugt dieser Teil beim ersten Bedarf in /ausfuehrer.token (nur +für den Dienst lesbar); die Einrichtung auf dem Proxmox-Host kopiert es in /etc/mc2-ausfuehrer.json. + +Aufträge liegen in /ausfuehrer-auftraege.json und überleben so einen Neustart dieses Teils. +""" + +import hmac +import json +import os +import secrets +import threading +import time +import uuid +from pathlib import Path + +from kern.einstellungen import einstellungen + +AKTIONEN = {"bericht", "snapshot", "update", "os_update", "suchen", "zurueck", "snapshot_loeschen", + "host_update", "host_neustart"} +VERLOREN_S = 3 * 3600 # abgeholt, aber nie beantwortet (Ausführer abgestürzt, Host neu gestartet) +BEHALTEN = 60 # so viele erledigte Aufträge bleiben sichtbar + +_lock = threading.Lock() +_kontakt: dict = {"zuletzt": None} + + +def _dir() -> Path: + return einstellungen().daten_dir + + +def _json_schreiben(pfad: Path, daten: object) -> None: + pfad.parent.mkdir(parents=True, exist_ok=True) + tmp = pfad.with_suffix(pfad.suffix + ".tmp") + tmp.write_text(json.dumps(daten, ensure_ascii=False), encoding="utf-8") + tmp.replace(pfad) + + +def _json_lesen(pfad: Path, leer: object) -> object: + try: + return json.loads(pfad.read_text(encoding="utf-8")) + except (OSError, ValueError): + return leer + + +# --- Gemeinsames Geheimnis --------------------------------------------------------------- + +def token() -> str: + """Das Geheimnis des Kanals; beim ersten Aufruf erzeugt (0600). MC_AUSFUEHRER_TOKEN geht vor.""" + if wert := os.environ.get("MC_AUSFUEHRER_TOKEN", "").strip(): + return wert + pfad = _dir() / "ausfuehrer.token" + try: + return pfad.read_text(encoding="utf-8").strip() + except OSError: + pass + wert = secrets.token_urlsafe(32) + pfad.parent.mkdir(parents=True, exist_ok=True) + try: + fd = os.open(pfad, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + except FileExistsError: # gleichzeitig erzeugt: den anderen nehmen + return pfad.read_text(encoding="utf-8").strip() + with os.fdopen(fd, "w", encoding="utf-8") as f: + f.write(wert + "\n") + return wert + + +def token_gueltig(wert: str | None) -> bool: + return bool(wert) and hmac.compare_digest(str(wert), token()) + + +def kontakt() -> None: + _kontakt["zuletzt"] = time.time() + + +def zuletzt() -> float | None: + """Letztes Lebenszeichen des Ausführers (nach einem Neustart: Zeit des letzten Berichts).""" + return _kontakt["zuletzt"] or (bericht() or {}).get("empfangen") + + +# --- Bericht ------------------------------------------------------------------------------ + +def bericht_speichern(daten: dict) -> None: + _json_schreiben(_dir() / "pve-bericht.json", {"empfangen": time.time(), "bericht": daten}) + + +def bericht() -> dict | None: + """{"empfangen": Unix-Sekunden, "bericht": {...}} oder None, solange nie einer kam.""" + daten = _json_lesen(_dir() / "pve-bericht.json", None) + return daten if isinstance(daten, dict) and isinstance(daten.get("bericht"), dict) else None + + +# --- Aufträge ------------------------------------------------------------------------------- + +def _pfad() -> Path: + return _dir() / "ausfuehrer-auftraege.json" + + +def _alle() -> list[dict]: + daten = _json_lesen(_pfad(), []) + return daten if isinstance(daten, list) else [] + + +def _merken(auftraege: list[dict]) -> None: + offen = [a for a in auftraege if a["status"] in ("wartet", "laeuft")] + fertig = [a for a in auftraege if a["status"] not in ("wartet", "laeuft")][-BEHALTEN:] + _json_schreiben(_pfad(), sorted(offen + fertig, key=lambda a: a["erstellt"])) + + +def anlegen(aktion: str, parameter: dict | None = None) -> str: + if aktion not in AKTIONEN: + raise ValueError(f"Unbekannte Aktion: {aktion}") + auftrag = {"id": uuid.uuid4().hex[:12], "aktion": aktion, "parameter": parameter or {}, + "status": "wartet", "erstellt": time.time(), "abgeholt": None, "fertig": None, + "code": None, "text": None} + with _lock: + auftraege = _alle() + auftraege.append(auftrag) + _merken(auftraege) + return auftrag["id"] + + +def naechster() -> dict | None: + """Für den Ausführer: der älteste wartende Auftrag, ab jetzt „läuft“. Hängengebliebene werden + vorher als verloren abgeschlossen.""" + jetzt = time.time() + with _lock: + auftraege = _alle() + for a in auftraege: + if a["status"] == "laeuft" and jetzt - (a["abgeholt"] or jetzt) > VERLOREN_S: + a.update(status="verloren", fertig=jetzt, text="Der Ausführer hat nie geantwortet.") + auftrag = next((a for a in auftraege if a["status"] == "wartet"), None) + if auftrag: + auftrag.update(status="laeuft", abgeholt=jetzt) + _merken(auftraege) + return {k: auftrag[k] for k in ("id", "aktion", "parameter")} if auftrag else None + + +def ergebnis(auftrag_id: str, code: int, text: str) -> bool: + with _lock: + auftraege = _alle() + auftrag = next((a for a in auftraege if a["id"] == auftrag_id), None) + if auftrag is None or auftrag["status"] != "laeuft": + return False + auftrag.update(status="fertig" if code == 0 else "fehler", fertig=time.time(), code=code, + text=str(text)[-20000:]) + _merken(auftraege) + return True + + +def auftrag(auftrag_id: str) -> dict | None: + return next((a for a in _alle() if a["id"] == auftrag_id), None) + + +def warten(auftrag_id: str, zeitlimit_s: float, takt_s: float = 2.0) -> dict | None: + """Bis der Auftrag fertig ist (oder das Zeitlimit abläuft). Rückgabe: der Auftrag, None bei Zeitablauf.""" + ende = time.time() + zeitlimit_s + while time.time() < ende: + a = auftrag(auftrag_id) + if a and a["status"] not in ("wartet", "laeuft"): + return a + time.sleep(takt_s) + return None + + +def liste() -> list[dict]: + return list(reversed(_alle())) diff --git a/backend/services/homelab/updates.py b/backend/services/homelab/updates.py new file mode 100644 index 0000000..6ef8c7f --- /dev/null +++ b/backend/services/homelab/updates.py @@ -0,0 +1,212 @@ +"""„Jetzt updaten“ im Homelab (Phase 4, User-Entscheide 24.09.2026). + +Nur per Knopf, mit Erfolgsmeldung und Erreichbarkeits-Check; ist er rot, geht es automatisch zurück +und es wird gemeldet. Host-Updates gibt es mit Warnung, der Neustart ist ein eigener Knopf. + +Ablauf für einen Gast: + 1. Snapshot (wo möglich; beim PBS verhindert der Bind-Mount ihn — dann ohne Rückweg, die Rückfrage sagt es) + 2. Update: die App per Community-Script (`update`, still) oder die Pakete des Gasts + 3. 20 s warten, frischen Bericht holen + 4. Prüfen: läuft der Gast, antwortet die Weboberfläche, ist die App-Version jetzt neuer? + 5. Rot und Snapshot da → zurück auf den Snapshot → dringende Meldung. + Grün → ältere Snapshots des Orchestrators für diesen Gast weg → Meldung „eingespielt“. +Jeder Lauf steht in /homelab-laeufe.json und im strukturierten Update-Verlauf. Pro Ziel +läuft höchstens ein Lauf; startet dieser Teil neu, gilt ein offener Lauf als unterbrochen. +""" + +import json +import logging +import re +import threading +import time +import uuid +from datetime import datetime +from pathlib import Path + +from kern.einstellungen import einstellungen +from kern.zeit import LOCAL_TZ + +from services import announce, update_verlauf +from services.homelab import apps, inventar, kanal + +log = logging.getLogger(__name__) + +WARTEN_NACH_UPDATE_S = 20 +ZEITLIMIT_UPDATE_S = 45 * 60 +ZEITLIMIT_KURZ_S = 15 * 60 +BETREFF = "[Homelab-Update]" + +_lock = threading.Lock() + + +def _pfad() -> Path: + return einstellungen().daten_dir / "homelab-laeufe.json" + + +def _laeufe() -> list[dict]: + try: + daten = json.loads(_pfad().read_text(encoding="utf-8")) + except (OSError, ValueError): + return [] + return daten if isinstance(daten, list) else [] + + +def _merken(lauf: dict) -> None: + with _lock: + laeufe = [x for x in _laeufe() if x["id"] != lauf["id"]] + [lauf] + _pfad().parent.mkdir(parents=True, exist_ok=True) + tmp = _pfad().with_suffix(".tmp") + tmp.write_text(json.dumps(laeufe[-100:], ensure_ascii=False), encoding="utf-8") + tmp.replace(_pfad()) + + +def laeufe(anzahl: int = 20) -> list[dict]: + return list(reversed(_laeufe()))[:anzahl] + + +def unterbrochene_abschliessen() -> None: + """Beim Start: Läufe, die noch „läuft“ heißen, gehörten zum vorigen Prozess.""" + for lauf in _laeufe(): + if lauf.get("status") == "laeuft": + lauf.update(status="unterbrochen", ende=time.time()) + lauf["schritte"].append("Der Homelab-Teil wurde während des Laufs neu gestartet.") + _merken(lauf) + + +def _melden(text: str, dringend: bool = False) -> None: + try: + announce.notify_telegram("[Alarm] Homelab-Update" if dringend else BETREFF, text) + except Exception: + log.warning("homelab: Meldung ging nicht raus", exc_info=True) + + +def _auftrag(lauf: dict, aktion: str, parameter: dict, zeitlimit_s: float) -> dict: + """Auftrag an den Ausführer und auf das Ergebnis warten. Wirft RuntimeError bei Fehler/Zeitablauf.""" + aid = kanal.anlegen(aktion, parameter) + lauf["schritte"].append(f"{aktion} …") + _merken(lauf) + a = kanal.warten(aid, zeitlimit_s) + if a is None: + raise RuntimeError(f"{aktion}: keine Antwort des Ausführers in {int(zeitlimit_s // 60)} Minuten") + if a["status"] != "fertig": + raise RuntimeError(f"{aktion} gescheitert: {str(a.get('text') or '')[-300:]}") + lauf["schritte"][-1] = f"{aktion}: ok" + return a + + +def _frischer_gast(lauf: dict, vmid: int) -> dict | None: + try: + a = _auftrag(lauf, "bericht", {}, 5 * 60) + bericht = json.loads(a.get("text") or "{}") + kanal.bericht_speichern(bericht) + except (RuntimeError, ValueError): + return None + return next((g for g in bericht.get("gaeste") or [] if g.get("vmid") == vmid), None) + + +def pruefen(vorher: dict, nachher: dict | None, baustein: str) -> list[str]: + """Was nach dem Update nicht stimmt (leer = grün).""" + if nachher is None: + return ["Der Gast fehlt im neuen Bericht."] + fehler = [] + if nachher.get("status") != "running": + fehler.append(f"Der Gast läuft nicht ({nachher.get('status')}).") + app = apps.app_fuer((nachher.get("app") or {}).get("kennung"), nachher.get("name")) + url = apps.adresse(app, nachher.get("ip")) + if url and not inventar.erreichbar_frisch(url): + fehler.append(f"Die Weboberfläche ({url}) antwortet nicht.") + if baustein == "app": + alt, neu = (vorher.get("app") or {}).get("version"), (nachher.get("app") or {}).get("version") + if alt and neu and alt == neu: + fehler.append(f"Die App-Version ist unverändert ({alt}).") + return fehler + + +def _gast_lauf(lauf: dict, gast: dict) -> None: + vmid, baustein = gast["vmid"], lauf["baustein"] + snapshot = None + try: + if gast.get("snapshot_moeglich"): + a = _auftrag(lauf, "snapshot", {"vmid": vmid}, ZEITLIMIT_KURZ_S) + m = re.search(r"snapshot=(mc2-\d{8}-\d{6})", a.get("text") or "") + snapshot = m.group(1) if m else None + _auftrag(lauf, "update" if baustein == "app" else "os_update", {"vmid": vmid}, ZEITLIMIT_UPDATE_S) + time.sleep(WARTEN_NACH_UPDATE_S) + fehler = pruefen(gast, _frischer_gast(lauf, vmid), baustein) + except RuntimeError as exc: + fehler = [str(exc)] + if not fehler: + _aufraeumen(lauf, vmid, behalten=snapshot) + _ende(lauf, "eingespielt", f"{lauf['name']}: eingespielt, Prüfung grün.") + return + if snapshot: + try: + _auftrag(lauf, "zurueck", {"vmid": vmid, "snapshot": snapshot}, ZEITLIMIT_KURZ_S) + _ende(lauf, "zurueckgerollt", + f"{lauf['name']}: Update gescheitert ({' '.join(fehler)}) — automatisch zurück auf den Snapshot " + f"{snapshot}. Läuft wieder wie vorher.", dringend=True) + return + except RuntimeError as exc: + fehler.append(f"Auch der Rückweg scheiterte: {exc}") + _ende(lauf, "fehler", f"{lauf['name']}: Update gescheitert — {' '.join(fehler)}" + + ("" if snapshot else " Es gab keinen Snapshot, also keinen automatischen Rückweg."), + dringend=True) + + +def _aufraeumen(lauf: dict, vmid: int, behalten: str | None) -> None: + """Ältere Snapshots des Orchestrators für diesen Gast löschen; den neuesten behalten.""" + gast = inventar.gast(lauf["ziel"]) or {} + for name in gast.get("snapshots") or []: + if name.startswith("mc2-") and name != behalten: + try: + _auftrag(lauf, "snapshot_loeschen", {"vmid": vmid, "snapshot": name}, ZEITLIMIT_KURZ_S) + except RuntimeError: + lauf["schritte"].append(f"Alter Snapshot {name} blieb stehen.") + + +def _host_lauf(lauf: dict) -> None: + try: + if lauf["baustein"] == "neustart": + _auftrag(lauf, "host_neustart", {}, 60) + _ende(lauf, "neustart", "Der Proxmox-Host startet neu. Die KI-Box meldet, ob alles wiederkommt.") + return + _auftrag(lauf, "host_update", {}, 60 * 60) + _auftrag(lauf, "bericht", {}, 5 * 60) + _ende(lauf, "eingespielt", "Proxmox-Host: Pakete eingespielt. Ein Neustart ist ein eigener Knopf.") + except RuntimeError as exc: + _ende(lauf, "fehler", f"Proxmox-Host: Update gescheitert — {exc}", dringend=True) + + +def _ende(lauf: dict, ergebnis: str, text: str, dringend: bool = False) -> None: + lauf.update(status="fertig", ergebnis=ergebnis, text=text, ende=time.time()) + _merken(lauf) + try: + start = datetime.fromtimestamp(lauf["start"], LOCAL_TZ).isoformat(timespec="seconds") + update_verlauf.eintragen(start, "Update von Hand", lauf["ziel"], ergebnis, text) + except Exception: + log.warning("homelab: Verlauf nicht geschrieben", exc_info=True) + _melden(text, dringend=dringend) + + +def starten(ziel_id: str, baustein: str) -> dict: + """Knopf „Jetzt updaten“. Rückgabe {"ok": True, "lauf": id} oder {"ok": False, "detail": …}.""" + if baustein not in ("app", "os", "pakete", "neustart"): + return {"ok": False, "detail": "Unbekannter Baustein."} + if any(x.get("ziel") == ziel_id and x.get("status") == "laeuft" for x in _laeufe()): + return {"ok": False, "detail": "Für dieses Gerät läuft schon ein Update."} + if ziel_id == "pve": + name, gast = "Proxmox-Host", None + else: + gast = inventar.gast(ziel_id) + if not gast: + return {"ok": False, "detail": "Dieses Gerät steht nicht im Bericht des Ausführers."} + if not gast.get("erlaubt"): + return {"ok": False, "detail": "Dieses Gerät ist nicht freigegeben (Etikett watcher fehlt)."} + app = apps.app_fuer((gast.get("app") or {}).get("kennung"), gast.get("name")) + name = app.name if app else str(gast.get("name")) + lauf = {"id": uuid.uuid4().hex[:12], "ziel": ziel_id, "baustein": baustein, "name": name, "status": "laeuft", + "start": time.time(), "ende": None, "ergebnis": None, "text": None, "schritte": []} + _merken(lauf) + ziel = _host_lauf if gast is None else (lambda lf: _gast_lauf(lf, gast)) + threading.Thread(target=ziel, args=(lauf,), name=f"homelab-{ziel_id}", daemon=True).start() + return {"ok": True, "lauf": lauf["id"]} diff --git a/backend/services/maintenance.py b/backend/services/maintenance.py index 6c34460..cc271f9 100644 --- a/backend/services/maintenance.py +++ b/backend/services/maintenance.py @@ -14,6 +14,7 @@ from datetime import datetime from pathlib import Path import httpx +from kern.github import github_json from kern.zeit import LOCAL_TZ from services import jobengine, system, update_verlauf @@ -51,21 +52,8 @@ _engine_cache = {"ts": 0.0, "avail": False} _ENGINE_ASSET_RX = re.compile(r"ubuntu-vulkan-x64\.tar\.gz$", re.IGNORECASE) -# GitHub erlaubt ohne Anmeldung 60 Anfragen pro Stunde. Jeder Blick auf die Update-Details fragte -# bisher neu; jetzt gilt eine Antwort 15 Minuten. Fehler (auch das Anfragelimit) werden nicht gemerkt. -GITHUB_CACHE_S = int(os.environ.get("MC_GITHUB_CACHE_S", "900")) -_github_cache: dict[str, tuple[float, object]] = {} - - -def _github_json(pfad: str, timeout: float = 8) -> object: - jetzt = time.time() - if (eintrag := _github_cache.get(pfad)) and jetzt - eintrag[0] < GITHUB_CACHE_S: - return eintrag[1] - r = httpx.get(f"https://api.github.com/{pfad}", timeout=timeout, headers={"User-Agent": "MissionControl2"}) - r.raise_for_status() - daten = r.json() - _github_cache[pfad] = (jetzt, daten) - return daten +# GitHub-Abfragen mit 15 Minuten Zwischenspeicher (kern/github.py, seit 24.09.2026 für beide Rollen). +_github_json = github_json def _latest_engine_asset_release() -> dict | None: diff --git a/backend/services/waechter.py b/backend/services/waechter.py index 06097db..e57fe27 100644 --- a/backend/services/waechter.py +++ b/backend/services/waechter.py @@ -457,12 +457,35 @@ def pruefe_partner() -> list[Befund]: quelle="partner", nach_takten=PARTNER_TAKTE)] +def pruefe_ausfuehrer() -> list[Befund]: + """Homelab: Der Ausführer auf dem Proxmox-Host meldet sich alle zehn Minuten. Schweigt er, zeigt die + Übersicht nichts Neues mehr, und „Jetzt updaten“ bliebe liegen. Vor seinem ersten Bericht: kein Alarm.""" + from services.homelab import inventar, kanal + if kanal.bericht() is None: + return [] + zuletzt = kanal.zuletzt() + if zuletzt and time.time() - zuletzt < inventar.BERICHT_ALT_S: + return [] + return [Befund(id="ausfuehrer", stufe="rot", titel="Der Ausführer auf dem Proxmox-Host schweigt", + text="Seit über 30 Minuten kein Bericht. Läuft mc2-ausfuehrer.service auf dem Proxmox-Host?", + quelle="ausfuehrer")] + + +def pruefe_gaeste() -> list[Befund]: + """Homelab: Läuft jeder freigegebene Gast, und antwortet seine Weboberfläche?""" + from services.homelab import inventar + return [Befund(id=f"gast:{e['id']}", stufe="rot", titel=f"{e['name']} antwortet nicht", + text=(f"Die Weboberfläche ({e['url']}) antwortet nicht." if e["laeuft"] + else "Der Gast läuft nicht."), quelle=e["id"]) + for e in inventar.erreichbarkeit() if not e["ok"]] + + # Was jede Rolle prüft. Die KI-Box kennt Dienste, Timer, Hermes und ihren Kern; der Homelab-Teil -# bekommt seine Prüfungen mit den Homelab-Bausteinen. +# den Ausführer auf dem Proxmox-Host und seine Gäste. PRUEFUNGEN = { "box": (pruefe_dienste, pruefe_timer_dienste, pruefe_hermes_jobs, pruefe_kern, pruefe_platte, pruefe_festgehalten, pruefe_partner), - "homelab": (pruefe_platte, pruefe_partner), + "homelab": (pruefe_platte, pruefe_partner, pruefe_ausfuehrer, pruefe_gaeste), }[ROLLE] BETREFF_PROBLEM, BETREFF_OK = {"box": ("[Box-Problem]", "[Box wieder ok]"), "homelab": ("[Homelab-Problem]", "[Homelab wieder ok]")}[ROLLE] diff --git a/backend/tests/fixtures/pve-bericht.json b/backend/tests/fixtures/pve-bericht.json new file mode 100644 index 0000000..d9e38c7 --- /dev/null +++ b/backend/tests/fixtures/pve-bericht.json @@ -0,0 +1,215 @@ +{ + "zeit": "2026-09-24T16:26:04+00:00", + "host": { + "node": "pve", + "neustart_noetig": false, + "version": "9.2.11", + "updates": [ + { + "paket": "libvirt-common", + "alt": "11.3.0-3+deb13u2", + "neu": "11.3.0-3+deb13u3", + "herkunft": "Debian" + }, + { + "paket": "librados2", + "alt": "19.2.3-pve1", + "neu": "19.2.6-pve4", + "herkunft": "Proxmox" + }, + { + "paket": "libperl5.40", + "alt": "5.40.1-6", + "neu": "5.40.1-6+deb13u1", + "herkunft": "Debian" + }, + { + "paket": "pve-docs", + "alt": "9.2.4", + "neu": "9.2.12", + "herkunft": "Proxmox" + }, + { + "paket": "pve-edk2-firmware-ovmf", + "alt": "4.2025.05-3", + "neu": "4.2026.08-1", + "herkunft": "Proxmox" + } + ], + "kernel": "7.0.14-12-pve" + }, + "gaeste": [ + { + "vmid": 100, + "art": "lxc", + "name": "adguard", + "status": "running", + "etiketten": [ + "adblock", + "community-script" + ], + "erlaubt": true, + "uptime": 3366322, + "snapshots": [], + "onboot": true, + "snapshot_moeglich": true, + "snapshot_grund": null, + "ostype": "debian", + "ip": "192.168.178.100", + "app": { + "kennung": "adguard", + "version": "0.107.79" + }, + "os_updates": { + "anzahl": 0, + "listen_stand": 1761157469 + } + }, + { + "vmid": 101, + "art": "lxc", + "name": "npmplus", + "status": "running", + "etiketten": [ + "community-script", + "nginx", + "proxy" + ], + "erlaubt": true, + "uptime": 2868368, + "snapshots": [], + "onboot": true, + "snapshot_moeglich": true, + "snapshot_grund": null, + "ostype": "alpine", + "ip": "192.168.178.110", + "app": { + "kennung": "npmplus", + "version": "Image vom 2026-08-27" + }, + "os_updates": { + "anzahl": 0, + "listen_stand": 1787398997 + } + }, + { + "vmid": 102, + "art": "lxc", + "name": "netbird", + "status": "running", + "etiketten": [ + "community-script", + "network", + "vpn" + ], + "erlaubt": true, + "uptime": 3366322, + "snapshots": [], + "onboot": true, + "snapshot_moeglich": true, + "snapshot_grund": null, + "ostype": "debian", + "ip": "192.168.178.148", + "app": { + "kennung": "netbird", + "version": "0.77.1" + }, + "os_updates": { + "anzahl": 0, + "listen_stand": 1787399037 + } + }, + { + "vmid": 103, + "art": "lxc", + "name": "pve-scripts-local", + "status": "running", + "etiketten": [ + "community-script", + "pve-scripts-local" + ], + "erlaubt": true, + "uptime": 1713361, + "snapshots": [], + "onboot": true, + "snapshot_moeglich": true, + "snapshot_grund": null, + "ostype": "debian", + "ip": "192.168.178.149", + "app": { + "kennung": "pve-scripts-local", + "version": "0.5.8" + }, + "os_updates": { + "anzahl": 0, + "listen_stand": 1777589263 + } + }, + { + "vmid": 104, + "art": "lxc", + "name": "gitea", + "status": "running", + "etiketten": [ + "community-script", + "git" + ], + "erlaubt": true, + "uptime": 3366321, + "snapshots": [], + "onboot": true, + "snapshot_moeglich": true, + "snapshot_grund": null, + "ostype": "debian", + "ip": "192.168.178.153", + "app": { + "kennung": "gitea", + "version": "1.27.2" + }, + "os_updates": { + "anzahl": 0, + "listen_stand": 1781978337 + } + }, + { + "vmid": 105, + "art": "lxc", + "name": "proxmox-backup-server", + "status": "running", + "etiketten": [ + "backup", + "community-script" + ], + "erlaubt": true, + "uptime": 3366321, + "snapshots": [], + "onboot": true, + "snapshot_moeglich": false, + "snapshot_grund": "Bind-Mount mp0 (/mnt/qnap-backup/pbs-datastore) — nur Backup möglich", + "ostype": "debian", + "ip": "192.168.178.156", + "app": { + "kennung": "proxmox-backup-server", + "version": "4.2.5-1" + }, + "os_updates": { + "anzahl": 58, + "listen_stand": 1790207866 + } + }, + { + "vmid": 106, + "art": "qemu", + "name": "arcane", + "status": "running", + "etiketten": [], + "erlaubt": false, + "uptime": 1713735, + "snapshots": [], + "onboot": true, + "snapshot_moeglich": true, + "snapshot_grund": null, + "ip": "192.168.178.28" + } + ] +} diff --git a/backend/tests/test_homelab.py b/backend/tests/test_homelab.py new file mode 100644 index 0000000..fd709ac --- /dev/null +++ b/backend/tests/test_homelab.py @@ -0,0 +1,254 @@ +"""Homelab-Teil (Phase 3/4, 24.09.2026): Kanal zum Ausführer, Ziele aus dem echten Bericht des +Proxmox-Hosts (tests/fixtures/pve-bericht.json, gelesen am 24.09.), „Jetzt updaten“ mit Rückweg.""" + +import importlib.util +import json +import os +import threading +import time +from pathlib import Path + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from kern import einstellungen as einstellungen_mod +from services.homelab import inventar, kanal, updates + +BERICHT = json.loads((Path(__file__).parent / "fixtures" / "pve-bericht.json").read_text(encoding="utf-8")) +AUSFUEHRER = Path(__file__).resolve().parents[2] / "deploy" / "homelab" / "ausfuehrer.py" + + +@pytest.fixture +def daten(tmp_path, monkeypatch): + monkeypatch.setenv("MC_DATEN_DIR", str(tmp_path)) + monkeypatch.delenv("MC_AUSFUEHRER_TOKEN", raising=False) + einstellungen_mod.einstellungen.cache_clear() + monkeypatch.setattr(kanal, "_kontakt", {"zuletzt": None}) + # Keine Netzabfragen: neueste Versionen und Weboberflächen sind fest. + neueste = {"AdguardTeam/AdGuardHome": "0.107.79", "go-gitea/gitea": "1.27.3", "netbirdio/netbird": "0.79.0", + "community-scripts/ProxmoxVE-Local": "1.2.1"} + monkeypatch.setattr(inventar, "neueste_version", lambda repo: { + "tag": neueste.get(repo, "2026-07-24-r1"), "version": neueste.get(repo, "2026-07-24-r1"), + "datum": "2026-07-24"}) + monkeypatch.setattr(inventar, "erreichbar", lambda url: True) + yield tmp_path + einstellungen_mod.einstellungen.cache_clear() + + +# --- Kanal --------------------------------------------------------------------------------- + +def test_token_wird_einmal_erzeugt_und_geprueft(daten): + wert = kanal.token() + assert len(wert) > 30 and kanal.token() == wert + assert kanal.token_gueltig(wert) and not kanal.token_gueltig("falsch") and not kanal.token_gueltig(None) + if os.name == "posix": + assert (daten / "ausfuehrer.token").stat().st_mode & 0o077 == 0 + + +def test_auftraege_der_reihe_nach_und_verloren(daten, monkeypatch): + erster = kanal.anlegen("bericht") + zweiter = kanal.anlegen("suchen", {"vmid": 104}) + with pytest.raises(ValueError): + kanal.anlegen("rm -rf") + assert kanal.naechster()["id"] == erster + assert kanal.ergebnis(erster, 0, "ok") and not kanal.ergebnis(erster, 0, "doppelt") + assert kanal.naechster() == {"id": zweiter, "aktion": "suchen", "parameter": {"vmid": 104}} + monkeypatch.setattr(kanal, "VERLOREN_S", -1) + assert kanal.naechster() is None + assert kanal.auftrag(zweiter)["status"] == "verloren" + + +def _client() -> TestClient: + from routers import homelab + app = FastAPI() + app.include_router(homelab.router) + return TestClient(app) + + +def test_nur_der_ausfuehrer_darf_in_den_kanal(daten): + c = _client() + assert c.get("/api/homelab/ausfuehrer/auftrag").status_code == 403 + assert c.post("/api/homelab/ausfuehrer/bericht", json=BERICHT, + headers={"X-MC2-Ausfuehrer": "falsch"}).status_code == 403 + kopf = {"X-MC2-Ausfuehrer": kanal.token()} + assert c.post("/api/homelab/ausfuehrer/bericht", json=BERICHT, headers=kopf).json() == {"ok": True} + assert c.get("/api/homelab/ausfuehrer/auftrag", headers=kopf).json() == {} + assert c.get("/api/homelab/ausfuehrer").json()["verbunden"] is True + + +# --- Ziele aus dem echten Bericht --------------------------------------------------------------- + +def _ziele(daten) -> dict[str, dict]: + kanal.bericht_speichern(BERICHT) + return {z["id"]: z for z in inventar.ziele()["ziele"]} + + +def test_ziele_aus_dem_echten_bericht(daten): + z = _ziele(daten) + assert list(z) == ["pve", "ct-100", "ct-101", "ct-102", "ct-103", "ct-104", "ct-105", "vm-106"] + host = {b["id"]: b for b in z["pve"]["bausteine"]} + assert host["pakete"]["zustand"] == "neu" and host["pakete"]["aktion"]["pfad"] == "/api/homelab/ziele/pve/update" + + gitea = {b["id"]: b for b in z["ct-104"]["bausteine"]} + assert (gitea["app"]["zustand"], gitea["app"]["kurz"]) == ("neu", "1.27.2 → 1.27.3") + assert z["ct-104"]["name"] == "Gitea" and z["ct-104"]["rueckweg"] == "Snapshot vor jedem Update" + + adguard = {b["id"]: b for b in z["ct-100"]["bausteine"]} + assert adguard["app"]["zustand"] == "aktuell" + # Paketlisten vom Oktober 2025: ehrlich „unbekannt“ mit Knopf zum Suchen statt „aktuell“. + assert adguard["os"]["zustand"] == "unbekannt" and adguard["os"]["aktion"]["label"] == "Nach Updates suchen" + + pbs = {b["id"]: b for b in z["ct-105"]["bausteine"]} + assert (pbs["os"]["zustand"], pbs["os"]["kurz"]) == ("neu", "58 Pakete") + assert "Bind-Mount" in z["ct-105"]["rueckweg"] + + assert {b["id"]: b for b in z["ct-103"]["bausteine"]}["app"]["kurz"] == "0.5.8 → 1.2.1" + assert {b["id"]: b for b in z["ct-101"]["bausteine"]}["app"]["zustand"] == "aktuell" # Image jünger + # NetBird hat keine Weboberfläche: erreichbar heißt hier „läuft“. + assert z["ct-102"]["erreichbar"] is True + arcane = z["vm-106"] + assert arcane["name"] == "Arcane (Docker)" and arcane["bausteine"][0]["id"] == "freigabe" + + +def test_ohne_bericht_leer_und_ehrlich(daten): + stand = inventar.ziele() + assert stand["ziele"] == [] and stand["bericht"] is None and stand["ausfuehrer"]["verbunden"] is False + + +def test_wachter_sieht_stumme_gaeste(daten, monkeypatch): + from services import waechter + kanal.bericht_speichern(BERICHT) + monkeypatch.setattr(inventar, "erreichbar", lambda url: "153" not in url) + befunde = waechter.pruefe_gaeste() + assert [b.id for b in befunde] == ["gast:ct-104"] and befunde[0].titel == "Gitea antwortet nicht" + assert waechter.pruefe_ausfuehrer() == [] # Bericht ist frisch + + +# --- „Jetzt updaten“ mit einem nachgespielten Ausführer ---------------------------------------- + +def _ausfuehrer_spielen(stopp: threading.Event, neue_version: str, protokoll: list[str]) -> None: + while not stopp.is_set(): + auftrag = kanal.naechster() + if not auftrag: + time.sleep(0.02) + continue + protokoll.append(auftrag["aktion"]) + text = "" + if auftrag["aktion"] == "snapshot": + text = "snapshot=mc2-20260924-190000" + elif auftrag["aktion"] == "bericht": + neu = json.loads(json.dumps(BERICHT)) + next(g for g in neu["gaeste"] if g["vmid"] == 104)["app"]["version"] = neue_version + text = json.dumps(neu) + kanal.ergebnis(auftrag["id"], 0, text) + + +@pytest.fixture +def ausfuehrer(daten, monkeypatch): + monkeypatch.setattr(updates, "WARTEN_NACH_UPDATE_S", 0) + monkeypatch.setattr(kanal, "warten", _schnell_warten) + meldungen: list[tuple[str, bool]] = [] + monkeypatch.setattr(updates, "_melden", lambda text, dringend=False: meldungen.append((text, dringend))) + verlauf: list[tuple] = [] + monkeypatch.setattr(updates.update_verlauf, "eintragen", lambda *a: verlauf.append(a)) + kanal.bericht_speichern(BERICHT) + stopp, protokoll = threading.Event(), [] + + def starten(neue_version: str) -> None: + threading.Thread(target=_ausfuehrer_spielen, args=(stopp, neue_version, protokoll), daemon=True).start() + + yield starten, protokoll, meldungen, verlauf + stopp.set() + + +_echtes_warten = kanal.warten + + +def _schnell_warten(auftrag_id, zeitlimit_s, takt_s=2.0): + return _echtes_warten(auftrag_id, min(zeitlimit_s, 10), 0.02) + + +def _auf_ende(lauf_id: str) -> dict: + ende = time.time() + 20 + while time.time() < ende: + lauf = next((x for x in updates.laeufe() if x["id"] == lauf_id), {}) + if lauf.get("status") == "fertig": + return lauf + time.sleep(0.05) + raise AssertionError(f"Lauf {lauf_id} wurde nicht fertig") + + +def test_update_gruen(ausfuehrer, monkeypatch): + starten, protokoll, meldungen, verlauf = ausfuehrer + starten("1.27.3") + antwort = updates.starten("ct-104", "app") + assert antwort["ok"] + assert updates.starten("ct-104", "app")["detail"] == "Für dieses Gerät läuft schon ein Update." + lauf = _auf_ende(antwort["lauf"]) + assert lauf["ergebnis"] == "eingespielt" and protokoll == ["snapshot", "update", "bericht"] + assert meldungen == [("Gitea: eingespielt, Prüfung grün.", False)] + assert verlauf[0][2:4] == ("ct-104", "eingespielt") + + +def test_update_rot_geht_zurueck_und_meldet_dringend(ausfuehrer, monkeypatch): + starten, protokoll, meldungen, _ = ausfuehrer + monkeypatch.setattr(inventar, "erreichbar", lambda url: False) + starten("1.27.3") + lauf = _auf_ende(updates.starten("ct-104", "app")["lauf"]) + assert lauf["ergebnis"] == "zurueckgerollt" + assert protokoll == ["snapshot", "update", "bericht", "zurueck"] + text, dringend = meldungen[0] + assert dringend and "zurück auf den Snapshot mc2-20260924-190000" in text and "antwortet nicht" in text + + +def test_update_ohne_neue_version_ist_rot(ausfuehrer): + starten, _, _, _ = ausfuehrer + starten("1.27.2") + lauf = _auf_ende(updates.starten("ct-104", "app")["lauf"]) + assert lauf["ergebnis"] == "zurueckgerollt" and "unverändert" in lauf["text"] + + +def test_nicht_freigegeben_und_unbekannt(ausfuehrer): + assert updates.starten("vm-106", "app")["detail"].startswith("Dieses Gerät ist nicht freigegeben") + assert updates.starten("ct-999", "app")["ok"] is False + assert updates.starten("ct-104", "boese")["ok"] is False + + +# --- Der Ausführer selbst (läuft auf dem Proxmox-Host) ------------------------------------------- + +def _ausfuehrer_modul(): + spec = importlib.util.spec_from_file_location("ausfuehrer", AUSFUEHRER) + modul = importlib.util.module_from_spec(spec) + spec.loader.exec_module(modul) + return modul + + +def test_ausfuehrer_entscheidet_selbst_ueber_etiketten(): + a = _ausfuehrer_modul() + assert a.erlaubt(["community-script", "git"]) and a.erlaubt(["watcher"]) + assert not a.erlaubt(["community-script", "watcher-aus"]) and not a.erlaubt([]) + + +def test_ausfuehrer_snapshot_faehigkeit(): + a = _ausfuehrer_modul() + arten = {"local-lvm": "lvmthin", "local": "dir"} + assert a.snapshot_moeglich({"rootfs": "local-lvm:vm-104-disk-0,size=8G"}, arten) == (True, None) + ok, grund = a.snapshot_moeglich({"rootfs": "local-lvm:vm-105-disk-0", "mp0": "/mnt/qnap,mp=/mnt/ds"}, arten) + assert not ok and "Bind-Mount" in grund + assert a.snapshot_moeglich({"rootfs": "local:105/vm-105-disk-0.raw"}, arten)[0] is False + + +def test_ausfuehrer_lehnt_fremdes_ab(monkeypatch): + a = _ausfuehrer_modul() + monkeypatch.setattr(a, "_pvesh", lambda pfad, *p: [ + {"vmid": 104, "type": "lxc", "tags": "community-script;git"}, + {"vmid": 106, "type": "qemu", "tags": ""}]) + ausgefuehrt: list[list[str]] = [] + monkeypatch.setattr(a, "_laufen", lambda befehl, zeitlimit=60: ausgefuehrt.append(befehl) or (0, "ok")) + assert a.ausfuehren({"aktion": "rm", "parameter": {}})["code"] == 2 + assert "kein erlaubtes Etikett" in a.ausfuehren({"aktion": "update", "parameter": {"vmid": 106}})["text"] + assert a.ausfuehren({"aktion": "zurueck", "parameter": {"vmid": 104, "snapshot": "vorher"}})["code"] == 2 + assert a.ausfuehren({"aktion": "update", "parameter": {"vmid": "104; reboot"}})["code"] == 2 + assert ausgefuehrt == [] + assert a.ausfuehren({"aktion": "update", "parameter": {"vmid": 104}})["code"] == 0 + assert ausgefuehrt[-1][:4] == ["pct", "exec", "104", "--"] and "PHS_SILENT=1" in ausgefuehrt[-1][-1] diff --git a/backend/tests/test_partner.py b/backend/tests/test_partner.py index aaf1a64..4439e4f 100644 --- a/backend/tests/test_partner.py +++ b/backend/tests/test_partner.py @@ -208,3 +208,18 @@ def test_unbekannte_rolle_bricht_ab(monkeypatch): finally: monkeypatch.delenv("MC_ROLLE") einstellungen_mod.einstellungen.cache_clear() + + +def test_homelab_waechter_prueft_ausfuehrer_und_gaeste(tmp_path): + ergebnis = _starte("homelab", tmp_path, """ +import json +import steward +from services import waechter +print(json.dumps({"pruefungen": [p.__name__ for p in waechter.PRUEFUNGEN], "daten": str(waechter.DATEN_DIR)})) +""") + assert ergebnis["pruefungen"] == ["pruefe_platte", "pruefe_partner", "pruefe_ausfuehrer", "pruefe_gaeste"] + + +def test_homelab_rolle_hat_die_homelab_schnittstellen(tmp_path): + ergebnis = _starte("homelab", tmp_path, _PROBE) + assert "/api/homelab/ziele" in ergebnis["pfade"] and "/api/ziele" not in ergebnis["pfade"] diff --git a/backend/tests/test_wartung.py b/backend/tests/test_wartung.py index 3ddd971..7ba4ac1 100644 --- a/backend/tests/test_wartung.py +++ b/backend/tests/test_wartung.py @@ -37,15 +37,19 @@ class _Antwort: def test_github_antworten_werden_gemerkt_fehler_nicht(monkeypatch): - monkeypatch.setattr(maintenance, "_github_cache", {}) - antworten = [_Antwort(403, {"message": "API rate limit exceeded"}), _Antwort(200, {"tag_name": "v258"})] + from kern import github + + monkeypatch.setattr(github, "_cache", {}) + antworten = [_Antwort(403, {"message": "API rate limit exceeded"}), + _Antwort(200, {"tag_name": "v258", "published_at": "2026-09-20T10:00:00Z"})] aufrufe = [] - monkeypatch.setattr(maintenance.httpx, "get", lambda url, **kw: aufrufe.append(url) or antworten.pop(0)) + monkeypatch.setattr(github.httpx, "get", lambda url, **kw: aufrufe.append(url) or antworten.pop(0)) with pytest.raises(httpx.HTTPStatusError): - maintenance._github_json("repos/a/b/releases/latest") - assert maintenance._github_json("repos/a/b/releases/latest") == {"tag_name": "v258"} - assert maintenance._github_json("repos/a/b/releases/latest") == {"tag_name": "v258"} + github.github_json("repos/a/b/releases/latest") + assert github.neueste_version("a/b") == {"tag": "v258", "version": "258", "datum": "2026-09-20"} + assert github.github_json("repos/a/b/releases/latest")["tag_name"] == "v258" assert len(aufrufe) == 2 + assert maintenance._github_json is github.github_json def test_hermes_version_auch_wenn_der_abruf_haengt(monkeypatch, tmp_path): diff --git a/deploy/frontend-bauen-box.sh b/deploy/frontend-bauen-box.sh new file mode 100644 index 0000000..63748fe --- /dev/null +++ b/deploy/frontend-bauen-box.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# frontend-bauen-box.sh — Frontend-Prüfungen und Build auf der KI-Box statt am PC (24.09.2026). +# +# Der PC hatte zu wenig freien Speicher (Spiel lief nebenher), tsc und eslint brachen mit „heap out of +# memory" ab. Die Box hat genug davon und einen Node (den Hermes für seine Oberfläche mitbringt). +# Geschickt wird der Arbeitsstand von frontend/ (ohne node_modules und dist), zurück kommt dist/. +# +# Nutzung (Git-Bash am PC, im Repo): bash deploy/frontend-bauen-box.sh +# Ändert auf der Box nichts außer einem Wegwerf-Ordner unter /tmp. +set -euo pipefail +cd "$(dirname "$0")/.." +BOX="${MC_BOX:-hitonabi@192.168.178.151}" +ORDNER="/tmp/mc2-frontend-$$" + +echo "Frontend-Prüfung und Build auf $BOX …" +tar --exclude=node_modules --exclude=dist -cf - frontend \ + | ssh -o BatchMode=yes "$BOX" "rm -rf $ORDNER && mkdir -p $ORDNER && tar -x -C $ORDNER" +# shellcheck disable=SC2029 # $ORDNER soll hier (am PC) eingesetzt werden +ssh -o BatchMode=yes "$BOX" "set -e; export PATH=\$HOME/.hermes/node/bin:\$PATH; cd $ORDNER/frontend + npm ci --no-audit --no-fund --loglevel=error + echo '── Lint'; npm run --silent lint + echo '── Typen'; npx tsc --noEmit + echo '── Tests'; npm test --silent + echo '── Build'; npm run --silent build" +rm -rf frontend/dist +ssh -o BatchMode=yes "$BOX" "tar -cf - -C $ORDNER/frontend dist && rm -rf $ORDNER" | tar -x -C frontend +echo "✅ frontend/dist ist neu gebaut ($(du -sh frontend/dist | cut -f1))." diff --git a/deploy/homelab/ausfuehrer-einrichten.sh b/deploy/homelab/ausfuehrer-einrichten.sh new file mode 100644 index 0000000..36f9425 --- /dev/null +++ b/deploy/homelab/ausfuehrer-einrichten.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# ausfuehrer-einrichten.sh — den Ausführer auf dem Proxmox-Host einrichten (Phase 3/4). +# +# Läuft am PC (Git-Bash, im Repo). Legt ausfuehrer.py, die Unit und die Konfiguration auf den +# Proxmox-Host und startet den Dienst. Das gemeinsame Geheimnis wandert vom Container des Homelab-Teils +# per Pipe direkt auf den Host (0600) — es steht in keiner Befehlszeile. +# +# Nutzung: bash deploy/homelab/ausfuehrer-einrichten.sh +set -euo pipefail +cd "$(dirname "$0")/../.." +IP="${1:?Nutzung: ausfuehrer-einrichten.sh }" +PVE="${MC_PVE:-pve}" + +ssh -o BatchMode=yes "$PVE" "install -d -m 755 /usr/local/lib/mc2" +ssh -o BatchMode=yes "$PVE" "cat > /usr/local/lib/mc2/ausfuehrer.py && chmod 755 /usr/local/lib/mc2/ausfuehrer.py" \ + < deploy/homelab/ausfuehrer.py +ssh -o BatchMode=yes "$PVE" "cat > /etc/systemd/system/mc2-ausfuehrer.service" < deploy/homelab/mc2-ausfuehrer.service +# shellcheck disable=SC2029 # $IP soll hier (am PC) eingesetzt werden +ssh -o BatchMode=yes "root@$IP" "cat /var/lib/mc2/ausfuehrer.token" \ + | ssh -o BatchMode=yes "$PVE" "umask 077; read -r t; [ -n \"\$t\" ] || exit 1; \ + printf '{\"server\": \"http://%s:9001\", \"token\": \"%s\", \"node\": \"%s\"}\n' '$IP' \"\$t\" \"\$(hostname)\" \ + > /etc/mc2-ausfuehrer.json" +ssh -o BatchMode=yes "$PVE" "systemctl daemon-reload && systemctl enable --now mc2-ausfuehrer.service \ + && systemctl restart mc2-ausfuehrer.service && sleep 5 && systemctl is-active mc2-ausfuehrer.service" +echo "✅ Ausführer läuft. Der erste Bericht erscheint in etwa einer Minute unter /api/homelab/ziele." diff --git a/deploy/homelab/ausfuehrer.py b/deploy/homelab/ausfuehrer.py new file mode 100644 index 0000000..2141e2c --- /dev/null +++ b/deploy/homelab/ausfuehrer.py @@ -0,0 +1,401 @@ +#!/usr/bin/env python3 +"""Ausführer des Homelab Orchestrators auf dem Proxmox-Host (Phase 3/4, 24.09.2026). + +Läuft als root auf dem Proxmox-PC (mc2-ausfuehrer.service) und holt sich seine Arbeit selbst +beim Homelab-Teil ab („Pull“, User-Entscheid 24.09.): Der Proxmox-Host öffnet keinen Port, und der +Container des Homelab-Teils braucht keinen Proxmox-Schlüssel. + + • Bericht alle 10 Minuten (oder auf Anfrage): Host-Updates, Gäste mit Status, App-Version, + OS-Updates, Snapshots und ob ein Snapshot überhaupt geht. Nur lesend. + • Aufträge eine feste Liste von Aktionen (AKTIONEN); alles andere wird abgelehnt. Welche Gäste + angefasst werden dürfen, entscheidet dieser Host selbst anhand der Etiketten: + community-script oder watcher, aber nicht watcher-aus. Dem Server wird dabei nicht + vertraut — er kann nur aus der Liste wählen. + +Nur die Standardbibliothek (Debian-Python des Hosts). Konfiguration: /etc/mc2-ausfuehrer.json + {"server": "http://192.168.178.x:9001", "token": "", "node": "pve"} + +Aufruf: ausfuehrer.py Dauerbetrieb (systemd) + ausfuehrer.py --bericht Bericht einmal auf die Konsole (nur lesend, zum Prüfen) +""" + +import argparse +import json +import logging +import os +import platform +import re +import subprocess +import sys +import time +import urllib.error +import urllib.request +from datetime import datetime, timezone + +KONFIG = os.environ.get("MC2_AUSFUEHRER_KONFIG", "/etc/mc2-ausfuehrer.json") +BERICHT_ALLE_S = 600 +ABFRAGE_ALLE_S = 5 +ERLAUBT = {"community-script", "watcher"} +AUSGENOMMEN = "watcher-aus" +SNAPSHOT_NAME = re.compile(r"^mc2-\d{8}-\d{6}$") +# Speicherarten, auf denen Proxmox Snapshots kann. +SNAPSHOT_SPEICHER = {"lvmthin", "zfspool", "rbd", "btrfs", "cephfs"} + +# App-Version je Community-Script (Kennung aus /usr/bin/update im Gast). Neuere Skripte legen die Version +# in /root/. ab; für die älteren steht hier, wie man sie erfährt. Festes Wissen dieses Hosts. +VERSION_PROBEN = { + "adguard": "/opt/AdGuardHome/AdGuardHome --version", + "netbird": "netbird version", + "proxmox-backup-server": "dpkg-query -W proxmox-backup-server", + "npmplus": "docker image inspect zoeyvid/npmplus:latest --format '{{.Created}}'", +} +VERSIONSDATEIEN = {"pve-scripts-local": "/root/.proxmoxve-local"} + +log = logging.getLogger("ausfuehrer") + + +# --- Hilfen ----------------------------------------------------------------------------- + +def _laufen(befehl: list[str], zeitlimit: int = 60) -> tuple[int, str]: + try: + r = subprocess.run(befehl, capture_output=True, text=True, timeout=zeitlimit) + except subprocess.TimeoutExpired: + return 124, f"Zeitlimit ({zeitlimit} s) überschritten" + except OSError as exc: + return 127, str(exc) + return r.returncode, (r.stdout + r.stderr).strip() + + +def _pvesh(pfad: str, *parameter: str) -> object: + code, text = _laufen(["pvesh", "get", pfad, *parameter, "--output-format", "json"], 30) + if code != 0: + raise RuntimeError(f"pvesh {pfad}: {text[:200]}") + return json.loads(text or "null") + + +def _im_gast(vmid: int, befehl: str, zeitlimit: int = 30) -> tuple[int, str]: + return _laufen(["pct", "exec", str(vmid), "--", "sh", "-c", befehl], zeitlimit) + + +def _etiketten(roh: str | None) -> list[str]: + return [t for t in (roh or "").replace(",", ";").split(";") if t] + + +def erlaubt(etiketten: list[str]) -> bool: + menge = set(etiketten) + return bool(menge & ERLAUBT) and AUSGENOMMEN not in menge + + +# --- Bericht (nur lesend) ------------------------------------------------------------------- + +def _speicherart() -> dict[str, str]: + try: + return {s["storage"]: s["type"] for s in _pvesh(f"/nodes/{NODE}/storage")} + except Exception: + return {} + + +def snapshot_moeglich(konfig: dict, arten: dict[str, str]) -> tuple[bool, str | None]: + """Geht ein Snapshot? Bind-Mounts (mpX mit Host-Pfad) und Speicher ohne Snapshots verhindern ihn.""" + for schluessel, wert in konfig.items(): + if re.fullmatch(r"mp\d+", schluessel) and str(wert).split(",")[0].startswith("/"): + return False, f"Bind-Mount {schluessel} ({str(wert).split(',')[0]}) — nur Backup möglich" + for schluessel in ("rootfs", "scsi0", "virtio0", "sata0", "ide0"): + if schluessel in konfig: + speicher = str(konfig[schluessel]).split(":")[0] + art = arten.get(speicher) + if art and art not in SNAPSHOT_SPEICHER: + return False, f"Speicher {speicher} ({art}) kann keine Snapshots" + return True, None + + +def _app_kennung(vmid: int) -> str | None: + """Welches Community-Script steckt im Gast? /usr/bin/update lädt ct/.sh.""" + code, text = _im_gast(vmid, "cat /usr/bin/update 2>/dev/null", 10) + m = re.search(r"/ct/([a-z0-9-]+)\.sh", text) if code == 0 else None + return m.group(1) if m else None + + +def _app_version(vmid: int, kennung: str) -> str | None: + befehl = VERSION_PROBEN.get(kennung) + if befehl is None: + datei = VERSIONSDATEIEN.get(kennung, f"/root/.{kennung}") + befehl = f"cat {datei} 2>/dev/null" + code, text = _im_gast(vmid, befehl, 20) + if code != 0 or not text: + return None + zeile = text.splitlines()[-1].strip() + # Docker-Apps (NPMplus) haben keine Versionsnummer, nur das Datum ihres Images. + if m := re.match(r"(\d{4}-\d{2}-\d{2})T", zeile): + return f"Image vom {m.group(1)}" + m = re.search(r"v?(\d+(?:\.\d+){1,3}(?:[-+][\w.]+)?)", zeile) + return m.group(1) if m else zeile[:40] + + +def _os_updates(vmid: int, ostype: str) -> dict: + """Aktualisierbare Pakete nach den vorhandenen Paketlisten (liest nur) und wie alt die Listen sind.""" + if ostype == "alpine": + code, text = _im_gast(vmid, "apk version -l '<' 2>/dev/null | tail -n +2 | wc -l; " + "stat -c %Y /var/cache/apk 2>/dev/null || echo 0", 30) + else: + code, text = _im_gast(vmid, "LC_ALL=C apt list --upgradable 2>/dev/null | grep -c upgradable; " + "stat -c %Y /var/lib/apt/lists 2>/dev/null || echo 0", 30) + zeilen = text.split() if code in (0, 1) else [] + try: + return {"anzahl": int(zeilen[0]), "listen_stand": int(zeilen[1]) or None} + except (IndexError, ValueError): + return {"anzahl": None, "listen_stand": None, "fehler": text[:160]} + + +def _ip_lxc(vmid: int) -> str | None: + try: + for schnitt in _pvesh(f"/nodes/{NODE}/lxc/{vmid}/interfaces"): + if schnitt.get("name") in ("eth0", "ens18"): + for adresse in schnitt.get("ip-addresses", []): + if adresse.get("ip-address-type") == "inet": + return adresse["ip-address"].split("/")[0] + except Exception: + pass + return None + + +def _ip_vm(vmid: int) -> str | None: + try: + daten = _pvesh(f"/nodes/{NODE}/qemu/{vmid}/agent/network-get-interfaces") + for schnitt in (daten or {}).get("result", []): + if schnitt.get("name") == "lo": + continue + for adresse in schnitt.get("ip-addresses", []): + ip = adresse.get("ip-address", "") + if adresse.get("ip-address-type") == "ipv4" and not ip.startswith(("127.", "172.")): + return ip + except Exception: + pass + return None + + +def _snapshots(art: str, vmid: int) -> list[str]: + try: + return [s["name"] for s in _pvesh(f"/nodes/{NODE}/{art}/{vmid}/snapshot") if s.get("name") != "current"] + except Exception: + return [] + + +def _gast(eintrag: dict, arten: dict[str, str]) -> dict: + art, vmid = eintrag["type"], int(eintrag["vmid"]) + etiketten = _etiketten(eintrag.get("tags")) + gast = {"vmid": vmid, "art": art, "name": eintrag.get("name"), "status": eintrag.get("status"), + "etiketten": etiketten, "erlaubt": erlaubt(etiketten), "uptime": eintrag.get("uptime"), + "snapshots": _snapshots(art, vmid)} + try: + konfig = _pvesh(f"/nodes/{NODE}/{art}/{vmid}/config") + except Exception as exc: + gast["fehler"] = str(exc)[:200] + return gast + gast["onboot"] = bool(konfig.get("onboot")) + gast["snapshot_moeglich"], gast["snapshot_grund"] = snapshot_moeglich(konfig, arten) + if art == "lxc": + gast["ostype"] = konfig.get("ostype") + gast["ip"] = _ip_lxc(vmid) if gast["status"] == "running" else None + if gast["status"] == "running" and gast["erlaubt"]: + kennung = _app_kennung(vmid) + gast["app"] = {"kennung": kennung, "version": _app_version(vmid, kennung) if kennung else None} + gast["os_updates"] = _os_updates(vmid, str(konfig.get("ostype") or "debian")) + else: + gast["ip"] = _ip_vm(vmid) if gast["status"] == "running" else None + return gast + + +def _host() -> dict: + host: dict = {"node": NODE, "neustart_noetig": os.path.exists("/var/run/reboot-required")} + try: + host["version"] = _pvesh("/version").get("version") + host["updates"] = [{"paket": p.get("Package"), "alt": p.get("OldVersion"), "neu": p.get("Version"), + "herkunft": p.get("Origin")} for p in _pvesh(f"/nodes/{NODE}/apt/update")] + except Exception as exc: + host["fehler"] = str(exc)[:200] + code, kernel = _laufen(["uname", "-r"], 5) + host["kernel"] = kernel if code == 0 else None + return host + + +def bericht() -> dict: + arten = _speicherart() + gaeste = [] + for eintrag in _pvesh("/cluster/resources", "--type", "vm") or []: + if eintrag.get("node") == NODE and not eintrag.get("template"): + gaeste.append(_gast(eintrag, arten)) + return {"zeit": datetime.now(timezone.utc).isoformat(timespec="seconds"), "host": _host(), + "gaeste": sorted(gaeste, key=lambda g: g["vmid"])} + + +# --- Aufträge (feste Liste) --------------------------------------------------------------- + +def _gast_pruefen(vmid: object) -> tuple[str, int]: + """Gibt (art, vmid) zurück, wenn dieser Host den Gast anfassen darf — sonst ValueError.""" + if not isinstance(vmid, int) or not 100 <= vmid <= 999_999_999: + raise ValueError("ungültige Gast-Nummer") + for eintrag in _pvesh("/cluster/resources", "--type", "vm") or []: + if int(eintrag.get("vmid", -1)) == vmid: + if not erlaubt(_etiketten(eintrag.get("tags"))): + raise ValueError(f"Gast {vmid} hat kein erlaubtes Etikett (community-script/watcher) " + f"oder ist mit {AUSGENOMMEN} ausgenommen") + return eintrag["type"], vmid + raise ValueError(f"Gast {vmid} gibt es nicht") + + +def _werkzeug(art: str) -> str: + return "pct" if art == "lxc" else "qm" + + +def a_snapshot(p: dict) -> tuple[int, str]: + art, vmid = _gast_pruefen(p.get("vmid")) + name = "mc2-" + datetime.now().strftime("%Y%m%d-%H%M%S") + code, text = _laufen([_werkzeug(art), "snapshot", str(vmid), name, "--description", + "Vor einem Update durch den Homelab Orchestrator"], 600) + return code, f"snapshot={name}\n{text}" if code == 0 else text + + +def a_update(p: dict) -> tuple[int, str]: + art, vmid = _gast_pruefen(p.get("vmid")) + if art != "lxc": + raise ValueError("Update per Community-Script gibt es nur für Container") + # PHS_SILENT=1: das Community-Script fragt nichts. Es lädt ct/.sh live von GitHub (main). + return _im_gast(vmid, "PHS_SILENT=1 bash /usr/bin/update", int(p.get("zeitlimit") or 1800)) + + +def a_suchen(p: dict) -> tuple[int, str]: + art, vmid = _gast_pruefen(p.get("vmid")) + if art != "lxc": + raise ValueError("nur für Container") + return _im_gast(vmid, "if command -v apk >/dev/null; then apk update; else apt-get update -q; fi", 300) + + +def a_os_update(p: dict) -> tuple[int, str]: + """Die Pakete des Gasts (unabhängig vom App-Update des Community-Scripts).""" + art, vmid = _gast_pruefen(p.get("vmid")) + if art != "lxc": + raise ValueError("nur für Container") + befehl = ("if command -v apk >/dev/null; then apk update && apk upgrade; else apt-get update -q && " + "DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::=--force-confold dist-upgrade; fi") + return _im_gast(vmid, befehl, int(p.get("zeitlimit") or 1800)) + + +def a_zurueck(p: dict) -> tuple[int, str]: + art, vmid = _gast_pruefen(p.get("vmid")) + name = str(p.get("snapshot") or "") + if not SNAPSHOT_NAME.match(name): + raise ValueError("nur Snapshots des Orchestrators (mc2-…) lassen sich zurückspielen") + code, text = _laufen([_werkzeug(art), "rollback", str(vmid), name], 900) + if code == 0: + code2, text2 = _laufen([_werkzeug(art), "start", str(vmid)], 120) + text += "\n" + text2 + code = 0 if code2 == 0 or "already running" in text2 else code2 + return code, text + + +def a_snapshot_loeschen(p: dict) -> tuple[int, str]: + art, vmid = _gast_pruefen(p.get("vmid")) + name = str(p.get("snapshot") or "") + if not SNAPSHOT_NAME.match(name): + raise ValueError("nur Snapshots des Orchestrators (mc2-…) werden gelöscht") + return _laufen([_werkzeug(art), "delsnapshot", str(vmid), name], 600) + + +def a_host_update(p: dict) -> tuple[int, str]: + # Kein Neustart: der ist ein eigener Knopf (User-Entscheid 24.09.2026). + befehl = ("apt-get update -q && DEBIAN_FRONTEND=noninteractive apt-get -y " + "-o Dpkg::Options::=--force-confold dist-upgrade") + return _laufen(["bash", "-c", befehl], 3600) + + +def a_host_neustart(p: dict) -> tuple[int, str]: + return _laufen(["systemctl", "reboot"], 30) + + +def a_bericht(p: dict) -> tuple[int, str]: + return 0, json.dumps(bericht(), ensure_ascii=False) + + +AKTIONEN = {"snapshot": a_snapshot, "update": a_update, "os_update": a_os_update, "suchen": a_suchen, + "zurueck": a_zurueck, + "snapshot_loeschen": a_snapshot_loeschen, "host_update": a_host_update, + "host_neustart": a_host_neustart, "bericht": a_bericht} + + +def ausfuehren(auftrag: dict) -> dict: + aktion = AKTIONEN.get(str(auftrag.get("aktion"))) + if aktion is None: + return {"code": 2, "text": f"Unbekannte Aktion: {auftrag.get('aktion')!r}"} + try: + code, text = aktion(auftrag.get("parameter") or {}) + except (ValueError, RuntimeError) as exc: + return {"code": 2, "text": f"Abgelehnt: {exc}"} + return {"code": code, "text": text[-20000:]} + + +# --- Verbindung zum Homelab-Teil ------------------------------------------------------------- + +def _anfrage(methode: str, pfad: str, daten: object = None) -> object: + roh = None if daten is None else json.dumps(daten).encode() + anfrage = urllib.request.Request(SERVER + pfad, data=roh, method=methode, + headers={"Content-Type": "application/json", "X-MC2-Ausfuehrer": TOKEN}) + with urllib.request.urlopen(anfrage, timeout=30) as antwort: + text = antwort.read().decode() or "null" + return json.loads(text) + + +def dauerbetrieb() -> None: + naechster_bericht = 0.0 + while True: + try: + if time.time() >= naechster_bericht: + _anfrage("POST", "/api/homelab/ausfuehrer/bericht", bericht()) + naechster_bericht = time.time() + BERICHT_ALLE_S + auftrag = _anfrage("GET", "/api/homelab/ausfuehrer/auftrag") + if isinstance(auftrag, dict) and auftrag.get("id"): + log.info("Auftrag %s: %s %s", auftrag["id"], auftrag.get("aktion"), auftrag.get("parameter")) + ergebnis = ausfuehren(auftrag) + _anfrage("POST", f"/api/homelab/ausfuehrer/ergebnis/{auftrag['id']}", ergebnis) + if auftrag.get("aktion") != "bericht": + naechster_bericht = 0.0 # nach jeder Änderung einen frischen Bericht + continue + except (urllib.error.URLError, OSError, ValueError) as exc: + log.warning("Homelab-Teil nicht erreichbar: %s", exc) + except Exception: + log.exception("Unerwarteter Fehler") + time.sleep(ABFRAGE_ALLE_S) + + +def _konfig_laden() -> dict: + try: + with open(KONFIG, encoding="utf-8") as f: + return json.load(f) + except (OSError, ValueError): + return {} + + +_konfig = _konfig_laden() +SERVER = str(_konfig.get("server") or "").rstrip("/") +TOKEN = str(_konfig.get("token") or "") +NODE = str(_konfig.get("node") or platform.node()) + + +def main() -> int: + logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") + teile = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + teile.add_argument("--bericht", action="store_true", help="Bericht einmal ausgeben (nur lesend)") + args = teile.parse_args() + if args.bericht: + json.dump(bericht(), sys.stdout, ensure_ascii=False, indent=2) + print() + return 0 + if not SERVER or not TOKEN: + log.error("Konfiguration %s fehlt oder ist unvollständig (server, token).", KONFIG) + return 1 + dauerbetrieb() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/deploy/homelab/ausrollen.sh b/deploy/homelab/ausrollen.sh new file mode 100644 index 0000000..3eebbaa --- /dev/null +++ b/deploy/homelab/ausrollen.sh @@ -0,0 +1,19 @@ +#!/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). +# +# Nutzung: bash deploy/homelab/ausrollen.sh [partner-url] +set -euo pipefail +cd "$(dirname "$0")/../.." +IP="${1:?Nutzung: ausrollen.sh [partner-url]}" +PARTNER="${2:-http://192.168.178.151:9001}" +ZIEL="root@$IP" + +echo "Rolle $(git rev-parse --short HEAD) nach $IP aus …" +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'" diff --git a/deploy/homelab/box-partner.sh b/deploy/homelab/box-partner.sh new file mode 100644 index 0000000..f7f649d --- /dev/null +++ b/deploy/homelab/box-partner.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# box-partner.sh — der KI-Box sagen, wo der Homelab-Teil läuft (Phase 3). +# +# Läuft am PC (Git-Bash). Legt auf der Box Drop-ins für mission-control-2 und mc2-steward mit +# MC_PARTNER_URL an und startet beide neu. Die Drop-ins stehen nicht im Repo (die IP gehört zum Heimnetz), +# deploy.sh lässt sie stehen. Danach zeigt die Seite „Homelab“ die Geräte, und der Wächter der Box prüft +# den Homelab-Teil (und umgekehrt). +# +# Nutzung: bash deploy/homelab/box-partner.sh +set -euo pipefail +IP="${1:?Nutzung: box-partner.sh }" +BOX="${MC_BOX:-hitonabi@192.168.178.151}" +# shellcheck disable=SC2029 # $IP soll hier (am PC) eingesetzt werden +ssh -o BatchMode=yes "$BOX" "export XDG_RUNTIME_DIR=/run/user/\$(id -u); for u in mission-control-2 mc2-steward; do + mkdir -p ~/.config/systemd/user/\$u.service.d + printf '[Service]\nEnvironment=MC_PARTNER_URL=http://%s:9001\nEnvironment=MC_PARTNER_NAME=Homelab\n' '$IP' \ + > ~/.config/systemd/user/\$u.service.d/partner.conf + done + systemctl --user daemon-reload && systemctl --user restart mission-control-2 mc2-steward + sleep 4; curl -s -m 10 http://127.0.0.1:9001/api/partner" +echo diff --git a/deploy/homelab/container-anlegen.sh b/deploy/homelab/container-anlegen.sh new file mode 100644 index 0000000..425ecdb --- /dev/null +++ b/deploy/homelab/container-anlegen.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# container-anlegen.sh — den Container für den Homelab-Teil auf dem Proxmox-PC anlegen (Phase 3). +# +# Läuft am PC (Git-Bash) und spricht per SSH mit dem Proxmox-Host (Alias „pve“, Schlüssel id_lucy_infra). +# Legt EINEN unprivilegierten Debian-13-Container an: 1 Kern, 1 GB RAM, 4 GB Platte, DHCP, Autostart. +# Etikett „mc2“ — bewusst NICHT community-script/watcher: Der Orchestrator aktualisiert sich nicht selbst. +# +# Nutzung: bash deploy/homelab/container-anlegen.sh [vmid] +# Danach: bash deploy/homelab/ausrollen.sh +set -euo pipefail +PVE="${MC_PVE:-pve}" +VORLAGE="${MC_VORLAGE:-local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst}" +VMID="${1:-$(ssh -o BatchMode=yes "$PVE" pvesh get /cluster/nextid)}" + +echo "Lege Container $VMID (homelab-orchestrator) auf $PVE an …" +ssh -o BatchMode=yes "$PVE" "pct create $VMID $VORLAGE \ + --hostname homelab-orchestrator --description 'Homelab Orchestrator – Homelab-Teil (MC2, Rolle homelab)' \ + --cores 1 --memory 1024 --swap 512 --rootfs local-lvm:4 \ + --net0 name=eth0,bridge=vmbr0,ip=dhcp --unprivileged 1 --features nesting=1 \ + --onboot 1 --tags mc2 --timezone Europe/Berlin --start 1" + +# Auf die IP warten (DHCP), dann den SSH-Schlüssel des PCs und der Box eintragen (wie bei allen Gästen). +for _ in $(seq 1 30); do + IP="$(ssh -o BatchMode=yes "$PVE" "pct exec $VMID -- hostname -I 2>/dev/null | awk '{print \$1}'" || true)" + [ -n "$IP" ] && break + sleep 2 +done +[ -n "${IP:-}" ] || { echo "✗ Container $VMID hat keine IP bekommen."; exit 1; } +ssh -o BatchMode=yes "$PVE" "pct exec $VMID -- bash -c 'apt-get update -q && apt-get install -y -q openssh-server && \ + mkdir -p /root/.ssh && chmod 700 /root/.ssh' && cat /root/.ssh/authorized_keys | pct exec $VMID -- bash -c \ + 'cat >> /root/.ssh/authorized_keys && chmod 600 /root/.ssh/authorized_keys && systemctl enable --now ssh'" +echo "✅ Container $VMID läuft unter $IP. Weiter mit: bash deploy/homelab/ausrollen.sh $IP" diff --git a/deploy/homelab/einrichten.sh b/deploy/homelab/einrichten.sh new file mode 100644 index 0000000..384d5c1 --- /dev/null +++ b/deploy/homelab/einrichten.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# einrichten.sh — läuft IM Container des Homelab-Teils als root (ausrollen.sh ruft es auf). +# +# Idempotent: Pakete, Dienst-Nutzer mc2, Ordner, Python-Umgebung, systemd-Units. Die Konfiguration +# /etc/mc2/homelab.env (MC_PARTNER_URL …) bleibt, wenn es sie schon gibt. +set -euo pipefail +ZIEL=/opt/mc2 +PARTNER="${1:-http://192.168.178.151:9001}" + +apt-get update -q +DEBIAN_FRONTEND=noninteractive apt-get install -y -q python3 python3-venv curl ca-certificates + +id mc2 >/dev/null 2>&1 || useradd --system --home-dir /var/lib/mc2 --shell /usr/sbin/nologin mc2 +install -d -o mc2 -g mc2 -m 750 /var/lib/mc2 +install -d -o root -g mc2 -m 750 /etc/mc2 +[ -f /etc/mc2/homelab.env ] || printf 'MC_PARTNER_URL=%s\nMC_PARTNER_NAME=Box-Wart\n' "$PARTNER" > /etc/mc2/homelab.env +chown root:mc2 /etc/mc2/homelab.env && chmod 640 /etc/mc2/homelab.env + +python3 -m venv "$ZIEL/backend/.venv" +"$ZIEL/backend/.venv/bin/pip" install -q --upgrade pip +"$ZIEL/backend/.venv/bin/pip" install -q -r "$ZIEL/backend/requirements.txt" +chown -R root:root "$ZIEL" + +install -m 644 "$ZIEL"/deploy/homelab/mc2-homelab*.service "$ZIEL"/deploy/homelab/mc2-homelab*.timer /etc/systemd/system/ +systemctl daemon-reload +systemctl enable --now mc2-homelab.service mc2-homelab-steward.service mc2-homelab-morgenmeldung.timer +systemctl restart mc2-homelab.service mc2-homelab-steward.service + +for _ in $(seq 1 20); do + curl -sf -m 3 http://127.0.0.1:9001/api/health >/dev/null && break + sleep 1 +done +curl -sf -m 3 http://127.0.0.1:9001/api/health && echo +# Das gemeinsame Geheimnis für den Ausführer entsteht beim ersten Abruf (0600, Nutzer mc2). +curl -sf -m 5 http://127.0.0.1:9001/api/homelab/ausfuehrer >/dev/null || true +echo "✅ Homelab-Teil läuft." diff --git a/deploy/homelab/mc2-ausfuehrer.service b/deploy/homelab/mc2-ausfuehrer.service new file mode 100644 index 0000000..ff1c3eb --- /dev/null +++ b/deploy/homelab/mc2-ausfuehrer.service @@ -0,0 +1,13 @@ +[Unit] +Description=Homelab Orchestrator – Ausführer auf dem Proxmox-Host (holt Aufträge beim Homelab-Teil ab) +After=network-online.target pve-cluster.service +Wants=network-online.target + +[Service] +Type=simple +ExecStart=/usr/bin/python3 /usr/local/lib/mc2/ausfuehrer.py +Restart=always +RestartSec=15 + +[Install] +WantedBy=multi-user.target diff --git a/deploy/homelab/mc2-homelab-morgenmeldung.service b/deploy/homelab/mc2-homelab-morgenmeldung.service new file mode 100644 index 0000000..9471ecf --- /dev/null +++ b/deploy/homelab/mc2-homelab-morgenmeldung.service @@ -0,0 +1,13 @@ +[Unit] +Description=Homelab Orchestrator – Morgenmeldung des Homelab-Teils (Nachtmeldungen als eine Nachricht) + +[Service] +Type=oneshot +User=mc2 +Group=mc2 +Environment=MC_NOTIFY_HERMES=/bin/false +Environment=MC_TELEGRAM_ENV=/etc/mc2/telegram.env +Environment=MC_NOTIFY_LOG=/var/lib/mc2/notify.log +Environment=MC_NIGHT_QUEUE=/var/lib/mc2/night-queue.txt +Environment="MC_MORGENMELDUNG_BETREFF=[Morgenmeldung Homelab]" +ExecStart=/bin/bash /opt/mc2/deploy/morgenmeldung.sh diff --git a/deploy/homelab/mc2-homelab-morgenmeldung.timer b/deploy/homelab/mc2-homelab-morgenmeldung.timer new file mode 100644 index 0000000..3d13e2e --- /dev/null +++ b/deploy/homelab/mc2-homelab-morgenmeldung.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Homelab Orchestrator – Morgenmeldung des Homelab-Teils um 07:00 + +[Timer] +OnCalendar=*-*-* 07:00:00 +Persistent=false + +[Install] +WantedBy=timers.target diff --git a/deploy/homelab/mc2-homelab-steward.service b/deploy/homelab/mc2-homelab-steward.service new file mode 100644 index 0000000..4f97766 --- /dev/null +++ b/deploy/homelab/mc2-homelab-steward.service @@ -0,0 +1,24 @@ +[Unit] +Description=Homelab Orchestrator – Wächter des Homelab-Teils (Ausführer, Gäste, Partner, Platte) +After=mc2-homelab.service + +[Service] +Type=simple +User=mc2 +Group=mc2 +WorkingDirectory=/opt/mc2/backend +EnvironmentFile=/etc/mc2/homelab.env +Environment=MC_ROLLE=homelab +Environment=MC_DATEN_DIR=/var/lib/mc2 +Environment=MC_NOTIFY_HERMES=/bin/false +Environment=MC_NOTIFY_NO_ANNOUNCE=1 +Environment=MC_TELEGRAM_ENV=/etc/mc2/telegram.env +Environment=MC_NOTIFY_LOG=/var/lib/mc2/notify.log +Environment=MC_NIGHT_QUEUE=/var/lib/mc2/night-queue.txt +Environment=MC_SENTRY_MC2_URL=http://127.0.0.1:9001 +ExecStart=/opt/mc2/backend/.venv/bin/python steward.py +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target diff --git a/deploy/homelab/mc2-homelab.service b/deploy/homelab/mc2-homelab.service new file mode 100644 index 0000000..1682224 --- /dev/null +++ b/deploy/homelab/mc2-homelab.service @@ -0,0 +1,26 @@ +[Unit] +Description=Homelab Orchestrator – Homelab-Teil (Oberfläche und Schnittstellen, Rolle homelab) +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=mc2 +Group=mc2 +WorkingDirectory=/opt/mc2/backend +EnvironmentFile=/etc/mc2/homelab.env +Environment=MC_ROLLE=homelab +Environment=MC_DATEN_DIR=/var/lib/mc2 +# Kein Hermes im Container: Meldungen gehen über den Zweitweg direkt an die Telegram-Bot-API. +Environment=MC_NOTIFY_HERMES=/bin/false +Environment=MC_NOTIFY_NO_ANNOUNCE=1 +Environment=MC_TELEGRAM_ENV=/etc/mc2/telegram.env +Environment=MC_NOTIFY_LOG=/var/lib/mc2/notify.log +Environment=MC_NIGHT_QUEUE=/var/lib/mc2/night-queue.txt +Environment=MC_UPDATE_VERLAUF=/var/lib/mc2/mc2-update-verlauf.jsonl +ExecStart=/opt/mc2/backend/.venv/bin/uvicorn app:app --host 0.0.0.0 --port 9001 --timeout-graceful-shutdown 3 +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/deploy/morgenmeldung.sh b/deploy/morgenmeldung.sh index cb0cf19..bb0186f 100644 --- a/deploy/morgenmeldung.sh +++ b/deploy/morgenmeldung.sh @@ -13,6 +13,8 @@ SRC_DIR="$(cd "$(dirname "$0")" && pwd)" MAX_ZEICHEN="${MC_MORGENMELDUNG_MAX:-3500}" # Telegram erlaubt 4096 Zeichen je Nachricht VERSUCHE="${MC_MORGENMELDUNG_VERSUCHE:-3}" PAUSE="${MC_MORGENMELDUNG_PAUSE:-60}" +# Der Homelab-Teil schickt seine eigene Morgenmeldung (mc2-homelab-morgenmeldung.service). +BETREFF="${MC_MORGENMELDUNG_BETREFF:-[Morgenmeldung]}" # Melde-Log begrenzen (wuchs seit Juli ungebremst): über 3 MB bleiben die letzten 2 MB. # services/update_verlauf.py liest ohnehin nur das Ende, das reicht für Monate Update-Verlauf. @@ -44,7 +46,7 @@ for ((i = 1; i <= VERSUCHE; i++)); do # Als 07:00 senden (außerhalb der Nachtruhe); NO_ANNOUNCE, weil jede Meldung schon einzeln im # Briefkasten liegt. if MC_NOTIFY_STRICT=1 MC_NOTIFY_NO_ANNOUNCE=1 MC_NOTIFY_STUNDE=07 \ - bash "$SRC_DIR/notify.sh" -s "[Morgenmeldung]" "$NACHRICHT"; then + bash "$SRC_DIR/notify.sh" -s "$BETREFF" "$NACHRICHT"; then mv -f "$ARBEIT" "$QUEUE.zuletzt-gesendet" exit 0 fi diff --git a/deploy/pruefen.sh b/deploy/pruefen.sh index 6ed4df1..50f6493 100644 --- a/deploy/pruefen.sh +++ b/deploy/pruefen.sh @@ -20,7 +20,7 @@ fehler=0 schritt() { echo "── $1"; } schritt "Syntax der Shell-Skripte" -for f in deploy/*.sh deploy/jobs/*.sh; do +for f in deploy/*.sh deploy/jobs/*.sh deploy/homelab/*.sh; do bash -n "$f" || { echo " ✗ $f"; fehler=1; } done diff --git a/docs/ARCHITEKTUR.md b/docs/ARCHITEKTUR.md index faf5349..f4fa475 100644 --- a/docs/ARCHITEKTUR.md +++ b/docs/ARCHITEKTUR.md @@ -207,12 +207,45 @@ Routen. `Origin` (Skripte, `curl`), `Origin: null` bzw. `file://` (Lucy-Desktop), dieselbe Adresse wie die Oberfläche und die Entwicklungs-Ports 5173, 5180, 5181. `/v1` ist nicht betroffen. -## Ausblick: der Homelab-Teil (Phasen 3 und 4) +## Der Homelab-Teil (Phasen 3 und 4, Code seit 24.09.; Einrichtung wartet auf das User-OK) -Stand der Recherche vom 24.09.2026: Community-Script-Container aktualisiert man im Container mit `update`, ohne -Rückfragen mit `PHS_SILENT=1`; die installierte Version steht in `/root/.`. Die Proxmox-API kann keine Befehle -in Containern ausführen und gibt die Liste der Host-Updates nur mit Schreibrecht heraus. Deshalb: ein Lese-Schlüssel, -ein Proxmox-Webhook für Host-Pakete und ein kleiner Ausführer auf dem Host mit festen Aktionen. Container mit -Bind-Mount (z. B. PBS) lassen sich nicht snapshotten, dort gilt „Backup statt Snapshot". Arcane meldet Image-Updates -über eine eigene API mit Schlüssel. Ein Exit-Code 0 beweist kein gelungenes Update; geprüft wird unabhängig -(HTTP-Probe, Dienststatus, Version). Fahrplan: [wissen/OFFENE-FAEDEN.md](wissen/OFFENE-FAEDEN.md). +Derselbe Code in der Rolle `homelab`, in einem eigenen Container auf dem Proxmox-PC (`deploy/homelab/`). +Er braucht **keinen Proxmox-Schlüssel**: Alles, was vom Host kommt, liefert der Ausführer. + +```mermaid +flowchart LR + AF["Ausführer
Proxmox-Host, root"] -->|holt Aufträge, Bericht alle 10 min| HL["Homelab-Teil
Container, :9001"] + HL -->|Ziele, Läufe| UI["Oberfläche
Seite Homelab"] + BOX["KI-Box :9001"] <-->|prüfen sich, /api/partner| HL + HL -->|Webprüfung| G["Gäste
AdGuard, Gitea …"] +``` + +- **Ausführer** `deploy/homelab/ausfuehrer.py` (root auf dem Host, nur Standardbibliothek, `mc2-ausfuehrer.service`): + holt sich Arbeit beim Homelab-Teil ab („Pull“, kein offener Port auf dem Host) und weist sich mit einem + gemeinsamen Geheimnis aus (Kopfzeile `X-MC2-Ausfuehrer`; erzeugt der Homelab-Teil in + `/var/lib/mc2/ausfuehrer.token`, liegt auf dem Host in `/etc/mc2-ausfuehrer.json`, beides 0600). Er führt nur eine + feste Liste von Aktionen aus (`bericht`, `snapshot`, `update`, `os_update`, `suchen`, `zurueck`, + `snapshot_loeschen`, `host_update`, `host_neustart`) und prüft selbst, ob ein Gast das Etikett + `community-script` oder `watcher` trägt und nicht `watcher-aus` — dem Server vertraut er dabei nicht. +- **Bericht** (nur lesend, auch von Hand: `python3 ausfuehrer.py --bericht`): Host-Version und Paket-Updates, je + Gast Status, IP, Etiketten, Snapshots, ob ein Snapshot geht (Bind-Mounts wie beim PBS verhindern ihn), die + Community-Script-Kennung (aus `/usr/bin/update` im Gast), die App-Version (je App eigener Weg: `/root/.`, + `AdGuardHome --version`, `netbird version`, `dpkg-query`, Docker-Image-Datum) und die Paket-Updates im Gast samt + Alter der Paketlisten. +- **Homelab-Teil** `backend/services/homelab/`: `kanal.py` (Geheimnis, Auftragsliste, Bericht), `apps.py` (App- + Katalog: Name, GitHub-Quelle, Weboberfläche), `inventar.py` (Bericht + neueste Versionen von GitHub + + eigene Webprüfung → Ziele im gemeinsamen Modell; Paketlisten älter als 14 Tage = „unklar“ mit Knopf „Nach Updates + suchen“), `updates.py` („Jetzt updaten“). Schnittstellen `routers/homelab.py` unter `/api/homelab/…`. +- **„Jetzt updaten“** (nur per Knopf): Snapshot (wo möglich) → `update` des Community-Scripts (`PHS_SILENT=1`) bzw. + Pakete → 20 s warten → frischer Bericht → Prüfung (Gast läuft, Weboberfläche antwortet, App-Version neu). Rot + und Snapshot da → automatisch zurück + dringende Meldung; grün → ältere `mc2-`-Snapshots weg + Meldung. Läufe in + `/var/lib/mc2/homelab-laeufe.json` und im strukturierten Update-Verlauf. Host: Pakete per Knopf mit Warnung, + Neustart als eigener Knopf. +- **Wächter** in der Rolle `homelab`: Platte, Partner (die KI-Box), Ausführer (kein Bericht seit 30 min = rot) und + jede Weboberfläche der freigegebenen Gäste. +- **Oberfläche**: Die Seite „Homelab“ zeigt alle Geräte als Karten — die KI-Box (`/api/ziele`) und alles aus + `/api/homelab/ziele` — mit Stand je Baustein, Rückweg und Knopf samt Rückfrage. +- **Meldungen** ohne Hermes: `notify.sh` im Container nimmt den Zweitweg direkt an die Bot-API + (`/etc/mc2/telegram.env`), nachts sammelt eine eigene Morgenmeldung („[Morgenmeldung Homelab]“). +- **Einrichtung** (Reihenfolge, jeder Schritt braucht das User-OK): `container-anlegen.sh` → `ausrollen.sh ` → + `ausfuehrer-einrichten.sh ` → `box-partner.sh `; Details in [BETRIEB.md](BETRIEB.md). diff --git a/docs/BETRIEB.md b/docs/BETRIEB.md index b5e7291..e7c8d33 100644 --- a/docs/BETRIEB.md +++ b/docs/BETRIEB.md @@ -147,6 +147,25 @@ Neustarts von llama-swap und Hermes. als Abzug nach `/tmp` auf der Box und lässt dort das Prüftor mit dem Python des Box-Checkouts laufen. Ändert auf der Box sonst nichts. Grund: Unter Linux zeigen sich Wettläufe, die am Windows-PC durchgehen (24.09.). +## Homelab-Teil einrichten (Phase 3/4) + +Jeder Schritt braucht das OK des Users (Container anlegen, Ausführer als root auf dem Host, Telegram-Zugang in +den Container). Alles läuft am PC in Git-Bash, im Repo; SSH-Zugang zu `pve` (Schlüssel `id_lucy_infra`). + +1. `bash deploy/homelab/container-anlegen.sh` — unprivilegierter Debian-13-Container (nächste freie ID, 1 Kern, + 1 GB RAM, 4 GB, DHCP, Autostart, Etikett `mc2`), SSH-Schlüssel wie bei allen Gästen. Gibt die IP aus. +2. `bash deploy/homelab/ausrollen.sh ` — Code von HEAD in `/opt/mc2`, `einrichten.sh` (Nutzer `mc2`, Python- + Umgebung, Units `mc2-homelab`, `mc2-homelab-steward`, `mc2-homelab-morgenmeldung.timer`). Auch jedes spätere + Update des Homelab-Teils geht so (erst Prüftor und Probelauf auf der Box). +3. Telegram-Zugang (nur mit OK): `TELEGRAM_BOT_TOKEN` und `TELEGRAM_HOME_CHANNEL` in `/etc/mc2/telegram.env` + (Eigentümer `root:mc2`, 0640). +4. `bash deploy/homelab/ausfuehrer-einrichten.sh ` — Ausführer, Unit und Konfiguration (0600) auf den Host. +5. `bash deploy/homelab/box-partner.sh ` — Drop-ins `partner.conf` für `mission-control-2` und `mc2-steward` + auf der Box; ab dann prüfen sich beide Instanzen gegenseitig. + +Prüfen: `curl http://:9001/api/homelab/ziele` (nach etwa einer Minute stehen die Geräte darin), +`systemctl status mc2-ausfuehrer` auf dem Host, Seite „Homelab“ der Oberfläche. + ## Sicherung - **Wann:** `mc2-backup.timer` täglich 03:30 (+≤5 min); außerdem vor jedem Hermes-Update, vor jedem Zurückspielen und diff --git a/frontend/dist/assets/Auftraege-C3JDjG9Z.js b/frontend/dist/assets/Auftraege-C3JDjG9Z.js new file mode 100644 index 0000000..47ce7dc --- /dev/null +++ b/frontend/dist/assets/Auftraege-C3JDjG9Z.js @@ -0,0 +1,2 @@ +import{c as e,i as t,p as n,s as r,t as i,u as a}from"./button-BheHBEml.js";import{L as o,N as s,h as c,j as l,m as u,n as d,t as f}from"./index-SRwQwEnh.js";import{t as p}from"./Bestaetigen-CE_3b0PK.js";var m=r();function h(e,t){if(e===`os`)return{laeuft:`Ubuntu`,neu:t.count?`${t.count} ${t.count===1?`Paket`:`Pakete`}`:null};if(e===`hermes`)return{laeuft:t.installed_version?`v${t.installed_version}`:`–`,neu:t.behind?`+${t.behind.toLocaleString(`de-DE`)} Änderungen`:null};let n=e===`engine`?`b`:`v`;return{laeuft:t.installed_build==null?`–`:`${n}${t.installed_build}`,neu:t.latest_build!=null&&t.installed_build!=null&&t.latest_build>t.installed_build?`${n}${t.latest_build}`:null}}function g(e,t){return!t||t.error?null:e===`os`?typeof t.count==`number`?t.count>0:null:e===`hermes`?typeof t.behind==`number`?t.behind>0:null:t.latest_build==null||t.installed_build==null?null:t.latest_build>t.installed_build}function _(e){let t=e.trim();return/timed out|timeout|zeitüberschreitung/i.test(t)?`Konnte nicht prüfen: Die Gegenstelle hat nicht rechtzeitig geantwortet. Später klappt es meist wieder.`:/name resolution|name or service not known|network is unreachable|connection (refused|reset)|nicht erreichbar/i.test(t)?`Konnte nicht prüfen: Die Box kommt gerade nicht ins Internet.`:`Konnte nicht prüfen: ${t.length>160?`${t.slice(0,157)}…`:t}`}var v=e=>e.group===`download`||/^download\s/i.test(e.label),y=e=>e.group===`pruefen`||e.label===`Nach Updates suchen`,b=e=>o(e)&&v(e),x=[[/^download\s+(.+)$/i,`Download $1`],[/^OS-Update \(apt\)$/,`Update Betriebssystem`],[/^Engine-Update \(llama\.cpp Vulkan\)$/,`Update Motor (llama.cpp)`],[/^Router-Update \(llama-swap\)$/,`Update llama-swap`],[/^Hermes-Agent-Update$/,`Update Hermes`]],S=[[`Engine (llama.cpp)`,`Motor`],[`Router (llama-swap)`,`llama-swap`],[`Hermes-Agent`,`Hermes`],[`OS (apt)`,`Betriebssystem`]];function C(e){for(let[t,n]of x)if(t.test(e.label))return e.label.replace(t,n);return e.label.startsWith(`Alle aktualisieren`)?S.reduce((e,[t,n])=>e.split(t).join(n),e.label):e.label}function w(e){if(e<60)return`noch unter einer Minute`;let t=Math.round(e/60);if(t<60)return`noch etwa ${t} min`;let n=Math.floor(t/60),r=t%60;return r?`noch etwa ${n} h ${r} min`:`noch etwa ${n} h`}function T(e,t=Date.now()/1e3){let n=Math.max(0,t-e.started_at),r=Math.floor(n/60),i=n<60?`ein paar Sekunden`:r<60?`${r} min`:`${Math.floor(r/60)} h ${r%60} min`;return`${e.state===`queued`?`Wartet`:`Läuft`} seit ${i}`}function E(e){if(e.progress==null)return null;let t=[`${Math.round(e.progress)} %`];return e.total_bytes&&t.push(`${s(e.done_bytes??0,1)} von ${s(e.total_bytes,1)}`),e.eta_s!=null&&o(e)&&t.push(w(e.eta_s)),t.join(` · `)}function D(e){for(let t=(e.log?.length??0)-1;t>=0;t--){let n=e.log[t].trim();if(n&&!n.startsWith(`$ `))return n}return null}function O(e,t=Date.now()/1e3){let n=e.filter(o).sort((e,t)=>e.started_at-t.started_at),r=e.filter(e=>!o(e)&&t-(e.finished_at??e.started_at)<86400).sort((e,t)=>(t.finished_at??t.started_at)-(e.finished_at??e.started_at)).slice(0,3);return[...n,...r]}function k(e,t,n){if(!n)return`Nach dem Download übernimmt ${e} die Rolle „${t}“.`;let r=n.weitere.length?` Die Rolle${n.weitere.length===1?``:`n`} „${n.weitere.join(`“, „`)}“ behält es.`:``;return`Achtung: Nach dem Download übernimmt ${e} die Rolle „${t}“ und ersetzt damit ${n.name}. Das bisherige Modell bleibt auf der Platte.${r}`+(n.zwilling?` Bilder liest vorerst weiter sein Bild-Zwilling, also noch das bisherige Modell.`:``)}function A(e){return`Kurze Unterbrechung: Beim Eintragen lädt llama-swap sofort neu. Was Lucy oder OpenChamber gerade schreiben, bricht ab, und die Modelle werden neu geladen.`+(e?` Mit Rolle passiert das nach dem Download noch einmal.`:``)}var j=n(a(),1),M=e(),N={queued:{text:`Wartet`,art:`cyan`},running:{text:`Läuft`,art:`cyan`},done:{text:`Fertig`,art:`gruen`},failed:{text:`Fehler`,art:`rot`},canceled:{text:`Abgebrochen`,art:`grau`}},P={queued:`bg-cyan`,running:`bg-cyan`,done:`bg-gruen`,failed:`bg-rot`,canceled:`bg-text-3`},F=e=>l(new Date(e*1e3).toISOString());function I(e){let t=(0,m.c)(3),[n,r]=(0,j.useState)(L),i,a;return t[0]===e?(i=t[1],a=t[2]):(i=()=>{if(!e)return;let t=window.setInterval(()=>r(Date.now()/1e3),3e4);return()=>window.clearInterval(t)},a=[e],t[0]=e,t[1]=i,t[2]=a),(0,j.useEffect)(i,a),n}function L(){return Date.now()/1e3}function R(e){let n=(0,m.c)(75),{job:r,onAbbrechen:a,abbrechenGesperrt:s,ebene:c}=e,l=c===void 0?4:c,[u,f]=(0,j.useState)(!1),[h,g]=(0,j.useState)(!1),_;n[0]===r.state?_=n[1]:(_=N[r.state]??{text:r.state,art:`grau`},n[0]=r.state,n[1]=_);let y=_,x;n[2]===r?x=n[3]:(x=C(r),n[2]=r,n[3]=x);let S=x,w;n[4]===r?w=n[5]:(w=E(r),n[4]=r,n[5]=w);let O=w,k;n[6]===r?k=n[7]:(k=D(r),n[6]=r,n[7]=k);let A=k,L;n[8]===r?L=n[9]:(L=o(r),n[8]=r,n[9]=L);let R=L,z=I(R),B;n[10]!==r||n[11]!==a?(B=!!a&&b(r),n[10]=r,n[11]=a,n[12]=B):B=n[12];let V=B,H,U,W,G,K,q,J,Y;if(n[13]!==s||n[14]!==V||n[15]!==l||n[16]!==O||n[17]!==z||n[18]!==r||n[19]!==R||n[20]!==A||n[21]!==h||n[22]!==S||n[23]!==y.art||n[24]!==y.text){let e=(r.log??[]).slice(-12),a=l===3?`h3`:`h4`;J=`Auftrag: ${S}`,Y=`flex flex-col gap-2.5 rounded-xl border border-linie bg-erhaben/50 p-3.5`;let o;n[33]!==a||n[34]!==S?(o=(0,M.jsx)(a,{className:`m-0 min-w-0 text-[15px] leading-snug font-semibold break-words`,children:S}),n[33]=a,n[34]=S,n[35]=o):o=n[35];let c;n[36]!==y.art||n[37]!==y.text?(c=(0,M.jsx)(d,{art:y.art,children:y.text}),n[36]=y.art,n[37]=y.text,n[38]=c):c=n[38],n[39]!==o||n[40]!==c?(H=(0,M.jsxs)(`div`,{className:`flex items-start justify-between gap-3`,children:[o,c]}),n[39]=o,n[40]=c,n[41]=H):H=n[41],n[42]!==r.progress||n[43]!==r.state||n[44]!==S?(U=r.progress!=null&&(0,M.jsx)(`div`,{role:`progressbar`,"aria-label":`Fortschritt: ${S}`,"aria-valuemin":0,"aria-valuemax":100,"aria-valuenow":Math.round(r.progress),className:`h-2 overflow-hidden rounded-full bg-linie`,children:(0,M.jsx)(`div`,{className:t(`h-full`,P[r.state]??`bg-cyan`),style:{width:`${Math.max(2,Math.min(100,r.progress))}%`}})}),n[42]=r.progress,n[43]=r.state,n[44]=S,n[45]=U):U=n[45];let u;n[46]!==O||n[47]!==z||n[48]!==r||n[49]!==R||n[50]!==y.text?(u=R?O??T(r,z):`${y.text} · ${F(r.finished_at??r.started_at)}`,n[46]=O,n[47]=z,n[48]=r,n[49]=R,n[50]=y.text,n[51]=u):u=n[51],n[52]===u?W=n[53]:(W=(0,M.jsx)(`p`,{className:`ziffern m-0 text-sm text-text-2`,children:u}),n[52]=u,n[53]=W),n[54]!==r.progress||n[55]!==r.state||n[56]!==R||n[57]!==A?(G=A&&(r.state===`failed`||R&&r.progress==null)&&(0,M.jsx)(`p`,{className:r.state===`failed`?`m-0 text-sm break-words text-rot-text`:`ziffern m-0 line-clamp-2 text-xs break-all text-text-3`,children:r.state===`failed`?`Letzte Meldung: ${A}`:A}),n[54]=r.progress,n[55]=r.state,n[56]=R,n[57]=A,n[58]=G):G=n[58],K=(V||e.length>1)&&(0,M.jsxs)(`div`,{className:`flex flex-wrap gap-2`,children:[V&&(0,M.jsx)(i,{variant:`outline`,size:`sm`,disabled:s,onClick:()=>f(!0),children:`Abbrechen`}),e.length>1&&(0,M.jsx)(i,{variant:`ghost`,size:`sm`,"aria-expanded":h,onClick:()=>g(!h),children:h?`Protokoll zu`:`Protokoll`})]}),q=h&&(0,M.jsx)(`pre`,{className:`ziffern m-0 max-h-56 overflow-auto rounded-md bg-[#0b0d0f] p-2 text-xs whitespace-pre-wrap break-all text-text-2`,children:e.join(` +`)}),n[13]=s,n[14]=V,n[15]=l,n[16]=O,n[17]=z,n[18]=r,n[19]=R,n[20]=A,n[21]=h,n[22]=S,n[23]=y.art,n[24]=y.text,n[25]=H,n[26]=U,n[27]=W,n[28]=G,n[29]=K,n[30]=q,n[31]=J,n[32]=Y}else H=n[25],U=n[26],W=n[27],G=n[28],K=n[29],q=n[30],J=n[31],Y=n[32];let X;n[59]!==V||n[60]!==u||n[61]!==r||n[62]!==a||n[63]!==S?(X=V&&(0,M.jsx)(p,{offen:u,gefahr:!0,titel:`Auftrag abbrechen?`,text:v(r)?`Der ${S} stoppt. Du kannst ihn später über „Modelle selbst suchen“ neu starten.`:`„${S}“ wird abgebrochen.`,knopf:`Ja, abbrechen`,neinKnopf:`Weiterlaufen lassen`,onNein:()=>f(!1),onJa:()=>{u&&(f(!1),a?.(r))}}),n[59]=V,n[60]=u,n[61]=r,n[62]=a,n[63]=S,n[64]=X):X=n[64];let Z;return n[65]!==H||n[66]!==U||n[67]!==W||n[68]!==G||n[69]!==K||n[70]!==q||n[71]!==X||n[72]!==J||n[73]!==Y?(Z=(0,M.jsxs)(`article`,{"aria-label":J,className:Y,children:[H,U,W,G,K,q,X]}),n[65]=H,n[66]=U,n[67]=W,n[68]=G,n[69]=K,n[70]=q,n[71]=X,n[72]=J,n[73]=Y,n[74]=Z):Z=n[74],Z}function z(e){let t=(0,m.c)(17),{alsPanel:n}=e,r=n!==void 0&&n,i=c(),a=u(),o,s;if(t[0]!==a||t[1]!==r||t[2]!==i.data||t[3]!==i.error||t[4]!==i.isError){s=Symbol.for(`react.early_return_sentinel`);bb0:{let e=O(i.data?.jobs??[]);if(e.length>0){let n;t[7]!==a||t[8]!==r?(n=e=>(0,M.jsx)(R,{job:e,ebene:r?3:4,abbrechenGesperrt:a.isPending,onAbbrechen:e=>a.mutate({id:e.id,titel:C(e)})},e.id),t[7]=a,t[8]=r,t[9]=n):n=t[9],o=(0,M.jsx)(`div`,{className:`grid items-start gap-2.5 lg:grid-cols-2`,children:e.map(n)})}else if(i.isError&&!i.data){let e;t[10]===i.error.message?e=t[11]:(e=(0,M.jsxs)(`p`,{role:`alert`,className:`text-sm text-rot-text`,children:[`Die Aufträge lassen sich gerade nicht lesen: `,i.error.message]}),t[10]=i.error.message,t[11]=e),o=e}else{s=null;break bb0}}t[0]=a,t[1]=r,t[2]=i.data,t[3]=i.error,t[4]=i.isError,t[5]=o,t[6]=s}else o=t[5],s=t[6];if(s!==Symbol.for(`react.early_return_sentinel`))return s;if(r){let e;return t[12]===o?e=t[13]:(e=(0,M.jsx)(f,{titel:`Aufträge`,children:o}),t[12]=o,t[13]=e),e}let l;t[14]===Symbol.for(`react.memo_cache_sentinel`)?(l=(0,M.jsx)(`h3`,{className:`schild m-0 text-sm text-text-3`,children:`Aufträge`}),t[14]=l):l=t[14];let d;return t[15]===o?d=t[16]:(d=(0,M.jsxs)(`section`,{"aria-label":`Aufträge`,className:`flex flex-col gap-2.5`,children:[l,o]}),t[15]=o,t[16]=d),d}export{_ as a,y as i,g as n,k as o,A as r,h as s,z as t}; \ No newline at end of file diff --git a/frontend/dist/assets/Auftraege-C5bKfsD1.js b/frontend/dist/assets/Auftraege-C5bKfsD1.js deleted file mode 100644 index 7d3ce20..0000000 --- a/frontend/dist/assets/Auftraege-C5bKfsD1.js +++ /dev/null @@ -1,2 +0,0 @@ -import{i as e,n as t,o as n,r,t as i}from"./dist-BaE5-hhJ.js";import{D as a,N as o,d as s,f as c,k as l,n as u,t as d,z as f}from"./index-DSxwfDVg.js";import{Bestaetigen as p}from"./Bestaetigen-kFure1oW.js";var m=t();function h(e,t){if(e===`os`)return{laeuft:`Ubuntu`,neu:t.count?`${t.count} ${t.count===1?`Paket`:`Pakete`}`:null};if(e===`hermes`)return{laeuft:t.installed_version?`v${t.installed_version}`:`–`,neu:t.behind?`+${t.behind.toLocaleString(`de-DE`)} Änderungen`:null};let n=e===`engine`?`b`:`v`;return{laeuft:t.installed_build==null?`–`:`${n}${t.installed_build}`,neu:t.latest_build!=null&&t.installed_build!=null&&t.latest_build>t.installed_build?`${n}${t.latest_build}`:null}}function g(e,t){return!t||t.error?null:e===`os`?typeof t.count==`number`?t.count>0:null:e===`hermes`?typeof t.behind==`number`?t.behind>0:null:t.latest_build==null||t.installed_build==null?null:t.latest_build>t.installed_build}function _(e){let t=e.trim();return/timed out|timeout|zeitüberschreitung/i.test(t)?`Konnte nicht prüfen: Die Gegenstelle hat nicht rechtzeitig geantwortet. Später klappt es meist wieder.`:/name resolution|name or service not known|network is unreachable|connection (refused|reset)|nicht erreichbar/i.test(t)?`Konnte nicht prüfen: Die Box kommt gerade nicht ins Internet.`:`Konnte nicht prüfen: ${t.length>160?`${t.slice(0,157)}…`:t}`}var v=e=>e.group===`download`||/^download\s/i.test(e.label),y=e=>e.group===`pruefen`||e.label===`Nach Updates suchen`,b=e=>o(e)&&v(e),x=[[/^download\s+(.+)$/i,`Download $1`],[/^OS-Update \(apt\)$/,`Update Betriebssystem`],[/^Engine-Update \(llama\.cpp Vulkan\)$/,`Update Motor (llama.cpp)`],[/^Router-Update \(llama-swap\)$/,`Update llama-swap`],[/^Hermes-Agent-Update$/,`Update Hermes`]],S=[[`Engine (llama.cpp)`,`Motor`],[`Router (llama-swap)`,`llama-swap`],[`Hermes-Agent`,`Hermes`],[`OS (apt)`,`Betriebssystem`]];function C(e){for(let[t,n]of x)if(t.test(e.label))return e.label.replace(t,n);return e.label.startsWith(`Alle aktualisieren`)?S.reduce((e,[t,n])=>e.split(t).join(n),e.label):e.label}function w(e){if(e<60)return`noch unter einer Minute`;let t=Math.round(e/60);if(t<60)return`noch etwa ${t} min`;let n=Math.floor(t/60),r=t%60;return r?`noch etwa ${n} h ${r} min`:`noch etwa ${n} h`}function T(e,t=Date.now()/1e3){let n=Math.max(0,t-e.started_at),r=Math.floor(n/60),i=n<60?`ein paar Sekunden`:r<60?`${r} min`:`${Math.floor(r/60)} h ${r%60} min`;return`${e.state===`queued`?`Wartet`:`Läuft`} seit ${i}`}function E(e){if(e.progress==null)return null;let t=[`${Math.round(e.progress)} %`];return e.total_bytes&&t.push(`${l(e.done_bytes??0,1)} von ${l(e.total_bytes,1)}`),e.eta_s!=null&&o(e)&&t.push(w(e.eta_s)),t.join(` · `)}function D(e){for(let t=(e.log?.length??0)-1;t>=0;t--){let n=e.log[t].trim();if(n&&!n.startsWith(`$ `))return n}return null}function O(e,t=Date.now()/1e3){let n=e.filter(o).sort((e,t)=>e.started_at-t.started_at),r=e.filter(e=>!o(e)&&t-(e.finished_at??e.started_at)<86400).sort((e,t)=>(t.finished_at??t.started_at)-(e.finished_at??e.started_at)).slice(0,3);return[...n,...r]}function k(e,t,n){if(!n)return`Nach dem Download übernimmt ${e} die Rolle „${t}“.`;let r=n.weitere.length?` Die Rolle${n.weitere.length===1?``:`n`} „${n.weitere.join(`“, „`)}“ behält es.`:``;return`Achtung: Nach dem Download übernimmt ${e} die Rolle „${t}“ und ersetzt damit ${n.name}. Das bisherige Modell bleibt auf der Platte.${r}`+(n.zwilling?` Bilder liest vorerst weiter sein Bild-Zwilling, also noch das bisherige Modell.`:``)}function A(e){return`Kurze Unterbrechung: Beim Eintragen lädt llama-swap sofort neu. Was Lucy oder OpenChamber gerade schreiben, bricht ab, und die Modelle werden neu geladen.`+(e?` Mit Rolle passiert das nach dem Download noch einmal.`:``)}var j=n(e(),1),M=r(),N={queued:{text:`Wartet`,art:`cyan`},running:{text:`Läuft`,art:`cyan`},done:{text:`Fertig`,art:`gruen`},failed:{text:`Fehler`,art:`rot`},canceled:{text:`Abgebrochen`,art:`grau`}},P={queued:`bg-cyan`,running:`bg-cyan`,done:`bg-gruen`,failed:`bg-rot`,canceled:`bg-text-3`},F=e=>a(new Date(e*1e3).toISOString());function I(e){let t=(0,m.c)(3),[n,r]=(0,j.useState)(L),i,a;return t[0]===e?(i=t[1],a=t[2]):(i=()=>{if(!e)return;let t=window.setInterval(()=>r(Date.now()/1e3),3e4);return()=>window.clearInterval(t)},a=[e],t[0]=e,t[1]=i,t[2]=a),(0,j.useEffect)(i,a),n}function L(){return Date.now()/1e3}function R(e){let t=(0,m.c)(75),{job:n,onAbbrechen:r,abbrechenGesperrt:a,ebene:s}=e,c=s===void 0?4:s,[l,d]=(0,j.useState)(!1),[h,g]=(0,j.useState)(!1),_;t[0]===n.state?_=t[1]:(_=N[n.state]??{text:n.state,art:`grau`},t[0]=n.state,t[1]=_);let y=_,x;t[2]===n?x=t[3]:(x=C(n),t[2]=n,t[3]=x);let S=x,w;t[4]===n?w=t[5]:(w=E(n),t[4]=n,t[5]=w);let O=w,k;t[6]===n?k=t[7]:(k=D(n),t[6]=n,t[7]=k);let A=k,L;t[8]===n?L=t[9]:(L=o(n),t[8]=n,t[9]=L);let R=L,z=I(R),B;t[10]!==n||t[11]!==r?(B=!!r&&b(n),t[10]=n,t[11]=r,t[12]=B):B=t[12];let V=B,H,U,W,G,K,q,J,Y;if(t[13]!==a||t[14]!==V||t[15]!==c||t[16]!==O||t[17]!==z||t[18]!==n||t[19]!==R||t[20]!==A||t[21]!==h||t[22]!==S||t[23]!==y.art||t[24]!==y.text){let e=(n.log??[]).slice(-12),r=c===3?`h3`:`h4`;J=`Auftrag: ${S}`,Y=`flex flex-col gap-2.5 rounded-xl border border-linie bg-erhaben/50 p-3.5`;let o;t[33]!==r||t[34]!==S?(o=(0,M.jsx)(r,{className:`m-0 min-w-0 text-[15px] leading-snug font-semibold break-words`,children:S}),t[33]=r,t[34]=S,t[35]=o):o=t[35];let s;t[36]!==y.art||t[37]!==y.text?(s=(0,M.jsx)(u,{art:y.art,children:y.text}),t[36]=y.art,t[37]=y.text,t[38]=s):s=t[38],t[39]!==o||t[40]!==s?(H=(0,M.jsxs)(`div`,{className:`flex items-start justify-between gap-3`,children:[o,s]}),t[39]=o,t[40]=s,t[41]=H):H=t[41],t[42]!==n.progress||t[43]!==n.state||t[44]!==S?(U=n.progress!=null&&(0,M.jsx)(`div`,{role:`progressbar`,"aria-label":`Fortschritt: ${S}`,"aria-valuemin":0,"aria-valuemax":100,"aria-valuenow":Math.round(n.progress),className:`h-2 overflow-hidden rounded-full bg-linie`,children:(0,M.jsx)(`div`,{className:i(`h-full`,P[n.state]??`bg-cyan`),style:{width:`${Math.max(2,Math.min(100,n.progress))}%`}})}),t[42]=n.progress,t[43]=n.state,t[44]=S,t[45]=U):U=t[45];let l;t[46]!==O||t[47]!==z||t[48]!==n||t[49]!==R||t[50]!==y.text?(l=R?O??T(n,z):`${y.text} · ${F(n.finished_at??n.started_at)}`,t[46]=O,t[47]=z,t[48]=n,t[49]=R,t[50]=y.text,t[51]=l):l=t[51],t[52]===l?W=t[53]:(W=(0,M.jsx)(`p`,{className:`ziffern m-0 text-sm text-text-2`,children:l}),t[52]=l,t[53]=W),t[54]!==n.progress||t[55]!==n.state||t[56]!==R||t[57]!==A?(G=A&&(n.state===`failed`||R&&n.progress==null)&&(0,M.jsx)(`p`,{className:n.state===`failed`?`m-0 text-sm break-words text-rot-text`:`ziffern m-0 line-clamp-2 text-xs break-all text-text-3`,children:n.state===`failed`?`Letzte Meldung: ${A}`:A}),t[54]=n.progress,t[55]=n.state,t[56]=R,t[57]=A,t[58]=G):G=t[58],K=(V||e.length>1)&&(0,M.jsxs)(`div`,{className:`flex flex-wrap gap-2`,children:[V&&(0,M.jsx)(f,{variant:`outline`,size:`sm`,disabled:a,onClick:()=>d(!0),children:`Abbrechen`}),e.length>1&&(0,M.jsx)(f,{variant:`ghost`,size:`sm`,"aria-expanded":h,onClick:()=>g(!h),children:h?`Protokoll zu`:`Protokoll`})]}),q=h&&(0,M.jsx)(`pre`,{className:`ziffern m-0 max-h-56 overflow-auto rounded-md bg-[#0b0d0f] p-2 text-xs whitespace-pre-wrap break-all text-text-2`,children:e.join(` -`)}),t[13]=a,t[14]=V,t[15]=c,t[16]=O,t[17]=z,t[18]=n,t[19]=R,t[20]=A,t[21]=h,t[22]=S,t[23]=y.art,t[24]=y.text,t[25]=H,t[26]=U,t[27]=W,t[28]=G,t[29]=K,t[30]=q,t[31]=J,t[32]=Y}else H=t[25],U=t[26],W=t[27],G=t[28],K=t[29],q=t[30],J=t[31],Y=t[32];let X;t[59]!==V||t[60]!==l||t[61]!==n||t[62]!==r||t[63]!==S?(X=V&&(0,M.jsx)(p,{offen:l,gefahr:!0,titel:`Auftrag abbrechen?`,text:v(n)?`Der ${S} stoppt. Du kannst ihn später über „Modelle selbst suchen“ neu starten.`:`„${S}“ wird abgebrochen.`,knopf:`Ja, abbrechen`,neinKnopf:`Weiterlaufen lassen`,onNein:()=>d(!1),onJa:()=>{l&&(d(!1),r?.(n))}}),t[59]=V,t[60]=l,t[61]=n,t[62]=r,t[63]=S,t[64]=X):X=t[64];let Z;return t[65]!==H||t[66]!==U||t[67]!==W||t[68]!==G||t[69]!==K||t[70]!==q||t[71]!==X||t[72]!==J||t[73]!==Y?(Z=(0,M.jsxs)(`article`,{"aria-label":J,className:Y,children:[H,U,W,G,K,q,X]}),t[65]=H,t[66]=U,t[67]=W,t[68]=G,t[69]=K,t[70]=q,t[71]=X,t[72]=J,t[73]=Y,t[74]=Z):Z=t[74],Z}function z(e){let t=(0,m.c)(17),{alsPanel:n}=e,r=n!==void 0&&n,i=c(),a=s(),o,l;if(t[0]!==a||t[1]!==r||t[2]!==i.data||t[3]!==i.error||t[4]!==i.isError){l=Symbol.for(`react.early_return_sentinel`);bb0:{let e=O(i.data?.jobs??[]);if(e.length>0){let n;t[7]!==a||t[8]!==r?(n=e=>(0,M.jsx)(R,{job:e,ebene:r?3:4,abbrechenGesperrt:a.isPending,onAbbrechen:e=>a.mutate({id:e.id,titel:C(e)})},e.id),t[7]=a,t[8]=r,t[9]=n):n=t[9],o=(0,M.jsx)(`div`,{className:`grid items-start gap-2.5 lg:grid-cols-2`,children:e.map(n)})}else if(i.isError&&!i.data){let e;t[10]===i.error.message?e=t[11]:(e=(0,M.jsxs)(`p`,{role:`alert`,className:`text-sm text-rot-text`,children:[`Die Aufträge lassen sich gerade nicht lesen: `,i.error.message]}),t[10]=i.error.message,t[11]=e),o=e}else{l=null;break bb0}}t[0]=a,t[1]=r,t[2]=i.data,t[3]=i.error,t[4]=i.isError,t[5]=o,t[6]=l}else o=t[5],l=t[6];if(l!==Symbol.for(`react.early_return_sentinel`))return l;if(r){let e;return t[12]===o?e=t[13]:(e=(0,M.jsx)(d,{titel:`Aufträge`,children:o}),t[12]=o,t[13]=e),e}let u;t[14]===Symbol.for(`react.memo_cache_sentinel`)?(u=(0,M.jsx)(`h3`,{className:`schild m-0 text-sm text-text-3`,children:`Aufträge`}),t[14]=u):u=t[14];let f;return t[15]===o?f=t[16]:(f=(0,M.jsxs)(`section`,{"aria-label":`Aufträge`,className:`flex flex-col gap-2.5`,children:[u,o]}),t[15]=o,t[16]=f),f}export{_ as a,y as i,g as n,k as o,A as r,h as s,z as t}; \ No newline at end of file diff --git a/frontend/dist/assets/Bestaetigen-CE_3b0PK.js b/frontend/dist/assets/Bestaetigen-CE_3b0PK.js new file mode 100644 index 0000000..e35ce3f --- /dev/null +++ b/frontend/dist/assets/Bestaetigen-CE_3b0PK.js @@ -0,0 +1 @@ +import{a as e,c as t,f as n,i as r,s as i,t as a,u as o}from"./button-BheHBEml.js";import{a as s,i as c,n as l,o as u,r as d,s as f,t as p}from"./dist-CtXmxyGa.js";var m=i();o();var h=t();function g(e){let t=(0,m.c)(4),n;t[0]===e?n=t[1]:({...n}=e,t[0]=e,t[1]=n);let r;return t[2]===n?r=t[3]:(r=(0,h.jsx)(p,{"data-slot":`dialog`,...n}),t[2]=n,t[3]=r),r}function _(e){let t=(0,m.c)(4),n;t[0]===e?n=t[1]:({...n}=e,t[0]=e,t[1]=n);let r;return t[2]===n?r=t[3]:(r=(0,h.jsx)(u,{"data-slot":`dialog-portal`,...n}),t[2]=n,t[3]=r),r}function v(e){let t=(0,m.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0`,n),t[3]=n,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,h.jsx)(s,{"data-slot":`dialog-overlay`,className:a,...i}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}function y(t){let n=(0,m.c)(15),i,o,s,c;n[0]===t?(i=n[1],o=n[2],s=n[3],c=n[4]):({className:o,children:i,showCloseButton:c,...s}=t,n[0]=t,n[1]=i,n[2]=o,n[3]=s,n[4]=c);let u=c===void 0||c,f;n[5]===Symbol.for(`react.memo_cache_sentinel`)?(f=(0,h.jsx)(v,{}),n[5]=f):f=n[5];let p;n[6]===o?p=n[7]:(p=r(`fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95`,o),n[6]=o,n[7]=p);let g;n[8]===u?g=n[9]:(g=u&&(0,h.jsx)(l,{"data-slot":`dialog-close`,asChild:!0,children:(0,h.jsxs)(a,{variant:`ghost`,className:`absolute top-2 right-2`,size:`icon-sm`,children:[(0,h.jsx)(e,{}),(0,h.jsx)(`span`,{className:`sr-only`,children:`Schließen`})]})}),n[8]=u,n[9]=g);let y;return n[10]!==i||n[11]!==s||n[12]!==p||n[13]!==g?(y=(0,h.jsxs)(_,{children:[f,(0,h.jsxs)(d,{"data-slot":`dialog-content`,className:p,...s,children:[i,g]})]}),n[10]=i,n[11]=s,n[12]=p,n[13]=g,n[14]=y):y=n[14],y}function b(e){let t=(0,m.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`flex flex-col gap-2`,n),t[3]=n,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,h.jsx)(`div`,{"data-slot":`dialog-header`,className:a,...i}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}function x(e){let t=(0,m.c)(14),n,i,o,s;t[0]===e?(n=t[1],i=t[2],o=t[3],s=t[4]):({className:i,showCloseButton:s,children:n,...o}=e,t[0]=e,t[1]=n,t[2]=i,t[3]=o,t[4]=s);let c=s!==void 0&&s,u;t[5]===i?u=t[6]:(u=r(`-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end`,i),t[5]=i,t[6]=u);let d;t[7]===c?d=t[8]:(d=c&&(0,h.jsx)(l,{asChild:!0,children:(0,h.jsx)(a,{variant:`outline`,children:`Schließen`})}),t[7]=c,t[8]=d);let f;return t[9]!==n||t[10]!==o||t[11]!==u||t[12]!==d?(f=(0,h.jsxs)(`div`,{"data-slot":`dialog-footer`,className:u,...o,children:[n,d]}),t[9]=n,t[10]=o,t[11]=u,t[12]=d,t[13]=f):f=t[13],f}function S(e){let t=(0,m.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`font-heading text-base leading-none font-medium`,n),t[3]=n,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,h.jsx)(f,{"data-slot":`dialog-title`,className:a,...i}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}function C(e){let t=(0,m.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground`,n),t[3]=n,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,h.jsx)(c,{"data-slot":`dialog-description`,className:a,...i}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}var w=n({Bestaetigen:()=>T});function T(e){let t=(0,m.c)(32),{offen:n,titel:r,text:i,knopf:o,neinKnopf:s,gefahr:c,gesperrt:l,onJa:u,onNein:d,children:f}=e,p=s===void 0?`Abbrechen`:s,_;t[0]===d?_=t[1]:(_=e=>!e&&d(),t[0]=d,t[1]=_);let v;t[2]===r?v=t[3]:(v=(0,h.jsx)(S,{className:`schild pr-10 text-xl break-words`,children:r}),t[2]=r,t[3]=v);let w;t[4]===i?w=t[5]:(w=(0,h.jsx)(C,{className:`text-[15px] leading-relaxed text-text-2`,children:i}),t[4]=i,t[5]=w);let T;t[6]!==v||t[7]!==w?(T=(0,h.jsxs)(b,{children:[v,w]}),t[6]=v,t[7]=w,t[8]=T):T=t[8];let E;t[9]!==p||t[10]!==d?(E=(0,h.jsx)(a,{variant:`outline`,onClick:d,children:p}),t[9]=p,t[10]=d,t[11]=E):E=t[11];let D=c?`gefahr`:`default`,O=l||void 0,k;t[12]!==l||t[13]!==u?(k=()=>{l||u()},t[12]=l,t[13]=u,t[14]=k):k=t[14];let A;t[15]!==l||t[16]!==o||t[17]!==D||t[18]!==O||t[19]!==k?(A=(0,h.jsx)(a,{variant:D,disabled:l,"aria-busy":O,onClick:k,children:o}),t[15]=l,t[16]=o,t[17]=D,t[18]=O,t[19]=k,t[20]=A):A=t[20];let j;t[21]!==A||t[22]!==E?(j=(0,h.jsxs)(x,{className:`sticky bottom-0 mb-0 gap-2 bg-erhaben sm:gap-2`,children:[E,A]}),t[21]=A,t[22]=E,t[23]=j):j=t[23];let M;t[24]!==f||t[25]!==j||t[26]!==T?(M=(0,h.jsxs)(y,{className:`max-h-[calc(100dvh-2rem)] overflow-y-auto border-linie bg-panel pb-0 sm:max-w-lg`,children:[T,f,j]}),t[24]=f,t[25]=j,t[26]=T,t[27]=M):M=t[27];let N;return t[28]!==n||t[29]!==M||t[30]!==_?(N=(0,h.jsx)(g,{open:n,onOpenChange:_,children:M}),t[28]=n,t[29]=M,t[30]=_,t[31]=N):N=t[31],N}export{w as n,T as t}; \ No newline at end of file diff --git a/frontend/dist/assets/Bestaetigen-kFure1oW.js b/frontend/dist/assets/Bestaetigen-kFure1oW.js deleted file mode 100644 index 493a29e..0000000 --- a/frontend/dist/assets/Bestaetigen-kFure1oW.js +++ /dev/null @@ -1 +0,0 @@ -import{i as e,n as t,r as n,t as r}from"./dist-BaE5-hhJ.js";import{a as i,i as a,n as o,o as s,r as c,s as l,t as u}from"./dist-ndv4TK4_.js";import{W as d,z as f}from"./index-DSxwfDVg.js";var p=t();e();var m=n();function h(e){let t=(0,p.c)(4),n;t[0]===e?n=t[1]:({...n}=e,t[0]=e,t[1]=n);let r;return t[2]===n?r=t[3]:(r=(0,m.jsx)(u,{"data-slot":`dialog`,...n}),t[2]=n,t[3]=r),r}function g(e){let t=(0,p.c)(4),n;t[0]===e?n=t[1]:({...n}=e,t[0]=e,t[1]=n);let r;return t[2]===n?r=t[3]:(r=(0,m.jsx)(s,{"data-slot":`dialog-portal`,...n}),t[2]=n,t[3]=r),r}function _(e){let t=(0,p.c)(8),n,a;t[0]===e?(n=t[1],a=t[2]):({className:n,...a}=e,t[0]=e,t[1]=n,t[2]=a);let o;t[3]===n?o=t[4]:(o=r(`fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0`,n),t[3]=n,t[4]=o);let s;return t[5]!==a||t[6]!==o?(s=(0,m.jsx)(i,{"data-slot":`dialog-overlay`,className:o,...a}),t[5]=a,t[6]=o,t[7]=s):s=t[7],s}function v(e){let t=(0,p.c)(15),n,i,a,s;t[0]===e?(n=t[1],i=t[2],a=t[3],s=t[4]):({className:i,children:n,showCloseButton:s,...a}=e,t[0]=e,t[1]=n,t[2]=i,t[3]=a,t[4]=s);let l=s===void 0||s,u;t[5]===Symbol.for(`react.memo_cache_sentinel`)?(u=(0,m.jsx)(_,{}),t[5]=u):u=t[5];let h;t[6]===i?h=t[7]:(h=r(`fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95`,i),t[6]=i,t[7]=h);let v;t[8]===l?v=t[9]:(v=l&&(0,m.jsx)(o,{"data-slot":`dialog-close`,asChild:!0,children:(0,m.jsxs)(f,{variant:`ghost`,className:`absolute top-2 right-2`,size:`icon-sm`,children:[(0,m.jsx)(d,{}),(0,m.jsx)(`span`,{className:`sr-only`,children:`Schließen`})]})}),t[8]=l,t[9]=v);let y;return t[10]!==n||t[11]!==a||t[12]!==h||t[13]!==v?(y=(0,m.jsxs)(g,{children:[u,(0,m.jsxs)(c,{"data-slot":`dialog-content`,className:h,...a,children:[n,v]})]}),t[10]=n,t[11]=a,t[12]=h,t[13]=v,t[14]=y):y=t[14],y}function y(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`flex flex-col gap-2`,n),t[3]=n,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,m.jsx)(`div`,{"data-slot":`dialog-header`,className:a,...i}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}function b(e){let t=(0,p.c)(14),n,i,a,s;t[0]===e?(n=t[1],i=t[2],a=t[3],s=t[4]):({className:i,showCloseButton:s,children:n,...a}=e,t[0]=e,t[1]=n,t[2]=i,t[3]=a,t[4]=s);let c=s!==void 0&&s,l;t[5]===i?l=t[6]:(l=r(`-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end`,i),t[5]=i,t[6]=l);let u;t[7]===c?u=t[8]:(u=c&&(0,m.jsx)(o,{asChild:!0,children:(0,m.jsx)(f,{variant:`outline`,children:`Schließen`})}),t[7]=c,t[8]=u);let d;return t[9]!==n||t[10]!==a||t[11]!==l||t[12]!==u?(d=(0,m.jsxs)(`div`,{"data-slot":`dialog-footer`,className:l,...a,children:[n,u]}),t[9]=n,t[10]=a,t[11]=l,t[12]=u,t[13]=d):d=t[13],d}function x(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`font-heading text-base leading-none font-medium`,n),t[3]=n,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,m.jsx)(l,{"data-slot":`dialog-title`,className:a,...i}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}function S(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let o;t[3]===n?o=t[4]:(o=r(`text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground`,n),t[3]=n,t[4]=o);let s;return t[5]!==i||t[6]!==o?(s=(0,m.jsx)(a,{"data-slot":`dialog-description`,className:o,...i}),t[5]=i,t[6]=o,t[7]=s):s=t[7],s}function C(e){let t=(0,p.c)(32),{offen:n,titel:r,text:i,knopf:a,neinKnopf:o,gefahr:s,gesperrt:c,onJa:l,onNein:u,children:d}=e,g=o===void 0?`Abbrechen`:o,_;t[0]===u?_=t[1]:(_=e=>!e&&u(),t[0]=u,t[1]=_);let C;t[2]===r?C=t[3]:(C=(0,m.jsx)(x,{className:`schild pr-10 text-xl break-words`,children:r}),t[2]=r,t[3]=C);let w;t[4]===i?w=t[5]:(w=(0,m.jsx)(S,{className:`text-[15px] leading-relaxed text-text-2`,children:i}),t[4]=i,t[5]=w);let T;t[6]!==C||t[7]!==w?(T=(0,m.jsxs)(y,{children:[C,w]}),t[6]=C,t[7]=w,t[8]=T):T=t[8];let E;t[9]!==g||t[10]!==u?(E=(0,m.jsx)(f,{variant:`outline`,onClick:u,children:g}),t[9]=g,t[10]=u,t[11]=E):E=t[11];let D=s?`gefahr`:`default`,O=c||void 0,k;t[12]!==c||t[13]!==l?(k=()=>{c||l()},t[12]=c,t[13]=l,t[14]=k):k=t[14];let A;t[15]!==c||t[16]!==a||t[17]!==D||t[18]!==O||t[19]!==k?(A=(0,m.jsx)(f,{variant:D,disabled:c,"aria-busy":O,onClick:k,children:a}),t[15]=c,t[16]=a,t[17]=D,t[18]=O,t[19]=k,t[20]=A):A=t[20];let j;t[21]!==A||t[22]!==E?(j=(0,m.jsxs)(b,{className:`sticky bottom-0 mb-0 gap-2 bg-erhaben sm:gap-2`,children:[E,A]}),t[21]=A,t[22]=E,t[23]=j):j=t[23];let M;t[24]!==d||t[25]!==j||t[26]!==T?(M=(0,m.jsxs)(v,{className:`max-h-[calc(100dvh-2rem)] overflow-y-auto border-linie bg-panel pb-0 sm:max-w-lg`,children:[T,d,j]}),t[24]=d,t[25]=j,t[26]=T,t[27]=M):M=t[27];let N;return t[28]!==n||t[29]!==M||t[30]!==_?(N=(0,m.jsx)(h,{open:n,onOpenChange:_,children:M}),t[28]=n,t[29]=M,t[30]=_,t[31]=N):N=t[31],N}export{C as Bestaetigen}; \ No newline at end of file diff --git a/frontend/dist/assets/Dienste-Cy5bJFkH.js b/frontend/dist/assets/Dienste-Cy5bJFkH.js deleted file mode 100644 index b0f14f0..0000000 --- a/frontend/dist/assets/Dienste-Cy5bJFkH.js +++ /dev/null @@ -1 +0,0 @@ -import{i as e,n as t,o as n,r,t as i}from"./dist-BaE5-hhJ.js";import{$ as a,H as o,M as s,R as c,_ as l,o as u,z as d}from"./index-DSxwfDVg.js";import{a as f,i as p,n as m,r as h,t as g}from"./sheet-iGGgLZLg.js";var _=t(),v=n(e(),1),y=r();function b(e){let t=(0,_.c)(4),{unit:n}=e,r=l(n),i;if(r.isPending)i=`Wird gelesen …`;else if(r.isError)i=`Das Protokoll lässt sich gerade nicht lesen: ${r.error.message}`;else if(r.data.ok===!1)i=r.data.err||r.data.text||`Das Protokoll lässt sich nicht lesen.`;else{let e;t[0]===r.data.text?e=t[1]:(e=r.data.text?.trim()||`Noch keine Einträge.`,t[0]=r.data.text,t[1]=e),i=e}let a;return t[2]===i?a=t[3]:(a=(0,y.jsx)(`pre`,{className:`ziffern m-0 max-h-[50vh] w-full basis-full overflow-auto rounded-lg border border-linie bg-[#0b0d0f] p-3 text-xs leading-relaxed whitespace-pre-wrap break-all text-text-2`,children:i}),t[2]=i,t[3]=a),a}function x({offen:e,onSchliessen:t}){let n=a(),r=u(),[l,_]=(0,v.useState)(null),[x,S]=(0,v.useState)(null),[C,w]=(0,v.useState)(null);async function T(e){if(!C){S(null),w(e.unit);try{let t=await c(`/api/maintenance/restart`,{service:e.unit});t.ok?o(`erfolg`,`${e.name} ${e.schlaeft?`wird geweckt`:`startet neu`}.`):o(`fehler`,`${e.name} ließ sich nicht starten: ${s(t)}`),n.invalidateQueries({queryKey:[`dienste`]})}catch(e){o(`fehler`,e.message)}finally{w(null)}}}let E;return E=r.isPending?(0,y.jsx)(`p`,{role:`status`,className:`px-4 text-[15px] text-text-2`,children:`Dienste werden gelesen …`}):r.isError&&!r.data?(0,y.jsxs)(`p`,{role:`alert`,className:`px-4 text-[15px] text-rot-text`,children:[`Die Dienste lassen sich gerade nicht lesen: `,r.error.message]}):r.data?.services.length?(0,y.jsx)(`ul`,{className:`m-0 flex list-none flex-col px-4 pb-4`,children:r.data.services.map(e=>(0,y.jsxs)(`li`,{className:`flex flex-wrap items-center justify-between gap-2 border-t border-linie py-2.5`,children:[(0,y.jsxs)(`span`,{className:`flex min-w-0 items-center gap-2.5`,children:[(0,y.jsx)(`span`,{className:i(`size-2.5 shrink-0 rounded-full`,e.ok?`bg-gruen`:e.schlaeft?`bg-text-3`:`bg-rot`),"aria-hidden":!0}),(0,y.jsxs)(`span`,{className:`min-w-0`,children:[(0,y.jsx)(`span`,{className:`block text-[15px]`,children:e.name}),(0,y.jsxs)(`span`,{className:`ziffern text-xs text-text-3`,children:[e.unit,` · `,e.ok?`läuft`:e.schlaeft?`schläft (abgeschaltet)`:`antwortet nicht`]})]})]}),(0,y.jsxs)(`span`,{className:`flex flex-wrap gap-2`,children:[(0,y.jsx)(d,{variant:l===e.unit?`info`:`ghost`,size:`sm`,"aria-expanded":l===e.unit,onClick:()=>_(l===e.unit?null:e.unit),children:`Protokoll`}),x===e.unit?(0,y.jsxs)(y.Fragment,{children:[(0,y.jsx)(d,{variant:`gefahr`,size:`sm`,onClick:()=>T(e),children:e.schlaeft?`Wirklich wecken?`:`Wirklich neu starten?`}),(0,y.jsx)(d,{variant:`ghost`,size:`sm`,onClick:()=>S(null),children:`Nein`})]}):(0,y.jsx)(d,{variant:`outline`,size:`sm`,disabled:C!==null,onClick:()=>S(e.unit),children:C===e.unit?`Startet …`:e.schlaeft?`Wecken`:`Neu starten`})]}),l===e.unit&&(0,y.jsx)(b,{unit:e.unit})]},e.unit))}):(0,y.jsx)(`p`,{className:`px-4 text-[15px] text-text-2`,children:`Keine Dienste gefunden.`}),(0,y.jsx)(g,{open:e,onOpenChange:e=>!e&&t(),children:(0,y.jsxs)(m,{side:`right`,className:`gap-3 overflow-y-auto border-linie bg-panel data-[side=right]:w-full data-[side=right]:sm:max-w-2xl`,children:[(0,y.jsxs)(p,{children:[(0,y.jsx)(f,{className:`schild text-lg`,children:`Dienste und Protokolle`}),(0,y.jsx)(h,{className:`text-text-2`,children:`Welcher Dienst läuft, was er zuletzt geschrieben hat.`})]}),E]})})}export{x as Dienste}; \ No newline at end of file diff --git a/frontend/dist/assets/Dienste-DRmtFD1e.js b/frontend/dist/assets/Dienste-DRmtFD1e.js new file mode 100644 index 0000000..a88ca5d --- /dev/null +++ b/frontend/dist/assets/Dienste-DRmtFD1e.js @@ -0,0 +1 @@ +import{c as e,i as t,p as n,s as r,t as i,u as a}from"./button-BheHBEml.js";import{H as o,I as s,Q as c,U as l,b as u,s as d}from"./index-SRwQwEnh.js";import{a as f,i as p,n as m,r as h,t as g}from"./sheet-CKp82pOA.js";var _=r(),v=n(a(),1),y=e();function b(e){let t=(0,_.c)(4),{unit:n}=e,r=u(n),i;if(r.isPending)i=`Wird gelesen …`;else if(r.isError)i=`Das Protokoll lässt sich gerade nicht lesen: ${r.error.message}`;else if(r.data.ok===!1)i=r.data.err||r.data.text||`Das Protokoll lässt sich nicht lesen.`;else{let e;t[0]===r.data.text?e=t[1]:(e=r.data.text?.trim()||`Noch keine Einträge.`,t[0]=r.data.text,t[1]=e),i=e}let a;return t[2]===i?a=t[3]:(a=(0,y.jsx)(`pre`,{className:`ziffern m-0 max-h-[50vh] w-full basis-full overflow-auto rounded-lg border border-linie bg-[#0b0d0f] p-3 text-xs leading-relaxed whitespace-pre-wrap break-all text-text-2`,children:i}),t[2]=i,t[3]=a),a}function x({offen:e,onSchliessen:n}){let r=c(),a=d(),[u,_]=(0,v.useState)(null),[x,S]=(0,v.useState)(null),[C,w]=(0,v.useState)(null);async function T(e){if(!C){S(null),w(e.unit);try{let t=await o(`/api/maintenance/restart`,{service:e.unit});t.ok?l(`erfolg`,`${e.name} ${e.schlaeft?`wird geweckt`:`startet neu`}.`):l(`fehler`,`${e.name} ließ sich nicht starten: ${s(t)}`),r.invalidateQueries({queryKey:[`dienste`]})}catch(e){l(`fehler`,e.message)}finally{w(null)}}}let E;return E=a.isPending?(0,y.jsx)(`p`,{role:`status`,className:`px-4 text-[15px] text-text-2`,children:`Dienste werden gelesen …`}):a.isError&&!a.data?(0,y.jsxs)(`p`,{role:`alert`,className:`px-4 text-[15px] text-rot-text`,children:[`Die Dienste lassen sich gerade nicht lesen: `,a.error.message]}):a.data?.services.length?(0,y.jsx)(`ul`,{className:`m-0 flex list-none flex-col px-4 pb-4`,children:a.data.services.map(e=>(0,y.jsxs)(`li`,{className:`flex flex-wrap items-center justify-between gap-2 border-t border-linie py-2.5`,children:[(0,y.jsxs)(`span`,{className:`flex min-w-0 items-center gap-2.5`,children:[(0,y.jsx)(`span`,{className:t(`size-2.5 shrink-0 rounded-full`,e.ok?`bg-gruen`:e.schlaeft?`bg-text-3`:`bg-rot`),"aria-hidden":!0}),(0,y.jsxs)(`span`,{className:`min-w-0`,children:[(0,y.jsx)(`span`,{className:`block text-[15px]`,children:e.name}),(0,y.jsxs)(`span`,{className:`ziffern text-xs text-text-3`,children:[e.unit,` · `,e.ok?`läuft`:e.schlaeft?`schläft (abgeschaltet)`:`antwortet nicht`]})]})]}),(0,y.jsxs)(`span`,{className:`flex flex-wrap gap-2`,children:[(0,y.jsx)(i,{variant:u===e.unit?`info`:`ghost`,size:`sm`,"aria-expanded":u===e.unit,onClick:()=>_(u===e.unit?null:e.unit),children:`Protokoll`}),x===e.unit?(0,y.jsxs)(y.Fragment,{children:[(0,y.jsx)(i,{variant:`gefahr`,size:`sm`,onClick:()=>T(e),children:e.schlaeft?`Wirklich wecken?`:`Wirklich neu starten?`}),(0,y.jsx)(i,{variant:`ghost`,size:`sm`,onClick:()=>S(null),children:`Nein`})]}):(0,y.jsx)(i,{variant:`outline`,size:`sm`,disabled:C!==null,onClick:()=>S(e.unit),children:C===e.unit?`Startet …`:e.schlaeft?`Wecken`:`Neu starten`})]}),u===e.unit&&(0,y.jsx)(b,{unit:e.unit})]},e.unit))}):(0,y.jsx)(`p`,{className:`px-4 text-[15px] text-text-2`,children:`Keine Dienste gefunden.`}),(0,y.jsx)(g,{open:e,onOpenChange:e=>!e&&n(),children:(0,y.jsxs)(m,{side:`right`,className:`gap-3 overflow-y-auto border-linie bg-panel data-[side=right]:w-full data-[side=right]:sm:max-w-2xl`,children:[(0,y.jsxs)(p,{children:[(0,y.jsx)(f,{className:`schild text-lg`,children:`Dienste und Protokolle`}),(0,y.jsx)(h,{className:`text-text-2`,children:`Welcher Dienst läuft, was er zuletzt geschrieben hat.`})]}),E]})})}export{x as Dienste}; \ No newline at end of file diff --git a/frontend/dist/assets/Einstellungen-BSsBxIeF.js b/frontend/dist/assets/Einstellungen-BSsBxIeF.js deleted file mode 100644 index 6b0e81c..0000000 --- a/frontend/dist/assets/Einstellungen-BSsBxIeF.js +++ /dev/null @@ -1 +0,0 @@ -import{i as e,o as t,r as n}from"./dist-BaE5-hhJ.js";import{$ as r,H as i,L as a,Q as o,R as s,q as c,z as l}from"./index-DSxwfDVg.js";import{a as u,i as d,n as f,r as p,t as m}from"./sheet-iGGgLZLg.js";var h=t(e(),1),g=n();function _({offen:e,onSchliessen:t}){let n=r(),_=o({queryKey:[`geheimnisse`],queryFn:()=>a(`/api/maintenance/geheimnisse`),enabled:e}),[v,y]=(0,h.useState)(``),[b,x]=(0,h.useState)(!1),[S,C]=(0,h.useState)(!1);async function w(e){if(!b){x(!0),C(!1);try{await s(`/api/maintenance/geheimnisse`,{schluessel:`hf_token`,wert:e}),i(`erfolg`,e?`Hugging-Face-Zugang gespeichert.`:`Hugging-Face-Zugang gelöscht.`),y(``),n.invalidateQueries({queryKey:[`geheimnisse`]})}catch(e){i(`fehler`,e.message)}finally{x(!1)}}}let T=_.data,E;return E=_.isPending?`Wird gelesen …`:T?T.hf_token_gesetzt?`Ist hinterlegt.`:`Fehlt.`:`Lässt sich gerade nicht lesen${_.error?`: ${_.error.message}`:`.`}`,(0,g.jsx)(m,{open:e,onOpenChange:e=>!e&&t(),children:(0,g.jsxs)(f,{side:`right`,className:`gap-3 overflow-y-auto border-linie bg-panel data-[side=right]:w-full data-[side=right]:sm:max-w-lg`,children:[(0,g.jsxs)(d,{children:[(0,g.jsx)(u,{className:`schild text-lg`,children:`Einstellungen`}),(0,g.jsx)(p,{className:`text-text-2`,children:`Was die Box sich merkt.`})]}),(0,g.jsxs)(`section`,{className:`flex flex-col gap-3 border-t border-linie px-4 pt-4`,children:[(0,g.jsx)(`h3`,{className:`schild text-sm text-text-3`,children:`Hugging-Face-Zugang`}),(0,g.jsxs)(`p`,{className:`text-sm text-text-2`,"aria-live":`polite`,children:[(0,g.jsx)(`span`,{className:!T&&!_.isPending?`text-rot-text`:void 0,children:E}),` Nötig für gesperrte Modelle und schnellere Downloads.`,T?.hf_token_aus_env?` Er kommt aus der Dienst-Einstellung und lässt sich hier nicht löschen.`:``]}),T?.schreibbar===!1&&(0,g.jsx)(`p`,{className:`text-sm text-bernstein`,children:`Die Box kann den Zugang gerade nicht speichern: Ihre Ablage ist nicht beschreibbar.`}),(0,g.jsxs)(`form`,{className:`flex gap-2`,onSubmit:e=>{e.preventDefault(),v.trim()&&w(v.trim())},children:[(0,g.jsx)(`label`,{htmlFor:`hf-token`,className:`sr-only`,children:`Hugging-Face-Zugang`}),(0,g.jsx)(`input`,{id:`hf-token`,type:`password`,autoComplete:`off`,value:v,onChange:e=>y(e.target.value),placeholder:`hf_…`,className:`h-11 min-w-0 flex-1 rounded-lg border border-linie-stark bg-erhaben px-3 text-[15px] outline-none focus:border-cyan`}),(0,g.jsx)(l,{type:`submit`,disabled:!v.trim()||T?.schreibbar===!1||b,children:b?`Speichert …`:`Speichern`})]}),T?.hf_token_gesetzt&&!T.hf_token_aus_env&&(S?(0,g.jsxs)(`span`,{className:`flex flex-wrap gap-2`,children:[(0,g.jsx)(l,{variant:`gefahr`,size:`sm`,disabled:b,onClick:()=>w(null),children:`Wirklich löschen?`}),(0,g.jsx)(l,{variant:`ghost`,size:`sm`,onClick:()=>C(!1),children:`Nein`})]}):(0,g.jsx)(l,{variant:`ghost`,size:`sm`,className:`self-start`,disabled:b,onClick:()=>C(!0),children:`Zugang löschen`}))]}),(0,g.jsxs)(`section`,{className:`flex flex-col gap-2 border-t border-linie px-4 pt-4`,children:[(0,g.jsx)(`h3`,{className:`schild text-sm text-text-3`,children:`Hinweise`}),(0,g.jsx)(`p`,{className:`text-sm text-text-2`,children:`Dringende Hinweise gehen zusätzlich per Telegram raus, alles andere bleibt hier. Einfache Probleme wie einen abgestürzten Dienst behebt der Wächter selbst und schreibt es in den Verlauf.`})]}),(0,g.jsxs)(`section`,{className:`flex flex-col gap-2 border-t border-linie px-4 pt-4 pb-4`,children:[(0,g.jsx)(`h3`,{className:`schild text-sm text-text-3`,children:`Hermes`}),(0,g.jsxs)(`a`,{href:`/hermes-ui/`,target:`_blank`,rel:`noopener`,className:`flex h-11 items-center gap-2 self-start rounded-lg border border-linie-stark bg-erhaben px-4 text-[15px] hover:border-text-3`,children:[(0,g.jsx)(c,{className:`size-4`,"aria-hidden":!0}),` Hermes-Dashboard öffnen`]}),(0,g.jsx)(`p`,{className:`text-xs text-text-3`,children:`Chat, Sitzungen und Cron-Jobs verwaltet Hermes selbst. Es fragt nach seiner eigenen Anmeldung.`})]})]})})}export{_ as Einstellungen}; \ No newline at end of file diff --git a/frontend/dist/assets/Einstellungen-oERyGBBU.js b/frontend/dist/assets/Einstellungen-oERyGBBU.js new file mode 100644 index 0000000..23132ed --- /dev/null +++ b/frontend/dist/assets/Einstellungen-oERyGBBU.js @@ -0,0 +1 @@ +import{c as e,p as t,t as n,u as r}from"./button-BheHBEml.js";import{H as i,Q as a,U as o,V as s,Z as c,q as l}from"./index-SRwQwEnh.js";import{a as u,i as d,n as f,r as p,t as m}from"./sheet-CKp82pOA.js";var h=t(r(),1),g=e();function _({offen:e,onSchliessen:t}){let r=a(),_=c({queryKey:[`geheimnisse`],queryFn:()=>s(`/api/maintenance/geheimnisse`),enabled:e}),[v,y]=(0,h.useState)(``),[b,x]=(0,h.useState)(!1),[S,C]=(0,h.useState)(!1);async function w(e){if(!b){x(!0),C(!1);try{await i(`/api/maintenance/geheimnisse`,{schluessel:`hf_token`,wert:e}),o(`erfolg`,e?`Hugging-Face-Zugang gespeichert.`:`Hugging-Face-Zugang gelöscht.`),y(``),r.invalidateQueries({queryKey:[`geheimnisse`]})}catch(e){o(`fehler`,e.message)}finally{x(!1)}}}let T=_.data,E;return E=_.isPending?`Wird gelesen …`:T?T.hf_token_gesetzt?`Ist hinterlegt.`:`Fehlt.`:`Lässt sich gerade nicht lesen${_.error?`: ${_.error.message}`:`.`}`,(0,g.jsx)(m,{open:e,onOpenChange:e=>!e&&t(),children:(0,g.jsxs)(f,{side:`right`,className:`gap-3 overflow-y-auto border-linie bg-panel data-[side=right]:w-full data-[side=right]:sm:max-w-lg`,children:[(0,g.jsxs)(d,{children:[(0,g.jsx)(u,{className:`schild text-lg`,children:`Einstellungen`}),(0,g.jsx)(p,{className:`text-text-2`,children:`Was die Box sich merkt.`})]}),(0,g.jsxs)(`section`,{className:`flex flex-col gap-3 border-t border-linie px-4 pt-4`,children:[(0,g.jsx)(`h3`,{className:`schild text-sm text-text-3`,children:`Hugging-Face-Zugang`}),(0,g.jsxs)(`p`,{className:`text-sm text-text-2`,"aria-live":`polite`,children:[(0,g.jsx)(`span`,{className:!T&&!_.isPending?`text-rot-text`:void 0,children:E}),` Nötig für gesperrte Modelle und schnellere Downloads.`,T?.hf_token_aus_env?` Er kommt aus der Dienst-Einstellung und lässt sich hier nicht löschen.`:``]}),T?.schreibbar===!1&&(0,g.jsx)(`p`,{className:`text-sm text-bernstein`,children:`Die Box kann den Zugang gerade nicht speichern: Ihre Ablage ist nicht beschreibbar.`}),(0,g.jsxs)(`form`,{className:`flex gap-2`,onSubmit:e=>{e.preventDefault(),v.trim()&&w(v.trim())},children:[(0,g.jsx)(`label`,{htmlFor:`hf-token`,className:`sr-only`,children:`Hugging-Face-Zugang`}),(0,g.jsx)(`input`,{id:`hf-token`,type:`password`,autoComplete:`off`,value:v,onChange:e=>y(e.target.value),placeholder:`hf_…`,className:`h-11 min-w-0 flex-1 rounded-lg border border-linie-stark bg-erhaben px-3 text-[15px] outline-none focus:border-cyan`}),(0,g.jsx)(n,{type:`submit`,disabled:!v.trim()||T?.schreibbar===!1||b,children:b?`Speichert …`:`Speichern`})]}),T?.hf_token_gesetzt&&!T.hf_token_aus_env&&(S?(0,g.jsxs)(`span`,{className:`flex flex-wrap gap-2`,children:[(0,g.jsx)(n,{variant:`gefahr`,size:`sm`,disabled:b,onClick:()=>w(null),children:`Wirklich löschen?`}),(0,g.jsx)(n,{variant:`ghost`,size:`sm`,onClick:()=>C(!1),children:`Nein`})]}):(0,g.jsx)(n,{variant:`ghost`,size:`sm`,className:`self-start`,disabled:b,onClick:()=>C(!0),children:`Zugang löschen`}))]}),(0,g.jsxs)(`section`,{className:`flex flex-col gap-2 border-t border-linie px-4 pt-4`,children:[(0,g.jsx)(`h3`,{className:`schild text-sm text-text-3`,children:`Hinweise`}),(0,g.jsx)(`p`,{className:`text-sm text-text-2`,children:`Dringende Hinweise gehen zusätzlich per Telegram raus, alles andere bleibt hier. Einfache Probleme wie einen abgestürzten Dienst behebt der Wächter selbst und schreibt es in den Verlauf.`})]}),(0,g.jsxs)(`section`,{className:`flex flex-col gap-2 border-t border-linie px-4 pt-4 pb-4`,children:[(0,g.jsx)(`h3`,{className:`schild text-sm text-text-3`,children:`Hermes`}),(0,g.jsxs)(`a`,{href:`/hermes-ui/`,target:`_blank`,rel:`noopener`,className:`flex h-11 items-center gap-2 self-start rounded-lg border border-linie-stark bg-erhaben px-4 text-[15px] hover:border-text-3`,children:[(0,g.jsx)(l,{className:`size-4`,"aria-hidden":!0}),` Hermes-Dashboard öffnen`]}),(0,g.jsx)(`p`,{className:`text-xs text-text-3`,children:`Chat, Sitzungen und Cron-Jobs verwaltet Hermes selbst. Es fragt nach seiner eigenen Anmeldung.`})]})]})})}export{_ as Einstellungen}; \ No newline at end of file diff --git a/frontend/dist/assets/Homelab-BUx8ea4H.js b/frontend/dist/assets/Homelab-BUx8ea4H.js deleted file mode 100644 index 1deb56a..0000000 --- a/frontend/dist/assets/Homelab-BUx8ea4H.js +++ /dev/null @@ -1 +0,0 @@ -import{n as e,r as t}from"./dist-BaE5-hhJ.js";import{g as n,l as r,n as i,r as a,t as o}from"./index-DSxwfDVg.js";var s=e(),c=t(),l=[`Proxmox-Host`,`AdGuard`,`NPMplus`,`NetBird`,`PVE Scripts Local`,`Gitea`,`PBS`,`Arcane mit Docker`];function u(e){let t=(0,s.c)(11),{partner:n}=e;if(n.erreichbar){let e=n.instanz??n.name,r=n.version?` (Version ${n.version})`:``,i;t[0]!==e||t[1]!==r?(i=(0,c.jsxs)(`p`,{className:`text-[15px] text-text-2`,children:[e,` antwortet`,r,`. Die Übersicht der Geräte mit ihren Updates folgt als Nächstes.`]}),t[0]=e,t[1]=r,t[2]=i):i=t[2];let a;t[3]===n.url?a=t[4]:(a=(0,c.jsx)(`p`,{className:`ziffern text-sm text-text-3`,children:n.url}),t[3]=n.url,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,c.jsxs)(c.Fragment,{children:[i,a]}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}let r=n.fehler??`nicht erreichbar`,i;return t[8]!==n.url||t[9]!==r?(i=(0,c.jsxs)(`p`,{role:`alert`,className:`text-[15px] text-rot-text`,children:[`Der Homelab-Teil (`,n.url,`) ist `,r,`. Hält das an, meldet der Wächter es nach fünf Minuten auf Telegram.`]}),t[8]=n.url,t[9]=r,t[10]=i):i=t[10],i}function d(){let e=(0,s.c)(4),t,n;e[0]===Symbol.for(`react.memo_cache_sentinel`)?(t=(0,c.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Der Homelab-Teil läuft als eigene Instanz in einem kleinen Container auf dem Proxmox-PC. Von dort prüft er auch die KI-Box von außen.`}),n=(0,c.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Steht er, zeigt diese Seite alle Geräte mit ihren offenen Updates. Ein Update startest du dann hier mit „Jetzt updaten“: Vorher wird gesichert, danach die Erreichbarkeit geprüft, und ist sie rot, geht es von selbst zurück.`}),e[0]=t,e[1]=n):(t=e[0],n=e[1]);let r;e[2]===Symbol.for(`react.memo_cache_sentinel`)?(r=(0,c.jsx)(`h3`,{className:`schild text-sm tracking-[0.18em] text-text-3`,children:`Dazu kommen`}),e[2]=r):r=e[2];let i;return e[3]===Symbol.for(`react.memo_cache_sentinel`)?(i=(0,c.jsxs)(c.Fragment,{children:[t,n,(0,c.jsxs)(`div`,{className:`flex flex-col gap-2`,children:[r,(0,c.jsx)(`ul`,{className:`flex flex-wrap gap-2`,children:l.map(f)})]}),(0,c.jsx)(`p`,{className:`text-sm text-text-3`,children:`Der Container wird erst nach deiner Freigabe angelegt.`})]}),e[3]=i):i=e[3],i}function f(e){return(0,c.jsx)(`li`,{children:(0,c.jsx)(i,{children:e})},e)}function p(){let e=(0,s.c)(12),t=r(),l=n();if(t.isPending||l.isPending){let t;return e[0]===Symbol.for(`react.memo_cache_sentinel`)?(t=(0,c.jsx)(a,{text:`Wird gelesen …`}),e[0]=t):t=e[0],t}if(!l.data){let t=`Der Stand des Homelab-Teils lässt sich nicht lesen: ${l.error?.message??`keine Daten`}`,n;return e[1]===t?n=e[2]:(n=(0,c.jsx)(a,{fehler:!0,text:t}),e[1]=t,e[2]=n),n}if(t.data?.rolle===`homelab`){let t;return e[3]===Symbol.for(`react.memo_cache_sentinel`)?(t=(0,c.jsx)(o,{titel:`Homelab`,rechts:(0,c.jsx)(i,{art:`gruen`,children:`Diese Instanz`}),children:(0,c.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Diese Seite kommt vom Homelab-Teil auf dem Proxmox-PC.`})}),e[3]=t):t=e[3],t}let f=l.data,p;e[4]!==f.eingerichtet||e[5]!==f.erreichbar?(p=f.eingerichtet?f.erreichbar?(0,c.jsx)(i,{art:`gruen`,children:`Verbunden`}):(0,c.jsx)(i,{art:`rot`,children:`Antwortet nicht`}):(0,c.jsx)(i,{children:`Noch nicht eingerichtet`}),e[4]=f.eingerichtet,e[5]=f.erreichbar,e[6]=p):p=e[6];let m=p,h;e[7]===f?h=e[8]:(h=f.eingerichtet?(0,c.jsx)(u,{partner:f}):(0,c.jsx)(d,{}),e[7]=f,e[8]=h);let g;return e[9]!==m||e[10]!==h?(g=(0,c.jsx)(o,{titel:`Homelab`,rechts:m,children:h}),e[9]=m,e[10]=h,e[11]=g):g=e[11],g}export{p as HomelabSeite}; \ No newline at end of file diff --git a/frontend/dist/assets/Homelab-D1tOCjyo.js b/frontend/dist/assets/Homelab-D1tOCjyo.js new file mode 100644 index 0000000..6314efe --- /dev/null +++ b/frontend/dist/assets/Homelab-D1tOCjyo.js @@ -0,0 +1 @@ +import{c as e,i as t,p as n,s as r,t as i,u as a}from"./button-BheHBEml.js";import{f as o,j as s,k as c,l,n as u,o as d,r as f,t as p,u as m,y as h}from"./index-SRwQwEnh.js";import{t as g}from"./Bestaetigen-CE_3b0PK.js";var _=r(),v=n(a(),1),y=e(),b={neu:{art:`cyan`,text:`Update bereit`},aktuell:{art:`gruen`,text:`aktuell`},unbekannt:{art:`bernstein`,text:`unklar`},festgehalten:{art:`bernstein`,text:`festgehalten`},"wird-geprueft":{art:`grau`,text:`wird geprüft`}},x={"ki-box":`KI-Box`,"proxmox-host":`Proxmox-Host`,container:`Container`,vm:`VM`};function S(e){let t=e.id.match(/-(\d+)$/)?.[1],n=x[e.art]??e.art;return t?`${n} ${t}`:n}function C(e){let t=(0,_.c)(7),{b:n}=e;if(n.kurz){let e;return t[0]===n.kurz?e=t[1]:(e=(0,y.jsx)(`span`,{className:`ziffern`,children:n.kurz}),t[0]=n.kurz,t[1]=e),e}if(n.installiert&&n.verfuegbar){let e;return t[2]!==n.installiert||t[3]!==n.verfuegbar?(e=(0,y.jsxs)(`span`,{className:`ziffern`,children:[n.installiert,` → `,n.verfuegbar]}),t[2]=n.installiert,t[3]=n.verfuegbar,t[4]=e):e=t[4],e}if(n.installiert){let e;return t[5]===n.installiert?e=t[6]:(e=(0,y.jsx)(`span`,{className:`ziffern`,children:n.installiert}),t[5]=n.installiert,t[6]=e),e}return null}function w(e){let t=(0,_.c)(24),{b:n,gesperrt:r,onAktion:a}=e,o=b[n.zustand]??b.unbekannt,s;t[0]===n.name?s=t[1]:(s=(0,y.jsx)(`span`,{className:`text-[15px] font-medium`,children:n.name}),t[0]=n.name,t[1]=s);let c;t[2]===n?c=t[3]:(c=(0,y.jsx)(`span`,{className:`text-sm text-text-2`,children:(0,y.jsx)(C,{b:n})}),t[2]=n,t[3]=c);let l;t[4]!==s||t[5]!==c?(l=(0,y.jsxs)(`div`,{className:`flex min-w-0 flex-col`,children:[s,c]}),t[4]=s,t[5]=c,t[6]=l):l=t[6];let d;t[7]!==o.art||t[8]!==o.text?(d=(0,y.jsx)(u,{art:o.art,children:o.text}),t[7]=o.art,t[8]=o.text,t[9]=d):d=t[9];let f;t[10]!==l||t[11]!==d?(f=(0,y.jsxs)(`div`,{className:`flex flex-wrap items-center justify-between gap-2`,children:[l,d]}),t[10]=l,t[11]=d,t[12]=f):f=t[12];let p;t[13]===n.grund?p=t[14]:(p=n.grund&&(0,y.jsx)(`p`,{className:`text-sm text-text-2`,children:n.grund}),t[13]=n.grund,t[14]=p);let m;t[15]!==n.aktion||t[16]!==n.zustand||t[17]!==r||t[18]!==a?(m=n.aktion&&(0,y.jsx)(`div`,{children:(0,y.jsx)(i,{size:`sm`,variant:n.zustand===`neu`?`default`:`outline`,disabled:r,onClick:()=>n.aktion&&a(n.aktion),children:n.aktion.label})}),t[15]=n.aktion,t[16]=n.zustand,t[17]=r,t[18]=a,t[19]=m):m=t[19];let h;return t[20]!==f||t[21]!==p||t[22]!==m?(h=(0,y.jsxs)(`li`,{className:`flex flex-col gap-2 border-t border-linie py-3 first:border-t-0 first:pt-0 last:pb-0`,children:[f,p,m]}),t[20]=f,t[21]=p,t[22]=m,t[23]=h):h=t[23],h}function T(e){let n=(0,_.c)(48),{ziel:r,laeuft:i,gesperrt:a,onAktion:o}=e,[s,c]=(0,v.useState)(null),l=r.erreichbar,u=r.name,d;n[0]===r.name?d=n[1]:(d=(0,y.jsx)(`h3`,{className:`schild text-base font-bold tracking-[0.18em] break-words`,children:r.name}),n[0]=r.name,n[1]=d);let f;n[2]===r?f=n[3]:(f=S(r),n[2]=r,n[3]=f);let p;n[4]===f?p=n[5]:(p=(0,y.jsx)(`span`,{className:`text-sm text-text-3`,children:f}),n[4]=f,n[5]=p);let m;n[6]!==d||n[7]!==p?(m=(0,y.jsxs)(`div`,{className:`flex min-w-0 flex-col`,children:[d,p]}),n[6]=d,n[7]=p,n[8]=m):m=n[8];let h;n[9]===l?h=n[10]:(h=l!==null&&(0,y.jsxs)(`span`,{className:t(`schild flex items-center gap-1.5 text-xs`,l?`text-text-3`:`text-rot-text`),children:[(0,y.jsx)(`span`,{"aria-hidden":!0,className:t(`size-2 rounded-full`,l?`bg-gruen`:`bg-rot`)}),l?`erreichbar`:`antwortet nicht`]}),n[9]=l,n[10]=h);let b;n[11]!==m||n[12]!==h?(b=(0,y.jsxs)(`header`,{className:`flex flex-wrap items-start justify-between gap-2`,children:[m,h]}),n[11]=m,n[12]=h,n[13]=b):b=n[13];let x;n[14]===i?x=n[15]:(x=i&&(0,y.jsxs)(`p`,{role:`status`,className:`rounded-lg border border-cyan-rand bg-cyan-grund px-3 py-2 text-sm text-cyan-text`,children:[`Update läuft: `,i]}),n[14]=i,n[15]=x);let C;if(n[16]!==a||n[17]!==i||n[18]!==r.bausteine){let e;n[20]!==a||n[21]!==i?(e=e=>(0,y.jsx)(w,{b:e,gesperrt:a||!!i,onAktion:c},e.id),n[20]=a,n[21]=i,n[22]=e):e=n[22],C=r.bausteine.map(e),n[16]=a,n[17]=i,n[18]=r.bausteine,n[19]=C}else C=n[19];let T;n[23]===C?T=n[24]:(T=(0,y.jsx)(`ul`,{className:`flex flex-col`,children:C}),n[23]=C,n[24]=T);let E;n[25]===r.rueckweg?E=n[26]:(E=r.rueckweg&&(0,y.jsxs)(`p`,{className:`text-xs text-text-3`,children:[`Rückweg: `,r.rueckweg]}),n[25]=r.rueckweg,n[26]=E);let D=!!s,O=s?`${r.name}: ${s.label}`:``,k=s?.frage??``,A=s?.label??``,j;n[27]===s?.pfad?j=n[28]:(j=s?.pfad.endsWith(`/neustart`),n[27]=s?.pfad,n[28]=j);let M=!!j,N;n[29]!==s||n[30]!==o?(N=()=>{s&&o(s),c(null)},n[29]=s,n[30]=o,n[31]=N):N=n[31];let P;n[32]===Symbol.for(`react.memo_cache_sentinel`)?(P=()=>c(null),n[32]=P):P=n[32];let F;n[33]!==a||n[34]!==D||n[35]!==O||n[36]!==k||n[37]!==A||n[38]!==M||n[39]!==N?(F=(0,y.jsx)(g,{offen:D,titel:O,text:k,knopf:A,gefahr:M,gesperrt:a,onJa:N,onNein:P}),n[33]=a,n[34]=D,n[35]=O,n[36]=k,n[37]=A,n[38]=M,n[39]=N,n[40]=F):F=n[40];let I;return n[41]!==T||n[42]!==E||n[43]!==F||n[44]!==b||n[45]!==x||n[46]!==r.name?(I=(0,y.jsxs)(`section`,{"aria-label":u,className:`flex min-w-0 flex-col gap-3 rounded-2xl border border-linie bg-panel p-5`,children:[b,x,T,E,F]}),n[41]=T,n[42]=E,n[43]=F,n[44]=b,n[45]=x,n[46]=r.name,n[47]=I):I=n[47],I}var E=[`Proxmox-Host`,`AdGuard`,`NPMplus`,`NetBird`,`PVE Scripts Local`,`Gitea`,`PBS`,`Arcane mit Docker`],D={eingespielt:{art:`gruen`,text:`eingespielt`},zurueckgerollt:{art:`bernstein`,text:`zurückgerollt`},fehler:{art:`rot`,text:`Fehler`},neustart:{art:`cyan`,text:`Neustart`}};function O(){let e=(0,_.c)(4),t,n;e[0]===Symbol.for(`react.memo_cache_sentinel`)?(t=(0,y.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Der Homelab-Teil läuft als eigene Instanz in einem kleinen Container auf dem Proxmox-PC. Von dort prüft er auch die KI-Box von außen.`}),n=(0,y.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Steht er, erscheinen hier alle Geräte mit ihren offenen Updates. Ein Update startest du mit „Jetzt updaten“: Vorher wird gesichert, danach die Erreichbarkeit geprüft, und ist sie rot, geht es von selbst zurück.`}),e[0]=t,e[1]=n):(t=e[0],n=e[1]);let r;e[2]===Symbol.for(`react.memo_cache_sentinel`)?(r=(0,y.jsx)(`h3`,{className:`schild text-sm tracking-[0.18em] text-text-3`,children:`Dazu kommen`}),e[2]=r):r=e[2];let i;return e[3]===Symbol.for(`react.memo_cache_sentinel`)?(i=(0,y.jsxs)(y.Fragment,{children:[t,n,(0,y.jsxs)(`div`,{className:`flex flex-col gap-2`,children:[r,(0,y.jsx)(`ul`,{className:`flex flex-wrap gap-2`,children:E.map(k)})]}),(0,y.jsx)(`p`,{className:`text-sm text-text-3`,children:`Der Container wird erst nach deiner Freigabe angelegt.`})]}),e[3]=i):i=e[3],i}function k(e){return(0,y.jsx)(`li`,{children:(0,y.jsx)(u,{children:e})},e)}function A(e){let t=(0,_.c)(9),{partner:n}=e,r;t[0]===n.eingerichtet?r=t[1]:(r=n.eingerichtet?(0,y.jsx)(u,{art:`rot`,children:`Antwortet nicht`}):(0,y.jsx)(u,{children:`Noch nicht eingerichtet`}),t[0]=n.eingerichtet,t[1]=r);let i=r,a;t[2]!==n.eingerichtet||t[3]!==n.fehler||t[4]!==n.url?(a=n.eingerichtet?(0,y.jsxs)(`p`,{role:`alert`,className:`text-[15px] text-rot-text`,children:[`Der Homelab-Teil (`,n.url,`) ist `,n.fehler??`nicht erreichbar`,`. Hält das an, meldet der Wächter es nach fünf Minuten auf Telegram.`]}):(0,y.jsx)(O,{}),t[2]=n.eingerichtet,t[3]=n.fehler,t[4]=n.url,t[5]=a):a=t[5];let o;return t[6]!==i||t[7]!==a?(o=(0,y.jsx)(p,{titel:`Homelab`,rechts:i,children:a}),t[6]=i,t[7]=a,t[8]=o):o=t[8],o}function j(e){let t=(0,_.c)(13),{laeufe:n}=e,r,i,a,o,s;if(t[0]!==n){s=Symbol.for(`react.early_return_sentinel`);bb0:{let e=n.filter(N).slice(0,6);if(e.length===0){s=null;break bb0}r=p,o=`Letzte Updates im Homelab`,i=`flex flex-col gap-3`,a=e.map(M)}t[0]=n,t[1]=r,t[2]=i,t[3]=a,t[4]=o,t[5]=s}else r=t[1],i=t[2],a=t[3],o=t[4],s=t[5];if(s!==Symbol.for(`react.early_return_sentinel`))return s;let c;t[6]!==i||t[7]!==a?(c=(0,y.jsx)(`ul`,{className:i,children:a}),t[6]=i,t[7]=a,t[8]=c):c=t[8];let l;return t[9]!==r||t[10]!==o||t[11]!==c?(l=(0,y.jsx)(r,{titel:o,children:c}),t[9]=r,t[10]=o,t[11]=c,t[12]=l):l=t[12],l}function M(e){let t=e.ergebnis?D[e.ergebnis]:{art:`grau`,text:`unterbrochen`};return(0,y.jsxs)(`li`,{className:`flex flex-col gap-1 border-t border-linie pt-3 first:border-t-0 first:pt-0`,children:[(0,y.jsxs)(`div`,{className:`flex flex-wrap items-center justify-between gap-2`,children:[(0,y.jsx)(`span`,{className:`text-[15px] font-medium`,children:e.name}),(0,y.jsxs)(`span`,{className:`flex items-center gap-2`,children:[(0,y.jsx)(`span`,{className:`ziffern text-sm text-text-3`,children:s(new Date(e.start*1e3).toISOString())}),(0,y.jsx)(u,{art:t.art,children:t.text})]})]}),e.text&&(0,y.jsx)(`p`,{className:`text-sm text-text-2`,children:e.text})]},e.id)}function N(e){return e.status!==`laeuft`}function P(){let e=(0,_.c)(44),t=o(),n=h(),r=d(),i=t.data?.rolle===`homelab`||!!(n.data?.eingerichtet&&n.data.erreichbar),a=l(i),g=m(i),v=c();if(t.isPending||n.isPending){let t;return e[0]===Symbol.for(`react.memo_cache_sentinel`)?(t=(0,y.jsx)(f,{text:`Wird gelesen …`}),e[0]=t):t=e[0],t}let b;if(e[1]!==g.data?.laeufe){b=new Map;for(let e of g.data?.laeufe??[])e.status===`laeuft`&&b.set(e.ziel,e.schritte.at(-1)??`startet …`);e[1]=g.data?.laeufe,e[2]=b}else b=e[2];let x,S;e[3]!==r.data?.ziele||e[4]!==a.data?.ziele?(S=[...r.data?.ziele??[],...a.data?.ziele??[]],x=S.reduce(F,0),e[3]=r.data?.ziele,e[4]=a.data?.ziele,e[5]=x,e[6]=S):(x=e[5],S=e[6]);let C=x,w=a.data?.bericht,E;e[7]===C?E=e[8]:(E=C>0?(0,y.jsxs)(u,{art:`cyan`,children:[C,` `,C===1?`Update`:`Updates`,` bereit`]}):(0,y.jsx)(u,{art:`gruen`,children:`aktuell`}),e[7]=C,e[8]=E);let D;e[9]===Symbol.for(`react.memo_cache_sentinel`)?(D=(0,y.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Die KI-Box und das Homelab an einem Ort. Updates startest du nur hier per Knopf; jedes fragt vorher und sagt, welcher Rückweg bereitsteht.`}),e[9]=D):D=e[9];let O;e[10]!==i||e[11]!==a.data?(O=i&&a.data&&!a.data.ausfuehrer.verbunden&&(0,y.jsxs)(`p`,{role:`alert`,className:`text-[15px] text-bernstein`,children:[`Der Ausführer auf dem Proxmox-Host meldet sich nicht`,a.data.ausfuehrer.zuletzt?` (zuletzt ${s(new Date(a.data.ausfuehrer.zuletzt*1e3).toISOString())})`:``,`. Die Homelab-Geräte zeigen den letzten bekannten Stand.`]}),e[10]=i,e[11]=a.data,e[12]=O):O=e[12];let k;e[13]!==i||e[14]!==w||e[15]!==a.data?(k=i&&a.data&&!w&&(0,y.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Der Ausführer auf dem Proxmox-Host hat noch keinen Bericht geschickt.`}),e[13]=i,e[14]=w,e[15]=a.data,e[16]=k):k=e[16];let M;e[17]!==r.error||e[18]!==r.isError?(M=r.isError&&(0,y.jsxs)(`p`,{role:`alert`,className:`text-sm text-rot-text`,children:[`Die KI-Box antwortet gerade nicht: `,r.error.message]}),e[17]=r.error,e[18]=r.isError,e[19]=M):M=e[19];let N;e[20]!==a.error||e[21]!==a.isError?(N=a.isError&&(0,y.jsxs)(`p`,{role:`alert`,className:`text-sm text-rot-text`,children:[`Der Homelab-Teil antwortet gerade nicht: `,a.error.message]}),e[20]=a.error,e[21]=a.isError,e[22]=N):N=e[22];let P;e[23]!==E||e[24]!==O||e[25]!==k||e[26]!==M||e[27]!==N?(P=(0,y.jsxs)(p,{titel:`Alle Geräte`,rechts:E,children:[D,O,k,M,N]}),e[23]=E,e[24]=O,e[25]=k,e[26]=M,e[27]=N,e[28]=P):P=e[28];let I;e[29]!==v||e[30]!==b||e[31]!==S?(I=S.length>0&&(0,y.jsx)(`div`,{className:`grid gap-5 md:gap-6 xl:grid-cols-2`,children:S.map(e=>(0,y.jsx)(T,{ziel:e,laeuft:b.get(e.id)??null,gesperrt:v.isPending,onAktion:e=>v.mutate(e)},e.id))}),e[29]=v,e[30]=b,e[31]=S,e[32]=I):I=e[32];let L;e[33]!==i||e[34]!==n?(L=!i&&n.data&&(0,y.jsx)(A,{partner:n.data}),e[33]=i,e[34]=n,e[35]=L):L=e[35];let R;e[36]!==i||e[37]!==g?(R=i&&(0,y.jsx)(j,{laeufe:g.data?.laeufe??[]}),e[36]=i,e[37]=g,e[38]=R):R=e[38];let z;return e[39]!==R||e[40]!==P||e[41]!==I||e[42]!==L?(z=(0,y.jsxs)(y.Fragment,{children:[P,I,L,R]}),e[39]=R,e[40]=P,e[41]=I,e[42]=L,e[43]=z):z=e[43],z}function F(e,t){return e+t.offen}export{P as HomelabSeite}; \ No newline at end of file diff --git a/frontend/dist/assets/MehrMenue-Bb-CQEnN.js b/frontend/dist/assets/MehrMenue-Bb-CQEnN.js deleted file mode 100644 index 23f83b1..0000000 --- a/frontend/dist/assets/MehrMenue-Bb-CQEnN.js +++ /dev/null @@ -1 +0,0 @@ -import{n as e,r as t}from"./dist-BaE5-hhJ.js";import{G as n,K as r,q as i}from"./index-DSxwfDVg.js";import{a,i as o,n as s,r as c,t as l}from"./sheet-iGGgLZLg.js";var u=e(),d=t();function f(e){let t=(0,u.c)(16),{onDienste:f,onEinstellungen:p,onSchliessen:m}=e,h;t[0]===m?h=t[1]:(h=e=>!e&&m(),t[0]=m,t[1]=h);let g;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(g=(0,d.jsxs)(o,{children:[(0,d.jsx)(a,{className:`schild text-lg`,children:`Mehr`}),(0,d.jsx)(c,{className:`sr-only`,children:`Weitere Bereiche`})]}),t[2]=g):g=t[2];let _;t[3]===Symbol.for(`react.memo_cache_sentinel`)?(_=(0,d.jsxs)(`a`,{href:`/hermes-ui/`,target:`_blank`,rel:`noopener`,className:`flex h-12 items-center gap-3 rounded-lg border border-linie px-4 text-base`,children:[(0,d.jsx)(i,{className:`size-5`,"aria-hidden":!0}),` Hermes-Dashboard öffnen`]}),t[3]=_):_=t[3];let v;t[4]===Symbol.for(`react.memo_cache_sentinel`)?(v=(0,d.jsx)(r,{className:`size-5`,"aria-hidden":!0}),t[4]=v):v=t[4];let y;t[5]===f?y=t[6]:(y=(0,d.jsxs)(`button`,{type:`button`,onClick:f,className:`flex h-12 items-center gap-3 rounded-lg border border-linie px-4 text-left text-base`,children:[v,` Dienste und Protokolle`]}),t[5]=f,t[6]=y);let b;t[7]===Symbol.for(`react.memo_cache_sentinel`)?(b=(0,d.jsx)(n,{className:`size-5`,"aria-hidden":!0}),t[7]=b):b=t[7];let x;t[8]===p?x=t[9]:(x=(0,d.jsxs)(`button`,{type:`button`,onClick:p,className:`flex h-12 items-center gap-3 rounded-lg border border-linie px-4 text-left text-base`,children:[b,` Einstellungen`]}),t[8]=p,t[9]=x);let S;t[10]!==y||t[11]!==x?(S=(0,d.jsxs)(s,{side:`bottom`,className:`border-linie bg-panel pb-[calc(1rem+env(safe-area-inset-bottom))]`,children:[g,(0,d.jsxs)(`div`,{className:`flex flex-col gap-2 px-4`,children:[_,y,x]})]}),t[10]=y,t[11]=x,t[12]=S):S=t[12];let C;return t[13]!==h||t[14]!==S?(C=(0,d.jsx)(l,{open:!0,onOpenChange:h,children:S}),t[13]=h,t[14]=S,t[15]=C):C=t[15],C}export{f as MehrMenue}; \ No newline at end of file diff --git a/frontend/dist/assets/MehrMenue-Bk1bLJ2A.js b/frontend/dist/assets/MehrMenue-Bk1bLJ2A.js new file mode 100644 index 0000000..323e87f --- /dev/null +++ b/frontend/dist/assets/MehrMenue-Bk1bLJ2A.js @@ -0,0 +1 @@ +import{c as e,s as t}from"./button-BheHBEml.js";import{G as n,K as r,q as i}from"./index-SRwQwEnh.js";import{a,i as o,n as s,r as c,t as l}from"./sheet-CKp82pOA.js";var u=t(),d=e();function f(e){let t=(0,u.c)(16),{onDienste:f,onEinstellungen:p,onSchliessen:m}=e,h;t[0]===m?h=t[1]:(h=e=>!e&&m(),t[0]=m,t[1]=h);let g;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(g=(0,d.jsxs)(o,{children:[(0,d.jsx)(a,{className:`schild text-lg`,children:`Mehr`}),(0,d.jsx)(c,{className:`sr-only`,children:`Weitere Bereiche`})]}),t[2]=g):g=t[2];let _;t[3]===Symbol.for(`react.memo_cache_sentinel`)?(_=(0,d.jsxs)(`a`,{href:`/hermes-ui/`,target:`_blank`,rel:`noopener`,className:`flex h-12 items-center gap-3 rounded-lg border border-linie px-4 text-base`,children:[(0,d.jsx)(i,{className:`size-5`,"aria-hidden":!0}),` Hermes-Dashboard öffnen`]}),t[3]=_):_=t[3];let v;t[4]===Symbol.for(`react.memo_cache_sentinel`)?(v=(0,d.jsx)(r,{className:`size-5`,"aria-hidden":!0}),t[4]=v):v=t[4];let y;t[5]===f?y=t[6]:(y=(0,d.jsxs)(`button`,{type:`button`,onClick:f,className:`flex h-12 items-center gap-3 rounded-lg border border-linie px-4 text-left text-base`,children:[v,` Dienste und Protokolle`]}),t[5]=f,t[6]=y);let b;t[7]===Symbol.for(`react.memo_cache_sentinel`)?(b=(0,d.jsx)(n,{className:`size-5`,"aria-hidden":!0}),t[7]=b):b=t[7];let x;t[8]===p?x=t[9]:(x=(0,d.jsxs)(`button`,{type:`button`,onClick:p,className:`flex h-12 items-center gap-3 rounded-lg border border-linie px-4 text-left text-base`,children:[b,` Einstellungen`]}),t[8]=p,t[9]=x);let S;t[10]!==y||t[11]!==x?(S=(0,d.jsxs)(s,{side:`bottom`,className:`border-linie bg-panel pb-[calc(1rem+env(safe-area-inset-bottom))]`,children:[g,(0,d.jsxs)(`div`,{className:`flex flex-col gap-2 px-4`,children:[_,y,x]})]}),t[10]=y,t[11]=x,t[12]=S):S=t[12];let C;return t[13]!==h||t[14]!==S?(C=(0,d.jsx)(l,{open:!0,onOpenChange:h,children:S}),t[13]=h,t[14]=S,t[15]=C):C=t[15],C}export{f as MehrMenue}; \ No newline at end of file diff --git a/frontend/dist/assets/Modelle-CfdYZ2Ik.js b/frontend/dist/assets/Modelle-CfdYZ2Ik.js deleted file mode 100644 index a61fc3c..0000000 --- a/frontend/dist/assets/Modelle-CfdYZ2Ik.js +++ /dev/null @@ -1 +0,0 @@ -import{i as e,n as t,o as n,r}from"./dist-BaE5-hhJ.js";import{A as i,D as a,F as o,I as s,J as c,L as l,O as u,P as d,Q as f,S as p,U as m,X as h,Y as g,Z as _,a as v,b as y,c as b,f as x,h as S,i as C,j as w,k as T,m as E,n as D,p as O,r as k,t as A,v as j,y as M,z as N}from"./index-DSxwfDVg.js";import{a as P,i as F,n as I,r as L,t as R}from"./sheet-iGGgLZLg.js";import{Bestaetigen as z}from"./Bestaetigen-kFure1oW.js";import{o as ee,r as te,t as B}from"./Auftraege-C5bKfsD1.js";function V(e){let t=_();return h(t.stores.location,g(e,t))}var H={name:`search`,size:24,node:[[`path`,{d:`m21 21-4.34-4.34`,key:`14j7rj`}],[`circle`,{cx:`11`,cy:`11`,r:`8`,key:`4ej97u`}]]};H.node;var U=c(H),W=t(),G=n(e(),1),K=r(),ne={coder:`Coder`};function re(e){let t=(0,W.c)(102),{offen:n,onSchliessen:r,rollenJetzt:a}=e,o=b(),[s,c]=(0,G.useState)(``),[u,d]=(0,G.useState)(``),[p,m]=(0,G.useState)(null),[h,g]=(0,G.useState)(``),[_,v]=(0,G.useState)(null),y=(0,G.useRef)(!1),x,S;t[0]===u?(x=t[1],S=t[2]):(x=[`hf-suche`,u],S=()=>l(`/api/hf/search?q=${encodeURIComponent(u)}`),t[0]=u,t[1]=x,t[2]=S);let C=u.length>1,w;t[3]!==x||t[4]!==S||t[5]!==C?(w={queryKey:x,queryFn:S,enabled:C},t[3]=x,t[4]=S,t[5]=C,t[6]=w):w=t[6];let E=f(w),D,O;t[7]===p?(D=t[8],O=t[9]):(D=[`hf-quants`,p],O=()=>l(`/api/hf/quants?repo=${encodeURIComponent(p??``)}`),t[7]=p,t[8]=D,t[9]=O);let k=!!p,A;t[10]!==D||t[11]!==O||t[12]!==k?(A={queryKey:D,queryFn:O,enabled:k},t[10]=D,t[11]=O,t[12]=k,t[13]=A):A=t[13];let j=f(A),M,B,V,H,U,re,q,J,Y,X,Z,Q,oe;if(t[14]!==_||t[15]!==s||t[16]!==o||t[17]!==n||t[18]!==r||t[19]!==j.data||t[20]!==j.error||t[21]!==j.isError||t[22]!==j.isFetching||t[23]!==p||t[24]!==h||t[25]!==a||t[26]!==E){let e=(j.data?.quants??[]).map(ae),l=h?ne[h]??h:null,u;t[40]!==p||t[41]!==h||t[42]!==a||t[43]!==l?(u=l&&p?ee(p,l,a[h]):null,t[40]=p,t[41]=h,t[42]=a,t[43]=l,t[44]=u):u=t[44],V=u;let f;t[45]!==_||t[46]!==o||t[47]!==r||t[48]!==p||t[49]!==h?(f=function(){p&&_&&!y.current&&(y.current=!0,o.mutate({repo:p,quant:_.quant,rolle:h||null},{onSuccess:e=>{v(null),e.ok!==!1&&r()},onError:()=>v(null),onSettled:()=>{y.current=!1}}))},t[45]=_,t[46]=o,t[47]=r,t[48]=p,t[49]=h,t[50]=f):f=t[50],H=f,B=R,Z=n,t[51]===r?Q=t[52]:(Q=e=>!e&&r(),t[51]=r,t[52]=Q),M=I,q=`right`,J=`gap-3 overflow-y-auto border-linie bg-panel data-[side=right]:w-full data-[side=right]:sm:max-w-xl`,t[53]===Symbol.for(`react.memo_cache_sentinel`)?(Y=(0,K.jsxs)(F,{children:[(0,K.jsx)(P,{className:`schild text-lg`,children:`Modelle selbst suchen`}),(0,K.jsx)(L,{className:`text-text-2`,children:`Auf Hugging Face suchen, Quantisierung wählen, installieren.`})]}),t[53]=Y):Y=t[53];let b;t[54]===s?b=t[55]:(b=e=>{e.preventDefault(),m(null),d(s.trim())},t[54]=s,t[55]=b);let x;t[56]===Symbol.for(`react.memo_cache_sentinel`)?(x=(0,K.jsx)(`label`,{htmlFor:`hf-suche`,className:`sr-only`,children:`Suchbegriff`}),t[56]=x):x=t[56];let S;t[57]===Symbol.for(`react.memo_cache_sentinel`)?(S=e=>c(e.target.value),t[57]=S):S=t[57];let C;t[58]===s?C=t[59]:(C=(0,K.jsx)(`input`,{id:`hf-suche`,value:s,onChange:S,placeholder:`z. B. Qwen3.8 GGUF`,className:`h-11 min-w-0 flex-1 rounded-lg border border-linie-stark bg-erhaben px-3 text-[15px] outline-none focus:border-cyan`}),t[58]=s,t[59]=C);let w;t[60]===Symbol.for(`react.memo_cache_sentinel`)?(w=(0,K.jsx)(N,{type:`submit`,children:`Suchen`}),t[60]=w):w=t[60],t[61]!==b||t[62]!==C?(X=(0,K.jsxs)(`form`,{className:`flex gap-2 px-4`,onSubmit:b,children:[x,C,w]}),t[61]=b,t[62]=C,t[63]=X):X=t[63],oe=`flex flex-col px-4 pb-4`,t[64]!==p||t[65]!==E?(U=!p&&(0,K.jsxs)(K.Fragment,{children:[E.isFetching&&(0,K.jsx)(`p`,{role:`status`,className:`py-3 text-sm text-text-2`,children:`Suche läuft …`}),!E.isFetching&&E.isError&&(0,K.jsxs)(`p`,{role:`alert`,className:`py-3 text-sm text-rot-text`,children:[`Die Suche ging nicht: `,E.error.message]}),!E.isFetching&&E.data?.results.length===0&&(0,K.jsx)(`p`,{className:`py-3 text-sm text-text-2`,children:`Keine Treffer. Versuch einen anderen Suchbegriff — oder Hugging Face ist gerade nicht erreichbar.`}),E.data?.results.map(e=>(0,K.jsxs)(`button`,{type:`button`,onClick:()=>m(e.repo),className:`flex min-h-11 items-center justify-between gap-3 border-t border-linie py-2 text-left hover:text-cyan`,children:[(0,K.jsx)(`span`,{className:`ziffern min-w-0 text-sm break-all`,children:e.repo}),(0,K.jsxs)(`span`,{className:`ziffern shrink-0 text-xs text-text-3`,children:[i(e.downloads),` ↓`]})]},e.repo))]}),t[64]=p,t[65]=E,t[66]=U):U=t[66],re=p&&(0,K.jsxs)(`div`,{className:`flex flex-col gap-3 pt-2`,children:[(0,K.jsxs)(`div`,{className:`flex items-center justify-between gap-2`,children:[(0,K.jsx)(`span`,{className:`ziffern min-w-0 text-sm break-all`,children:p}),(0,K.jsx)(N,{variant:`ghost`,size:`sm`,onClick:()=>m(null),children:`Zurück`})]}),(0,K.jsxs)(`label`,{className:`flex flex-wrap items-center gap-3 text-sm text-text-2`,children:[`Rolle`,(0,K.jsxs)(`select`,{value:h,onChange:e=>g(e.target.value),className:`h-11 min-w-0 rounded-lg border border-linie-stark bg-erhaben px-3 text-foreground`,children:[(0,K.jsx)(`option`,{value:``,children:`ohne Rolle (nur installieren)`}),Object.entries(ne).map(ie)]})]}),V&&(0,K.jsx)(`p`,{className:`m-0 rounded-lg border border-bernstein/60 bg-bernstein-grund px-3 py-2.5 text-sm leading-relaxed text-bernstein`,children:V}),j.isFetching&&(0,K.jsx)(`p`,{role:`status`,className:`text-sm text-text-2`,children:`Quantisierungen werden gelesen …`}),!j.isFetching&&j.isError&&(0,K.jsxs)(`p`,{role:`alert`,className:`text-sm text-rot-text`,children:[`Die Dateien dieses Modells ließen sich nicht lesen: `,j.error.message]}),!j.isFetching&&j.data&&e.length===0&&(0,K.jsx)(`p`,{className:`text-sm text-text-2`,children:`Hier gibt es keine GGUF-Dateien, die sich installieren lassen.`}),e.map(e=>{let t=e.quant??e.name??`?`,n=e.total_bytes??e.size_bytes;return(0,K.jsxs)(`div`,{className:`flex items-center justify-between gap-3 border-t border-linie py-2`,children:[(0,K.jsx)(`span`,{className:`ziffern text-sm`,children:t}),(0,K.jsxs)(`span`,{className:`flex items-center gap-3`,children:[n?(0,K.jsx)(`span`,{className:`ziffern text-xs text-text-3`,children:T(n,1)}):null,(0,K.jsx)(N,{variant:`outline`,size:`sm`,disabled:o.isPending,onClick:()=>v({quant:t,bytes:n}),children:`Installieren`})]})]},t)})]}),t[14]=_,t[15]=s,t[16]=o,t[17]=n,t[18]=r,t[19]=j.data,t[20]=j.error,t[21]=j.isError,t[22]=j.isFetching,t[23]=p,t[24]=h,t[25]=a,t[26]=E,t[27]=M,t[28]=B,t[29]=V,t[30]=H,t[31]=U,t[32]=re,t[33]=q,t[34]=J,t[35]=Y,t[36]=X,t[37]=Z,t[38]=Q,t[39]=oe}else M=t[27],B=t[28],V=t[29],H=t[30],U=t[31],re=t[32],q=t[33],J=t[34],Y=t[35],X=t[36],Z=t[37],Q=t[38],oe=t[39];let se;t[67]!==U||t[68]!==re||t[69]!==oe?(se=(0,K.jsxs)(`div`,{className:oe,children:[U,re]}),t[67]=U,t[68]=re,t[69]=oe,t[70]=se):se=t[70];let ce=_!==null,le;t[71]!==_||t[72]!==p?(le=_&&p?`${p} (${_.quant}${_.bytes?`, ${T(_.bytes,1)}`:``}) wird heruntergeladen und in llama-swap eingetragen.`+(_.bytes?``:` Die Größe siehst du beim Auftrag, sobald der Download läuft.`):``,t[71]=_,t[72]=p,t[73]=le):le=t[73];let ue=o.isPending?`Wird gestartet …`:`Installieren`,de=o.isPending,fe;t[74]===Symbol.for(`react.memo_cache_sentinel`)?(fe=()=>v(null),t[74]=fe):fe=t[74];let pe;t[75]===V?pe=t[76]:(pe=V&&(0,K.jsx)(`p`,{className:`m-0 rounded-lg border border-bernstein/60 bg-bernstein-grund px-3 py-2.5 text-[15px] leading-relaxed text-bernstein`,children:V}),t[75]=V,t[76]=pe);let me=!!h,he;t[77]===me?he=t[78]:(he=te(me),t[77]=me,t[78]=he);let $;t[79]===he?$=t[80]:($=(0,K.jsx)(`p`,{className:`m-0 text-[15px] leading-relaxed text-text-2`,children:he}),t[79]=he,t[80]=$);let ge;t[81]!==o.isPending||t[82]!==H||t[83]!==ce||t[84]!==le||t[85]!==ue||t[86]!==pe||t[87]!==$?(ge=(0,K.jsxs)(z,{offen:ce,titel:`Modell installieren?`,text:le,knopf:ue,gesperrt:de,onNein:fe,onJa:H,children:[pe,$]}),t[81]=o.isPending,t[82]=H,t[83]=ce,t[84]=le,t[85]=ue,t[86]=pe,t[87]=$,t[88]=ge):ge=t[88];let _e;t[89]!==M||t[90]!==q||t[91]!==J||t[92]!==Y||t[93]!==X||t[94]!==se||t[95]!==ge?(_e=(0,K.jsxs)(M,{side:q,className:J,children:[Y,X,se,ge]}),t[89]=M,t[90]=q,t[91]=J,t[92]=Y,t[93]=X,t[94]=se,t[95]=ge,t[96]=_e):_e=t[96];let ve;return t[97]!==B||t[98]!==Z||t[99]!==Q||t[100]!==_e?(ve=(0,K.jsx)(B,{open:Z,onOpenChange:Q,children:_e}),t[97]=B,t[98]=Z,t[99]=Q,t[100]=_e,t[101]=ve):ve=t[101],ve}function ie(e){let[t,n]=e;return(0,K.jsx)(`option`,{value:t,children:n},t)}function ae(e){return typeof e==`string`?{quant:e}:e}var q=115;function J(e){let t=(0,W.c)(55),{modelle:n,laufend:r}=e,i=m(),a=p(),o,s,c,l,d,f,h,g,_,v;if(t[0]!==r||t[1]!==i?.ram_total||t[2]!==i?.ram_used||t[3]!==n||t[4]!==a){let e=u(i?.ram_total??a.data?.box.ram_total)??122.7,p=u(i?.ram_used??a.data?.box.ram_used)??0,m;t[15]===r?m=t[16]:(m=e=>r.includes(e.name),t[15]=r,t[16]=m);let y=n.filter(m).reduce(Y,0);s=Math.max(0,p-y);let b=t=>`${Math.min(100,t/e*100)}%`;o=A,g=`Speicher`,_=(0,K.jsxs)(`span`,{className:`ziffern text-sm text-text-2`,children:[Math.round(p),` von `,Math.round(e),` GB belegt · Grenze ~`,q,` GB`]});let x=`Speicher: ${Math.round(p)} von ${Math.round(e)} GB belegt`,S=b(p),C;t[17]===S?C=t[18]:(C={width:S},t[17]=S,t[18]=C);let w=`${p?y/p*100:0}%`,T;t[19]===w?T=t[20]:(T=(0,K.jsx)(`div`,{className:`h-full bg-cyan`,style:{width:w}}),t[19]=w,t[20]=T);let E=`${p?s/p*100:0}%`,D;t[21]===E?D=t[22]:(D=(0,K.jsx)(`div`,{className:`h-full bg-cyan/35`,style:{width:E}}),t[21]=E,t[22]=D);let O;t[23]!==C||t[24]!==T||t[25]!==D?(O=(0,K.jsxs)(`div`,{className:`absolute inset-y-0 left-0 flex overflow-hidden rounded-lg`,style:C,children:[T,D]}),t[23]=C,t[24]=T,t[25]=D,t[26]=O):O=t[26];let k=b(q),j;t[27]===k?j=t[28]:(j=(0,K.jsx)(`div`,{className:`absolute -top-1.5 -bottom-1.5 w-0.5 bg-bernstein`,style:{left:k}}),t[27]=k,t[28]=j),t[29]!==x||t[30]!==O||t[31]!==j?(v=(0,K.jsxs)(`div`,{className:`relative h-8 rounded-lg bg-erhaben`,role:`img`,"aria-label":x,children:[O,j]}),t[29]=x,t[30]=O,t[31]=j,t[32]=v):v=t[32],h=`flex flex-wrap gap-x-6 gap-y-2 text-sm text-text-2`,c=`flex items-center gap-2`,t[33]===Symbol.for(`react.memo_cache_sentinel`)?(l=(0,K.jsx)(`span`,{className:`size-2.5 rounded-sm bg-cyan`}),t[33]=l):l=t[33],d=` Geladene Modelle: `,f=Math.round(y),t[0]=r,t[1]=i?.ram_total,t[2]=i?.ram_used,t[3]=n,t[4]=a,t[5]=o,t[6]=s,t[7]=c,t[8]=l,t[9]=d,t[10]=f,t[11]=h,t[12]=g,t[13]=_,t[14]=v}else o=t[5],s=t[6],c=t[7],l=t[8],d=t[9],f=t[10],h=t[11],g=t[12],_=t[13],v=t[14];let y;t[34]!==c||t[35]!==l||t[36]!==d||t[37]!==f?(y=(0,K.jsxs)(`span`,{className:c,children:[l,d,f,` GB Dateien`]}),t[34]=c,t[35]=l,t[36]=d,t[37]=f,t[38]=y):y=t[38];let b;t[39]===Symbol.for(`react.memo_cache_sentinel`)?(b=(0,K.jsx)(`span`,{className:`size-2.5 rounded-sm bg-cyan/35`}),t[39]=b):b=t[39];let x;t[40]===s?x=t[41]:(x=Math.round(s),t[40]=s,t[41]=x);let S;t[42]===x?S=t[43]:(S=(0,K.jsxs)(`span`,{className:`flex items-center gap-2`,children:[b,` Cache, Dienste, System: `,x,` GB`]}),t[42]=x,t[43]=S);let C;t[44]===Symbol.for(`react.memo_cache_sentinel`)?(C=(0,K.jsxs)(`span`,{className:`flex items-center gap-2`,children:[(0,K.jsx)(`span`,{className:`h-3 w-0.5 bg-bernstein`}),` Ab hier wird es eng`]}),t[44]=C):C=t[44];let w;t[45]!==S||t[46]!==h||t[47]!==y?(w=(0,K.jsxs)(`div`,{className:h,children:[y,S,C]}),t[45]=S,t[46]=h,t[47]=y,t[48]=w):w=t[48];let T;return t[49]!==o||t[50]!==w||t[51]!==g||t[52]!==_||t[53]!==v?(T=(0,K.jsxs)(o,{titel:g,rechts:_,children:[v,w]}),t[49]=o,t[50]=w,t[51]=g,t[52]=_,t[53]=v,t[54]=T):T=t[54],T}function Y(e,t){return e+(u(t.size_bytes)??0)}function X(e){let t=(0,W.c)(7),{name:n,wert:r}=e,i;t[0]===n?i=t[1]:(i=(0,K.jsx)(`dt`,{className:`text-xs text-text-3`,children:n}),t[0]=n,t[1]=i);let a;t[2]===r?a=t[3]:(a=(0,K.jsx)(`dd`,{className:`ziffern m-0 text-lg`,children:r}),t[2]=r,t[3]=a);let o;return t[4]!==i||t[5]!==a?(o=(0,K.jsxs)(`div`,{className:`flex flex-col gap-1`,children:[i,a]}),t[4]=i,t[5]=a,t[6]=o):o=t[6],o}function Z(e){let t=(0,W.c)(48),{titel:n,modell:r,zwilling:i,laeuft:a,nutzer:o}=e,s=O();if(!r){let e;t[0]===n?e=t[1]:(e=(0,K.jsx)(`span`,{className:`schild text-sm text-text-3`,children:n}),t[0]=n,t[1]=e);let r;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(r=(0,K.jsx)(D,{children:`Frei`}),t[2]=r):r=t[2];let i;t[3]===e?i=t[4]:(i=(0,K.jsxs)(`div`,{className:`flex items-center justify-between`,children:[e,r]}),t[3]=e,t[4]=i);let a,o;t[5]===Symbol.for(`react.memo_cache_sentinel`)?(a=(0,K.jsx)(`h3`,{className:`font-anzeige text-xl font-semibold text-text-2`,children:`Nicht belegt`}),o=(0,K.jsx)(`p`,{className:`text-sm text-text-2`,children:`Nur belegen, wenn ein Modell hier echten Mehrwert bringt. Das Radar meldet Kandidaten.`}),t[5]=a,t[6]=o):(a=t[5],o=t[6]);let s;return t[7]===i?s=t[8]:(s=(0,K.jsxs)(`article`,{className:`flex flex-col gap-3.5 rounded-2xl border border-dashed border-linie-stark p-5`,children:[i,a,o]}),t[7]=i,t[8]=s),s}let c;t[9]!==r.ctx||t[10]!==r.parallel_slots?(c=r.ctx?Math.round(r.ctx/Math.max(1,r.parallel_slots)/1024):null,t[9]=r.ctx,t[10]=r.parallel_slots,t[11]=c):c=t[11];let l=c,u=!!r.capabilities?.vision||!!i,d;t[12]===n?d=t[13]:(d=(0,K.jsx)(`span`,{className:`schild text-sm text-text-3`,children:n}),t[12]=n,t[13]=d);let f=a?`gruen`:`grau`,p=a?`Geladen`:`Auf Abruf`,m;t[14]!==f||t[15]!==p?(m=(0,K.jsx)(D,{art:f,children:p}),t[14]=f,t[15]=p,t[16]=m):m=t[16];let h;t[17]!==d||t[18]!==m?(h=(0,K.jsxs)(`div`,{className:`flex items-center justify-between gap-2`,children:[d,m]}),t[17]=d,t[18]=m,t[19]=h):h=t[19];let g;t[20]===r.name?g=t[21]:(g=(0,K.jsx)(`h3`,{className:`ziffern m-0 text-lg font-medium break-all`,children:r.name}),t[20]=r.name,t[21]=g);let _;t[22]===r.size_bytes?_=t[23]:(_=T(r.size_bytes),t[22]=r.size_bytes,t[23]=_);let v;t[24]===_?v=t[25]:(v=(0,K.jsx)(X,{name:`Größe`,wert:_}),t[24]=_,t[25]=v);let y=l?`${l}k`:`–`,b;t[26]===y?b=t[27]:(b=(0,K.jsx)(X,{name:`Kontext`,wert:y}),t[26]=y,t[27]=b);let x=u?`ja`:`nein`,S;t[28]===x?S=t[29]:(S=(0,K.jsx)(X,{name:`Bilder`,wert:x}),t[28]=x,t[29]=S);let C;t[30]!==b||t[31]!==S||t[32]!==v?(C=(0,K.jsxs)(`dl`,{className:`m-0 grid grid-cols-3 gap-3`,children:[v,b,S]}),t[30]=b,t[31]=S,t[32]=v,t[33]=C):C=t[33];let w=r.spec_active?` Mit Draft-Beschleunigung.`:``,E=i?` Bilder sieht es über seinen Bild-Zwilling, der nur bei Bedarf lädt.`:``,k;t[34]!==o||t[35]!==w||t[36]!==E?(k=(0,K.jsxs)(`p`,{className:`text-sm text-text-2`,children:[`Genutzt von `,o,`.`,w,E]}),t[34]=o,t[35]=w,t[36]=E,t[37]=k):k=t[37];let A;t[38]!==s||t[39]!==a||t[40]!==r.name?(A=!a&&(0,K.jsx)(N,{variant:`outline`,size:`sm`,className:`mt-auto self-start`,onClick:()=>s.mutate(r.name),disabled:s.isPending,children:`Jetzt laden`}),t[38]=s,t[39]=a,t[40]=r.name,t[41]=A):A=t[41];let j;return t[42]!==C||t[43]!==k||t[44]!==A||t[45]!==h||t[46]!==g?(j=(0,K.jsxs)(`article`,{className:`flex flex-col gap-3.5 rounded-2xl border border-linie bg-panel p-5`,children:[h,g,C,k,A]}),t[42]=C,t[43]=k,t[44]=A,t[45]=h,t[46]=g,t[47]=j):j=t[47],j}function Q(e){let t=(0,W.c)(54),{nutzung:n,laedt:r,fehler:a}=e;if(!n){let e;return t[0]!==a||t[1]!==r?(e=(0,K.jsx)(A,{titel:`Wer nutzt die Modelle`,children:r?(0,K.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Wird gelesen …`}):(0,K.jsxs)(`p`,{role:`alert`,className:`text-[15px] text-rot-text`,children:[`Die Nutzung lässt sich gerade nicht lesen`,a?`: ${a}`:`.`]})}),t[0]=a,t[1]=r,t[2]=e):e=t[2],e}let o;t[3]===n.absender?o=t[4]:(o=Math.max(1,...n.absender.map(le)),t[3]=n.absender,t[4]=o);let s=o,c,l,u,d,f,p,m,h,g,_,v,y;if(t[5]!==s||t[6]!==n.absender||t[7]!==n.letzte_24h||t[8]!==n.tage){let e=n.letzte_24h.map(se),r=Math.max(1,...e);c=A,y=`Wer nutzt die Modelle`,t[21]===n.tage?l=t[22]:(l=(0,K.jsxs)(`span`,{className:`ziffern text-xs text-text-3`,children:[`letzte `,n.tage,` Tage`]}),t[21]=n.tage,t[22]=l),t[23]===n.absender.length?u=t[24]:(u=n.absender.length===0&&(0,K.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Keine Anfragen aufgezeichnet.`}),t[23]=n.absender.length,t[24]=u);let a;if(t[25]!==s||t[26]!==n.absender){let e;t[28]===s?e=t[29]:(e=e=>(0,K.jsxs)(`li`,{className:`flex flex-col gap-1.5`,children:[(0,K.jsxs)(`div`,{className:`flex justify-between gap-3 text-sm`,children:[(0,K.jsx)(`span`,{children:e.name}),(0,K.jsx)(`span`,{className:`ziffern`,children:i(e.anfragen)})]}),(0,K.jsx)(`div`,{className:`h-2 rounded-full bg-erhaben`,children:(0,K.jsx)(`div`,{className:`h-2 rounded-full bg-cyan`,style:{width:`${e.anfragen/s*100}%`}})})]},e.name),t[28]=s,t[29]=e),a=n.absender.map(e),t[25]=s,t[26]=n.absender,t[27]=a}else a=t[27];t[30]===a?d=t[31]:(d=(0,K.jsx)(`ul`,{className:`m-0 flex list-none flex-col gap-3.5 p-0`,children:a}),t[30]=a,t[31]=d),_=`flex flex-col gap-1.5`,t[32]===Symbol.for(`react.memo_cache_sentinel`)?(v=(0,K.jsx)(`span`,{className:`schild text-xs text-text-3`,children:`Anfragen je Stunde, letzte 24 h`}),t[32]=v):v=t[32],f=`0 0 240 64`,p=`h-16 w-full`,m=`img`,h=`Anfragen je Stunde in den letzten 24 Stunden`;let o;t[33]===r?o=t[34]:(o=(e,t)=>{let n=e?Math.max(3,e/r*50):2;return(0,K.jsx)(`rect`,{x:t*10+1.5,y:52-n,width:7,height:n,rx:1.5,fill:e?`var(--cyan)`:`var(--linie)`},t)},t[33]=r,t[34]=o),g=e.map(o),t[5]=s,t[6]=n.absender,t[7]=n.letzte_24h,t[8]=n.tage,t[9]=c,t[10]=l,t[11]=u,t[12]=d,t[13]=f,t[14]=p,t[15]=m,t[16]=h,t[17]=g,t[18]=_,t[19]=v,t[20]=y}else c=t[9],l=t[10],u=t[11],d=t[12],f=t[13],p=t[14],m=t[15],h=t[16],g=t[17],_=t[18],v=t[19],y=t[20];let b;t[35]===Symbol.for(`react.memo_cache_sentinel`)?(b=[0,6,12,18].map(oe),t[35]=b):b=t[35];let x;t[36]!==f||t[37]!==p||t[38]!==m||t[39]!==h||t[40]!==g?(x=(0,K.jsxs)(`svg`,{viewBox:f,className:p,role:m,"aria-label":h,children:[g,b]}),t[36]=f,t[37]=p,t[38]=m,t[39]=h,t[40]=g,t[41]=x):x=t[41];let S;t[42]!==x||t[43]!==_||t[44]!==v?(S=(0,K.jsxs)(`div`,{className:_,children:[v,x]}),t[42]=x,t[43]=_,t[44]=v,t[45]=S):S=t[45];let C;t[46]===Symbol.for(`react.memo_cache_sentinel`)?(C=(0,K.jsx)(`p`,{className:`text-xs text-text-3`,children:`Anfragen an llama-swap. NerdQuiz geht direkt dorthin, alles andere über MC2.`}),t[46]=C):C=t[46];let w;return t[47]!==c||t[48]!==l||t[49]!==u||t[50]!==d||t[51]!==S||t[52]!==y?(w=(0,K.jsxs)(c,{titel:y,rechts:l,children:[u,d,S,C]}),t[47]=c,t[48]=l,t[49]=u,t[50]=d,t[51]=S,t[52]=y,t[53]=w):w=t[53],w}function oe(e){return(0,K.jsx)(`text`,{x:e*10+5,y:63,textAnchor:`middle`,style:{fontSize:9,fill:`var(--text-3)`,fontFamily:`var(--font-mono)`},children:e},e)}function se(e){return Object.values(e.je_absender).reduce(ce,0)}function ce(e,t){return e+t}function le(e){return e.anfragen}var ue={bestanden:`gruen`,durchgefallen:`rot`,wartet:`cyan`,neu:`cyan`,getestet:`grau`,uebernommen:`gruen`,verworfen:`grau`},de={besser:`text-gruen`,schlechter:`text-rot-text`,gleich:`text-text-2`};function fe(e){let t=(0,W.c)(4),{test:n}=e,r,i;if(t[0]!==n.vergleich){i=Symbol.for(`react.early_return_sentinel`);bb0:{let e=Object.entries(n.vergleich??{});if(e.length===0){i=null;break bb0}let a;t[3]===Symbol.for(`react.memo_cache_sentinel`)?(a=(0,K.jsxs)(`div`,{role:`row`,className:`schild grid grid-cols-[minmax(0,1fr)_auto_auto] gap-x-4 pb-1 text-xs text-text-3`,children:[(0,K.jsx)(`span`,{role:`columnheader`,children:`Messwert`}),(0,K.jsx)(`span`,{role:`columnheader`,className:`text-right`,children:`Kandidat`}),(0,K.jsx)(`span`,{role:`columnheader`,className:`text-right`,children:`Heute`})]}),t[3]=a):a=t[3],r=(0,K.jsxs)(`div`,{role:`table`,"aria-label":`Messwerte gegen das heutige Modell`,className:`rounded-lg border border-linie bg-erhaben/50 px-3 py-2 text-sm`,children:[a,e.map(pe)]})}t[0]=n.vergleich,t[1]=r,t[2]=i}else r=t[1],i=t[2];return i===Symbol.for(`react.early_return_sentinel`)?r:i}function pe(e){let[t,n]=e;return(0,K.jsxs)(`div`,{role:`row`,className:`grid grid-cols-[minmax(0,1fr)_auto_auto] gap-x-4 border-t border-linie py-1`,children:[(0,K.jsx)(`span`,{role:`rowheader`,className:`text-text-2`,children:t}),(0,K.jsx)(`span`,{role:`cell`,className:`ziffern text-right ${de[n.urteil??``]??`text-text-2`}`,children:d(n.kandidat,n.einheit)}),(0,K.jsx)(`span`,{role:`cell`,className:`ziffern text-right text-text-3`,children:d(n.baseline,n.einheit)})]},t)}function me(e){let t=(0,W.c)(59),{k:n,test:r,heute:i}=e,a=M(),[o,s]=(0,G.useState)(null),c;t[0]===Symbol.for(`react.memo_cache_sentinel`)?(c=[`bestanden`,`wartet`,`neu`,`getestet`],t[0]=c):c=t[0];let l=c.includes(n.status),u=n.rolle===`hirn`,d;t[1]===n.name?d=t[2]:(d=(0,K.jsx)(`span`,{className:`ziffern text-[15px] font-medium`,children:n.name}),t[1]=n.name,t[2]=d);let f=u?`fürs Hirn`:`fürs Coden`,p;t[3]===f?p=t[4]:(p=(0,K.jsx)(D,{art:`cyan`,children:f}),t[3]=f,t[4]=p);let m;t[5]!==n.eng||t[6]!==n.groesse_gb||t[7]!==n.passt?(m=n.groesse_gb!=null&&(0,K.jsx)(D,{art:n.eng?`bernstein`:n.passt?`gruen`:`rot`,children:n.eng?`passt knapp`:n.passt?`passt`:`passt nicht`}),t[5]=n.eng,t[6]=n.groesse_gb,t[7]=n.passt,t[8]=m):m=t[8];let h=ue[n.status]??`grau`,g=w[n.status]??n.status,_;t[9]!==h||t[10]!==g?(_=(0,K.jsx)(D,{art:h,children:g}),t[9]=h,t[10]=g,t[11]=_):_=t[11];let v;t[12]!==d||t[13]!==p||t[14]!==m||t[15]!==_?(v=(0,K.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2`,children:[d,p,m,_]}),t[12]=d,t[13]=p,t[14]=m,t[15]=_,t[16]=v):v=t[16];let y;t[17]===n.groesse_gb?y=t[18]:(y=n.groesse_gb==null?``:`${n.groesse_gb.toLocaleString(`de-DE`)} GB. `,t[17]=n.groesse_gb,t[18]=y);let b;t[19]!==n.begruendung||t[20]!==y?(b=(0,K.jsxs)(`p`,{className:`text-sm text-text-2`,children:[y,n.begruendung]}),t[19]=n.begruendung,t[20]=y,t[21]=b):b=t[21];let x;t[22]!==n.status||t[23]!==r?(x=r&&[`bestanden`,`durchgefallen`].includes(n.status)&&(0,K.jsx)(fe,{test:r}),t[22]=n.status,t[23]=r,t[24]=x):x=t[24];let S;t[25]!==a.isPending||t[26]!==n.status||t[27]!==l?(S=l&&(0,K.jsxs)(`div`,{className:`flex flex-wrap gap-2`,children:[n.status===`bestanden`&&(0,K.jsx)(N,{size:`sm`,disabled:a.isPending,onClick:()=>s(`uebernehmen`),children:`Übernehmen`}),(0,K.jsx)(N,{variant:`outline`,size:`sm`,disabled:a.isPending,onClick:()=>s(`verwerfen`),children:`Verwerfen`})]}),t[25]=a.isPending,t[26]=n.status,t[27]=l,t[28]=S):S=t[28];let C=o===`uebernehmen`,T=`${n.name} übernehmen?`,E=u?`${n.name} wird Lucys Hirn${i?` und löst ${i} ab`:``}. NerdQuiz und OpenChamber nutzen ab dann ebenfalls das neue Modell. Hermes startet dafür kurz neu, Lucy ist etwa eine Minute nicht erreichbar. Das alte Modell bleibt auf der Platte.`:`${n.name} wird der Coder${i?` und löst ${i} ab`:``}. OpenChamber plant und baut ab dann damit. Das alte Modell bleibt auf der Platte.`,O;t[29]===Symbol.for(`react.memo_cache_sentinel`)?(O=()=>s(null),t[29]=O):O=t[29];let k;t[30]!==a||t[31]!==o||t[32]!==n.id||t[33]!==n.name?(k=()=>{o===`uebernehmen`&&(a.mutate({id:n.id,aktion:`uebernehmen`,name:n.name}),s(null))},t[30]=a,t[31]=o,t[32]=n.id,t[33]=n.name,t[34]=k):k=t[34];let A;t[35]!==a.isPending||t[36]!==C||t[37]!==T||t[38]!==E||t[39]!==k?(A=(0,K.jsx)(z,{offen:C,titel:T,text:E,knopf:`Übernehmen`,gesperrt:a.isPending,onNein:O,onJa:k}),t[35]=a.isPending,t[36]=C,t[37]=T,t[38]=E,t[39]=k,t[40]=A):A=t[40];let j=o===`verwerfen`,P=`${n.name} verwerfen?`,F;t[41]===Symbol.for(`react.memo_cache_sentinel`)?(F=()=>s(null),t[41]=F):F=t[41];let I;t[42]!==a||t[43]!==o||t[44]!==n.id||t[45]!==n.name?(I=()=>{o===`verwerfen`&&(a.mutate({id:n.id,aktion:`verwerfen`,name:n.name}),s(null))},t[42]=a,t[43]=o,t[44]=n.id,t[45]=n.name,t[46]=I):I=t[46];let L;t[47]!==a.isPending||t[48]!==j||t[49]!==P||t[50]!==I?(L=(0,K.jsx)(z,{offen:j,gefahr:!0,titel:P,text:`Die heruntergeladenen Dateien werden gelöscht, und das Radar testet dieses Modell nicht noch einmal.`,knopf:`Verwerfen`,gesperrt:a.isPending,onNein:F,onJa:I}),t[47]=a.isPending,t[48]=j,t[49]=P,t[50]=I,t[51]=L):L=t[51];let R;return t[52]!==b||t[53]!==x||t[54]!==S||t[55]!==A||t[56]!==L||t[57]!==v?(R=(0,K.jsxs)(`li`,{className:`flex flex-col gap-2 border-t border-linie py-3.5`,children:[v,b,x,S,A,L]}),t[52]=b,t[53]=x,t[54]=S,t[55]=A,t[56]=L,t[57]=v,t[58]=R):R=t[58],R}function he(e){let t=(0,W.c)(24),{hirn:n,coder:r}=e,i=j(),o=y();if(i.isError&&!i.data){let e=i.error instanceof s&&i.error.status===404,n;return t[0]!==e||t[1]!==i.error?(n=(0,K.jsx)(A,{titel:`Modell-Radar`,id:`radar`,className:`scroll-mt-4`,children:e?(0,K.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Das Radar ist auf dieser Box noch nicht eingerichtet.`}):(0,K.jsxs)(`p`,{role:`alert`,className:`text-[15px] text-rot-text`,children:[`Das Radar antwortet gerade nicht: `,i.error.message]})}),t[0]=e,t[1]=i.error,t[2]=n):n=t[2],n}let c=i.data,l;t[3]===o?l=t[4]:(l=()=>o.mutate(void 0),t[3]=o,t[4]=l);let u=o.isPending?`Sucht …`:`Jetzt suchen`,d;t[5]!==o.isPending||t[6]!==l||t[7]!==u?(d=(0,K.jsx)(N,{variant:`info`,size:`sm`,onClick:l,disabled:o.isPending,children:u}),t[5]=o.isPending,t[6]=l,t[7]=u,t[8]=d):d=t[8];let f;t[9]===c?f=t[10]:(f=c?.naechster_test?` Nächster Test: ${a(c.naechster_test)}.`:``,t[9]=c,t[10]=f);let p;t[11]===f?p=t[12]:(p=(0,K.jsxs)(`p`,{className:`-mt-2 text-sm text-text-2`,children:[`Testet nachts zwischen 00:30 und 02:30 selbst, höchstens einen Kandidaten pro Woche.`,f]}),t[11]=f,t[12]=p);let m;t[13]!==r||t[14]!==c||t[15]!==n?(m=c?c.kandidaten.length===0?(0,K.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Nichts Besseres in Sicht.`}):(0,K.jsx)(`ul`,{className:`m-0 flex list-none flex-col p-0`,children:c.kandidaten.map(e=>(0,K.jsx)(me,{k:e,test:c.tests.find(t=>t.kandidat===e.id),heute:e.rolle===`hirn`?n:r},e.id))}):(0,K.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Wird gelesen …`}),t[13]=r,t[14]=c,t[15]=n,t[16]=m):m=t[16];let h;t[17]===c?h=t[18]:(h=c&&c.tests.length>0&&(0,K.jsxs)(`div`,{className:`flex flex-col gap-2`,children:[(0,K.jsx)(`h3`,{className:`schild text-xs text-text-3`,children:`Letzte Tests`}),c.tests.slice(0,3).map($)]}),t[17]=c,t[18]=h);let g;return t[19]!==d||t[20]!==p||t[21]!==m||t[22]!==h?(g=(0,K.jsxs)(A,{titel:`Modell-Radar`,id:`radar`,className:`scroll-mt-4`,rechts:d,children:[p,m,h]}),t[19]=d,t[20]=p,t[21]=m,t[22]=h,t[23]=g):g=t[23],g}function $(e){return(0,K.jsxs)(`p`,{className:`text-sm text-text-2`,children:[(0,K.jsx)(`span`,{className:`ziffern text-text-3`,children:a(e.datum)}),` · `,e.zusammenfassung]},e.id)}function ge(){let e=(0,W.c)(33),t=C(),n=v(),[r,i]=(0,G.useState)(null),a,o,s,c,l;if(e[0]!==t.data||e[1]!==t.error||e[2]!==t.isError){l=Symbol.for(`react.early_return_sentinel`);bb0:{o=t.data?.kandidaten??[];let n=o.reduce(_e,0);if(t.isError&&!t.data){let n;e[8]===t.error.message?n=e[9]:(n=(0,K.jsx)(A,{titel:`Aufräumen`,children:(0,K.jsxs)(`p`,{role:`alert`,className:`text-[15px] text-rot-text`,children:[`Was sich aufräumen ließe, lässt sich gerade nicht lesen: `,t.error.message]})}),e[8]=t.error.message,e[9]=n),l=n;break bb0}a=A,s=`Aufräumen`,c=o.length>0?(0,K.jsxs)(`span`,{className:`ziffern text-sm text-text-2`,children:[`bis zu `,n.toLocaleString(`de-DE`,{maximumFractionDigits:0}),` GB frei`]}):void 0}e[0]=t.data,e[1]=t.error,e[2]=t.isError,e[3]=a,e[4]=o,e[5]=s,e[6]=c,e[7]=l}else a=e[3],o=e[4],s=e[5],c=e[6],l=e[7];if(l!==Symbol.for(`react.early_return_sentinel`))return l;let u;e[10]===Symbol.for(`react.memo_cache_sentinel`)?(u=(0,K.jsx)(`p`,{className:`-mt-2 text-sm text-text-2`,children:`Modelle und Ordner, die gerade niemand nutzt. Nichts davon wird von selbst gelöscht, das entscheidest du je Eintrag.`}),e[10]=u):u=e[10];let d;e[11]!==o||e[12]!==t.isPending||e[13]!==n.isPending?(d=t.isPending?(0,K.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Wird gelesen …`}):o.length===0?(0,K.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Nichts aufzuräumen.`}):(0,K.jsx)(`ul`,{className:`m-0 flex list-none flex-col p-0`,children:o.map(e=>(0,K.jsxs)(`li`,{className:`flex flex-wrap items-center justify-between gap-3 border-t border-linie py-3`,children:[(0,K.jsxs)(`span`,{className:`flex min-w-0 flex-1 flex-col gap-1`,children:[(0,K.jsxs)(`span`,{className:`flex flex-wrap items-center gap-2`,children:[(0,K.jsx)(`span`,{className:`ziffern text-sm break-all`,children:e.name}),(0,K.jsx)(D,{children:e.art===`eintrag`?`Eintrag`:`Ordner`})]}),(0,K.jsx)(`span`,{className:`text-sm text-text-2`,children:e.hinweis})]}),(0,K.jsxs)(`span`,{className:`flex items-center gap-3`,children:[(0,K.jsxs)(`span`,{className:`ziffern text-sm text-text-2`,children:[e.groesse_gb.toLocaleString(`de-DE`),` GB`]}),(0,K.jsx)(N,{variant:`gefahr`,size:`sm`,disabled:n.isPending,onClick:()=>i(e),children:`Löschen`})]})]},`${e.art}-${e.name}`))}),e[11]=o,e[12]=t.isPending,e[13]=n.isPending,e[14]=d):d=e[14];let f=r!==null,p=`${r?.name??``} löschen?`,m;e[15]===r?.groesse_gb?m=e[16]:(m=r?.groesse_gb.toLocaleString(`de-DE`)??``,e[15]=r?.groesse_gb,e[16]=m);let h=`${r?.hinweis??``} Es werden ${m} GB frei. Das lässt sich nicht rückgängig machen — zurück ginge es nur mit einem neuen Download.`,g;e[17]===Symbol.for(`react.memo_cache_sentinel`)?(g=()=>i(null),e[17]=g):g=e[17];let _;e[18]!==r||e[19]!==n?(_=()=>{r&&n.mutate(r),i(null)},e[18]=r,e[19]=n,e[20]=_):_=e[20];let y;e[21]!==n.isPending||e[22]!==_||e[23]!==f||e[24]!==p||e[25]!==h?(y=(0,K.jsx)(z,{offen:f,gefahr:!0,titel:p,text:h,knopf:`Endgültig löschen`,gesperrt:n.isPending,onNein:g,onJa:_}),e[21]=n.isPending,e[22]=_,e[23]=f,e[24]=p,e[25]=h,e[26]=y):y=e[26];let b;return e[27]!==a||e[28]!==s||e[29]!==c||e[30]!==y||e[31]!==d?(b=(0,K.jsxs)(a,{titel:s,rechts:c,children:[u,d,y]}),e[27]=a,e[28]=s,e[29]=c,e[30]=y,e[31]=d,e[32]=b):b=e[32],b}function _e(e,t){return e+t.groesse_gb}function ve(){let e=(0,W.c)(74),t=E(),n=S(),r=j(),i=x(),[o,s]=(0,G.useState)(!1),c;e[0]===Symbol.for(`react.memo_cache_sentinel`)?(c={select:Se},e[0]=c):c=e[0];let l=V(c),u=!!t.data&&!n.isPending&&!r.isPending&&!i.isPending,d,f;if(e[1]!==u||e[2]!==l?(d=()=>{u&&l===`radar`&&document.getElementById(`radar`)?.scrollIntoView({block:`start`})},f=[u,l],e[1]=u,e[2]=l,e[3]=d,e[4]=f):(d=e[3],f=e[4]),(0,G.useEffect)(d,f),t.isPending){let t;return e[5]===Symbol.for(`react.memo_cache_sentinel`)?(t=(0,K.jsx)(k,{text:`Modelle werden gelesen …`}),e[5]=t):t=e[5],t}if(!t.data){let n=`Die Modell-Liste fehlt: ${t.error?.message??`keine Daten`}`,r;return e[6]===n?r=e[7]:(r=(0,K.jsx)(k,{fehler:!0,text:n}),e[6]=n,e[7]=r),r}let p=t.data.models,m=t.data.running,h,g,_,v,y,b,C,w,D,O,M,P;if(e[8]!==m||e[9]!==p||e[10]!==n.data||e[11]!==n.error?.message||e[12]!==n.isPending){let t=p.find(xe);g=p.find(be);let r;e[25]===p?r=e[26]:(r=(e,t)=>e?p.find(n=>n!==e&&n.aliases.includes(t)&&n.filename===e.filename):void 0,e[25]=p,e[26]=r);let i=r,o=i(t,`vision`);_=i(g,`coder-bild`);let c=p.filter(e=>![t,g,o,_].includes(e)),l;e[27]===n.data?.zuletzt_geladen?l=e[28]:(l=n.data?.zuletzt_geladen??{},e[27]=n.data?.zuletzt_geladen,e[28]=l);let u=l;e[29]!==m||e[30]!==p?(M=(0,K.jsx)(J,{modelle:p,laufend:m}),e[29]=m,e[30]=p,e[31]=M):M=e[31],e[32]===Symbol.for(`react.memo_cache_sentinel`)?(P=(0,K.jsx)(B,{alsPanel:!0}),e[32]=P):P=e[32];let d;e[33]===Symbol.for(`react.memo_cache_sentinel`)?(d=(0,K.jsx)(Z,{titel:`Dritte Rolle`,laeuft:!1,nutzer:``}),e[33]=d):d=e[33],v=(0,K.jsxs)(`section`,{"aria-label":`Rollen`,className:`grid gap-5 md:grid-cols-3 md:gap-6`,children:[(0,K.jsx)(Z,{titel:`Hirn`,modell:t,zwilling:o,laeuft:!!t&&m.includes(t.name),nutzer:`Lucy, NerdQuiz und OpenChamber für Kleinkram`}),(0,K.jsx)(Z,{titel:`Coder`,modell:g,zwilling:_,laeuft:!!g&&m.includes(g.name),nutzer:`OpenChamber zum Planen und Bauen und Lucys Delegation`}),d]});let f=n.error?.message??null,x;e[34]!==n.data||e[35]!==n.isPending||e[36]!==f?(x=(0,K.jsx)(Q,{nutzung:n.data,laedt:n.isPending,fehler:f}),e[34]=n.data,e[35]=n.isPending,e[36]=f,e[37]=x):x=e[37];let S=t?.name,E=g?.name,k;e[38]!==S||e[39]!==E?(k=(0,K.jsx)(he,{hirn:S,coder:E}),e[38]=S,e[39]=E,e[40]=k):k=e[40],e[41]!==x||e[42]!==k?(y=(0,K.jsxs)(`div`,{className:`grid gap-5 md:gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.3fr)]`,children:[x,k]}),e[41]=x,e[42]=k,e[43]=y):y=e[43],h=A,w=`Weitere Einträge`,e[44]===Symbol.for(`react.memo_cache_sentinel`)?(D=(0,K.jsxs)(N,{variant:`outline`,size:`sm`,onClick:()=>s(!0),children:[(0,K.jsx)(U,{"aria-hidden":!0}),` Modelle selbst suchen`]}),e[44]=D):D=e[44],O=c.length===0&&(0,K.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Keine weiteren Einträge.`}),b=`m-0 flex list-none flex-col p-0`;let j;e[45]!==m||e[46]!==u?(j=e=>(0,K.jsxs)(`li`,{className:`grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3 border-t border-linie py-2.5 sm:grid-cols-[minmax(0,1fr)_120px_160px]`,children:[(0,K.jsxs)(`span`,{className:`min-w-0`,children:[(0,K.jsx)(`span`,{className:`ziffern block text-sm break-all`,children:e.name}),(0,K.jsx)(`span`,{className:`text-xs text-text-3`,children:e.aliases.length?e.aliases.join(`, `):`ohne Rolle`})]}),(0,K.jsx)(`span`,{className:`ziffern text-sm text-text-2`,children:T(e.size_bytes)}),(0,K.jsx)(`span`,{className:`ziffern hidden text-xs text-text-3 sm:block`,children:m.includes(e.name)?`geladen`:u[e.name]?`zuletzt ${a(u[e.name])}`:`länger nicht geladen`})]},e.name),e[45]=m,e[46]=u,e[47]=j):j=e[47],C=c.map(j),e[8]=m,e[9]=p,e[10]=n.data,e[11]=n.error?.message,e[12]=n.isPending,e[13]=h,e[14]=g,e[15]=_,e[16]=v,e[17]=y,e[18]=b,e[19]=C,e[20]=w,e[21]=D,e[22]=O,e[23]=M,e[24]=P}else h=e[13],g=e[14],_=e[15],v=e[16],y=e[17],b=e[18],C=e[19],w=e[20],D=e[21],O=e[22],M=e[23],P=e[24];let F;e[48]!==b||e[49]!==C?(F=(0,K.jsx)(`ul`,{className:b,children:C}),e[48]=b,e[49]=C,e[50]=F):F=e[50];let I;e[51]!==h||e[52]!==F||e[53]!==w||e[54]!==D||e[55]!==O?(I=(0,K.jsxs)(h,{titel:w,rechts:D,children:[O,F]}),e[51]=h,e[52]=F,e[53]=w,e[54]=D,e[55]=O,e[56]=I):I=e[56];let L;e[57]===Symbol.for(`react.memo_cache_sentinel`)?(L=(0,K.jsx)(ge,{}),e[57]=L):L=e[57];let R;e[58]===Symbol.for(`react.memo_cache_sentinel`)?(R=()=>s(!1),e[58]=R):R=e[58];let z;e[59]!==g||e[60]!==_?(z=g?{name:g.name,zwilling:!!_,weitere:g.aliases.filter(ye)}:void 0,e[59]=g,e[60]=_,e[61]=z):z=e[61];let ee;e[62]===z?ee=e[63]:(ee={coder:z},e[62]=z,e[63]=ee);let te;e[64]!==o||e[65]!==ee?(te=(0,K.jsx)(re,{offen:o,onSchliessen:R,rollenJetzt:ee}),e[64]=o,e[65]=ee,e[66]=te):te=e[66];let H;return e[67]!==v||e[68]!==y||e[69]!==I||e[70]!==te||e[71]!==M||e[72]!==P?(H=(0,K.jsxs)(K.Fragment,{children:[M,P,v,y,I,L,te]}),e[67]=v,e[68]=y,e[69]=I,e[70]=te,e[71]=M,e[72]=P,e[73]=H):H=e[73],H}function ye(e){return e.toLowerCase()!==`coder`}function be(e){return o(e)===`coder`}function xe(e){return o(e)===`hirn`}function Se(e){return e.hash}export{ve as ModelleSeite}; \ No newline at end of file diff --git a/frontend/dist/assets/Modelle-DYCP6GrI.js b/frontend/dist/assets/Modelle-DYCP6GrI.js new file mode 100644 index 0000000..fc4f002 --- /dev/null +++ b/frontend/dist/assets/Modelle-DYCP6GrI.js @@ -0,0 +1 @@ +import{c as e,o as t,p as n,s as r,t as i,u as a}from"./button-BheHBEml.js";import{B as o,C as s,F as c,J as l,M as u,N as d,P as f,R as p,S as m,T as h,V as g,W as _,X as v,Y as y,Z as b,_ as x,a as S,d as C,g as w,h as T,i as E,j as D,n as O,r as k,t as A,v as j,x as M,z as N}from"./index-SRwQwEnh.js";import{a as P,i as F,n as I,r as L,t as R}from"./sheet-CKp82pOA.js";import{t as z}from"./Bestaetigen-CE_3b0PK.js";import{o as B,r as ee,t as te}from"./Auftraege-C3JDjG9Z.js";function V(e){let t=v();return y(t.stores.location,l(e,t))}var H={name:`search`,size:24,node:[[`path`,{d:`m21 21-4.34-4.34`,key:`14j7rj`}],[`circle`,{cx:`11`,cy:`11`,r:`8`,key:`4ej97u`}]]};H.node;var U=t(H),W=r(),G=n(a(),1),K=e(),ne={coder:`Coder`};function re(e){let t=(0,W.c)(102),{offen:n,onSchliessen:r,rollenJetzt:a}=e,o=C(),[s,c]=(0,G.useState)(``),[l,u]=(0,G.useState)(``),[p,m]=(0,G.useState)(null),[h,_]=(0,G.useState)(``),[v,y]=(0,G.useState)(null),x=(0,G.useRef)(!1),S,w;t[0]===l?(S=t[1],w=t[2]):(S=[`hf-suche`,l],w=()=>g(`/api/hf/search?q=${encodeURIComponent(l)}`),t[0]=l,t[1]=S,t[2]=w);let T=l.length>1,E;t[3]!==S||t[4]!==w||t[5]!==T?(E={queryKey:S,queryFn:w,enabled:T},t[3]=S,t[4]=w,t[5]=T,t[6]=E):E=t[6];let D=b(E),O,k;t[7]===p?(O=t[8],k=t[9]):(O=[`hf-quants`,p],k=()=>g(`/api/hf/quants?repo=${encodeURIComponent(p??``)}`),t[7]=p,t[8]=O,t[9]=k);let A=!!p,j;t[10]!==O||t[11]!==k||t[12]!==A?(j={queryKey:O,queryFn:k,enabled:A},t[10]=O,t[11]=k,t[12]=A,t[13]=j):j=t[13];let M=b(j),N,te,V,H,U,re,q,J,Y,X,Z,Q,oe;if(t[14]!==v||t[15]!==s||t[16]!==o||t[17]!==n||t[18]!==r||t[19]!==M.data||t[20]!==M.error||t[21]!==M.isError||t[22]!==M.isFetching||t[23]!==p||t[24]!==h||t[25]!==a||t[26]!==D){let e=(M.data?.quants??[]).map(ae),l=h?ne[h]??h:null,g;t[40]!==p||t[41]!==h||t[42]!==a||t[43]!==l?(g=l&&p?B(p,l,a[h]):null,t[40]=p,t[41]=h,t[42]=a,t[43]=l,t[44]=g):g=t[44],V=g;let b;t[45]!==v||t[46]!==o||t[47]!==r||t[48]!==p||t[49]!==h?(b=function(){p&&v&&!x.current&&(x.current=!0,o.mutate({repo:p,quant:v.quant,rolle:h||null},{onSuccess:e=>{y(null),e.ok!==!1&&r()},onError:()=>y(null),onSettled:()=>{x.current=!1}}))},t[45]=v,t[46]=o,t[47]=r,t[48]=p,t[49]=h,t[50]=b):b=t[50],H=b,te=R,Z=n,t[51]===r?Q=t[52]:(Q=e=>!e&&r(),t[51]=r,t[52]=Q),N=I,q=`right`,J=`gap-3 overflow-y-auto border-linie bg-panel data-[side=right]:w-full data-[side=right]:sm:max-w-xl`,t[53]===Symbol.for(`react.memo_cache_sentinel`)?(Y=(0,K.jsxs)(F,{children:[(0,K.jsx)(P,{className:`schild text-lg`,children:`Modelle selbst suchen`}),(0,K.jsx)(L,{className:`text-text-2`,children:`Auf Hugging Face suchen, Quantisierung wählen, installieren.`})]}),t[53]=Y):Y=t[53];let S;t[54]===s?S=t[55]:(S=e=>{e.preventDefault(),m(null),u(s.trim())},t[54]=s,t[55]=S);let C;t[56]===Symbol.for(`react.memo_cache_sentinel`)?(C=(0,K.jsx)(`label`,{htmlFor:`hf-suche`,className:`sr-only`,children:`Suchbegriff`}),t[56]=C):C=t[56];let w;t[57]===Symbol.for(`react.memo_cache_sentinel`)?(w=e=>c(e.target.value),t[57]=w):w=t[57];let T;t[58]===s?T=t[59]:(T=(0,K.jsx)(`input`,{id:`hf-suche`,value:s,onChange:w,placeholder:`z. B. Qwen3.8 GGUF`,className:`h-11 min-w-0 flex-1 rounded-lg border border-linie-stark bg-erhaben px-3 text-[15px] outline-none focus:border-cyan`}),t[58]=s,t[59]=T);let E;t[60]===Symbol.for(`react.memo_cache_sentinel`)?(E=(0,K.jsx)(i,{type:`submit`,children:`Suchen`}),t[60]=E):E=t[60],t[61]!==S||t[62]!==T?(X=(0,K.jsxs)(`form`,{className:`flex gap-2 px-4`,onSubmit:S,children:[C,T,E]}),t[61]=S,t[62]=T,t[63]=X):X=t[63],oe=`flex flex-col px-4 pb-4`,t[64]!==p||t[65]!==D?(U=!p&&(0,K.jsxs)(K.Fragment,{children:[D.isFetching&&(0,K.jsx)(`p`,{role:`status`,className:`py-3 text-sm text-text-2`,children:`Suche läuft …`}),!D.isFetching&&D.isError&&(0,K.jsxs)(`p`,{role:`alert`,className:`py-3 text-sm text-rot-text`,children:[`Die Suche ging nicht: `,D.error.message]}),!D.isFetching&&D.data?.results.length===0&&(0,K.jsx)(`p`,{className:`py-3 text-sm text-text-2`,children:`Keine Treffer. Versuch einen anderen Suchbegriff — oder Hugging Face ist gerade nicht erreichbar.`}),D.data?.results.map(e=>(0,K.jsxs)(`button`,{type:`button`,onClick:()=>m(e.repo),className:`flex min-h-11 items-center justify-between gap-3 border-t border-linie py-2 text-left hover:text-cyan`,children:[(0,K.jsx)(`span`,{className:`ziffern min-w-0 text-sm break-all`,children:e.repo}),(0,K.jsxs)(`span`,{className:`ziffern shrink-0 text-xs text-text-3`,children:[f(e.downloads),` ↓`]})]},e.repo))]}),t[64]=p,t[65]=D,t[66]=U):U=t[66],re=p&&(0,K.jsxs)(`div`,{className:`flex flex-col gap-3 pt-2`,children:[(0,K.jsxs)(`div`,{className:`flex items-center justify-between gap-2`,children:[(0,K.jsx)(`span`,{className:`ziffern min-w-0 text-sm break-all`,children:p}),(0,K.jsx)(i,{variant:`ghost`,size:`sm`,onClick:()=>m(null),children:`Zurück`})]}),(0,K.jsxs)(`label`,{className:`flex flex-wrap items-center gap-3 text-sm text-text-2`,children:[`Rolle`,(0,K.jsxs)(`select`,{value:h,onChange:e=>_(e.target.value),className:`h-11 min-w-0 rounded-lg border border-linie-stark bg-erhaben px-3 text-foreground`,children:[(0,K.jsx)(`option`,{value:``,children:`ohne Rolle (nur installieren)`}),Object.entries(ne).map(ie)]})]}),V&&(0,K.jsx)(`p`,{className:`m-0 rounded-lg border border-bernstein/60 bg-bernstein-grund px-3 py-2.5 text-sm leading-relaxed text-bernstein`,children:V}),M.isFetching&&(0,K.jsx)(`p`,{role:`status`,className:`text-sm text-text-2`,children:`Quantisierungen werden gelesen …`}),!M.isFetching&&M.isError&&(0,K.jsxs)(`p`,{role:`alert`,className:`text-sm text-rot-text`,children:[`Die Dateien dieses Modells ließen sich nicht lesen: `,M.error.message]}),!M.isFetching&&M.data&&e.length===0&&(0,K.jsx)(`p`,{className:`text-sm text-text-2`,children:`Hier gibt es keine GGUF-Dateien, die sich installieren lassen.`}),e.map(e=>{let t=e.quant??e.name??`?`,n=e.total_bytes??e.size_bytes;return(0,K.jsxs)(`div`,{className:`flex items-center justify-between gap-3 border-t border-linie py-2`,children:[(0,K.jsx)(`span`,{className:`ziffern text-sm`,children:t}),(0,K.jsxs)(`span`,{className:`flex items-center gap-3`,children:[n?(0,K.jsx)(`span`,{className:`ziffern text-xs text-text-3`,children:d(n,1)}):null,(0,K.jsx)(i,{variant:`outline`,size:`sm`,disabled:o.isPending,onClick:()=>y({quant:t,bytes:n}),children:`Installieren`})]})]},t)})]}),t[14]=v,t[15]=s,t[16]=o,t[17]=n,t[18]=r,t[19]=M.data,t[20]=M.error,t[21]=M.isError,t[22]=M.isFetching,t[23]=p,t[24]=h,t[25]=a,t[26]=D,t[27]=N,t[28]=te,t[29]=V,t[30]=H,t[31]=U,t[32]=re,t[33]=q,t[34]=J,t[35]=Y,t[36]=X,t[37]=Z,t[38]=Q,t[39]=oe}else N=t[27],te=t[28],V=t[29],H=t[30],U=t[31],re=t[32],q=t[33],J=t[34],Y=t[35],X=t[36],Z=t[37],Q=t[38],oe=t[39];let se;t[67]!==U||t[68]!==re||t[69]!==oe?(se=(0,K.jsxs)(`div`,{className:oe,children:[U,re]}),t[67]=U,t[68]=re,t[69]=oe,t[70]=se):se=t[70];let ce=v!==null,le;t[71]!==v||t[72]!==p?(le=v&&p?`${p} (${v.quant}${v.bytes?`, ${d(v.bytes,1)}`:``}) wird heruntergeladen und in llama-swap eingetragen.`+(v.bytes?``:` Die Größe siehst du beim Auftrag, sobald der Download läuft.`):``,t[71]=v,t[72]=p,t[73]=le):le=t[73];let ue=o.isPending?`Wird gestartet …`:`Installieren`,de=o.isPending,fe;t[74]===Symbol.for(`react.memo_cache_sentinel`)?(fe=()=>y(null),t[74]=fe):fe=t[74];let pe;t[75]===V?pe=t[76]:(pe=V&&(0,K.jsx)(`p`,{className:`m-0 rounded-lg border border-bernstein/60 bg-bernstein-grund px-3 py-2.5 text-[15px] leading-relaxed text-bernstein`,children:V}),t[75]=V,t[76]=pe);let me=!!h,he;t[77]===me?he=t[78]:(he=ee(me),t[77]=me,t[78]=he);let ge;t[79]===he?ge=t[80]:(ge=(0,K.jsx)(`p`,{className:`m-0 text-[15px] leading-relaxed text-text-2`,children:he}),t[79]=he,t[80]=ge);let $;t[81]!==o.isPending||t[82]!==H||t[83]!==ce||t[84]!==le||t[85]!==ue||t[86]!==pe||t[87]!==ge?($=(0,K.jsxs)(z,{offen:ce,titel:`Modell installieren?`,text:le,knopf:ue,gesperrt:de,onNein:fe,onJa:H,children:[pe,ge]}),t[81]=o.isPending,t[82]=H,t[83]=ce,t[84]=le,t[85]=ue,t[86]=pe,t[87]=ge,t[88]=$):$=t[88];let _e;t[89]!==N||t[90]!==q||t[91]!==J||t[92]!==Y||t[93]!==X||t[94]!==se||t[95]!==$?(_e=(0,K.jsxs)(N,{side:q,className:J,children:[Y,X,se,$]}),t[89]=N,t[90]=q,t[91]=J,t[92]=Y,t[93]=X,t[94]=se,t[95]=$,t[96]=_e):_e=t[96];let ve;return t[97]!==te||t[98]!==Z||t[99]!==Q||t[100]!==_e?(ve=(0,K.jsx)(te,{open:Z,onOpenChange:Q,children:_e}),t[97]=te,t[98]=Z,t[99]=Q,t[100]=_e,t[101]=ve):ve=t[101],ve}function ie(e){let[t,n]=e;return(0,K.jsx)(`option`,{value:t,children:n},t)}function ae(e){return typeof e==`string`?{quant:e}:e}var q=115;function J(e){let t=(0,W.c)(55),{modelle:n,laufend:r}=e,i=_(),a=h(),o,s,c,l,d,f,p,m,g,v;if(t[0]!==r||t[1]!==i?.ram_total||t[2]!==i?.ram_used||t[3]!==n||t[4]!==a){let e=u(i?.ram_total??a.data?.box.ram_total)??122.7,h=u(i?.ram_used??a.data?.box.ram_used)??0,_;t[15]===r?_=t[16]:(_=e=>r.includes(e.name),t[15]=r,t[16]=_);let y=n.filter(_).reduce(Y,0);s=Math.max(0,h-y);let b=t=>`${Math.min(100,t/e*100)}%`;o=A,m=`Speicher`,g=(0,K.jsxs)(`span`,{className:`ziffern text-sm text-text-2`,children:[Math.round(h),` von `,Math.round(e),` GB belegt · Grenze ~`,q,` GB`]});let x=`Speicher: ${Math.round(h)} von ${Math.round(e)} GB belegt`,S=b(h),C;t[17]===S?C=t[18]:(C={width:S},t[17]=S,t[18]=C);let w=`${h?y/h*100:0}%`,T;t[19]===w?T=t[20]:(T=(0,K.jsx)(`div`,{className:`h-full bg-cyan`,style:{width:w}}),t[19]=w,t[20]=T);let E=`${h?s/h*100:0}%`,D;t[21]===E?D=t[22]:(D=(0,K.jsx)(`div`,{className:`h-full bg-cyan/35`,style:{width:E}}),t[21]=E,t[22]=D);let O;t[23]!==C||t[24]!==T||t[25]!==D?(O=(0,K.jsxs)(`div`,{className:`absolute inset-y-0 left-0 flex overflow-hidden rounded-lg`,style:C,children:[T,D]}),t[23]=C,t[24]=T,t[25]=D,t[26]=O):O=t[26];let k=b(q),j;t[27]===k?j=t[28]:(j=(0,K.jsx)(`div`,{className:`absolute -top-1.5 -bottom-1.5 w-0.5 bg-bernstein`,style:{left:k}}),t[27]=k,t[28]=j),t[29]!==x||t[30]!==O||t[31]!==j?(v=(0,K.jsxs)(`div`,{className:`relative h-8 rounded-lg bg-erhaben`,role:`img`,"aria-label":x,children:[O,j]}),t[29]=x,t[30]=O,t[31]=j,t[32]=v):v=t[32],p=`flex flex-wrap gap-x-6 gap-y-2 text-sm text-text-2`,c=`flex items-center gap-2`,t[33]===Symbol.for(`react.memo_cache_sentinel`)?(l=(0,K.jsx)(`span`,{className:`size-2.5 rounded-sm bg-cyan`}),t[33]=l):l=t[33],d=` Geladene Modelle: `,f=Math.round(y),t[0]=r,t[1]=i?.ram_total,t[2]=i?.ram_used,t[3]=n,t[4]=a,t[5]=o,t[6]=s,t[7]=c,t[8]=l,t[9]=d,t[10]=f,t[11]=p,t[12]=m,t[13]=g,t[14]=v}else o=t[5],s=t[6],c=t[7],l=t[8],d=t[9],f=t[10],p=t[11],m=t[12],g=t[13],v=t[14];let y;t[34]!==c||t[35]!==l||t[36]!==d||t[37]!==f?(y=(0,K.jsxs)(`span`,{className:c,children:[l,d,f,` GB Dateien`]}),t[34]=c,t[35]=l,t[36]=d,t[37]=f,t[38]=y):y=t[38];let b;t[39]===Symbol.for(`react.memo_cache_sentinel`)?(b=(0,K.jsx)(`span`,{className:`size-2.5 rounded-sm bg-cyan/35`}),t[39]=b):b=t[39];let x;t[40]===s?x=t[41]:(x=Math.round(s),t[40]=s,t[41]=x);let S;t[42]===x?S=t[43]:(S=(0,K.jsxs)(`span`,{className:`flex items-center gap-2`,children:[b,` Cache, Dienste, System: `,x,` GB`]}),t[42]=x,t[43]=S);let C;t[44]===Symbol.for(`react.memo_cache_sentinel`)?(C=(0,K.jsxs)(`span`,{className:`flex items-center gap-2`,children:[(0,K.jsx)(`span`,{className:`h-3 w-0.5 bg-bernstein`}),` Ab hier wird es eng`]}),t[44]=C):C=t[44];let w;t[45]!==S||t[46]!==p||t[47]!==y?(w=(0,K.jsxs)(`div`,{className:p,children:[y,S,C]}),t[45]=S,t[46]=p,t[47]=y,t[48]=w):w=t[48];let T;return t[49]!==o||t[50]!==w||t[51]!==m||t[52]!==g||t[53]!==v?(T=(0,K.jsxs)(o,{titel:m,rechts:g,children:[v,w]}),t[49]=o,t[50]=w,t[51]=m,t[52]=g,t[53]=v,t[54]=T):T=t[54],T}function Y(e,t){return e+(u(t.size_bytes)??0)}function X(e){let t=(0,W.c)(7),{name:n,wert:r}=e,i;t[0]===n?i=t[1]:(i=(0,K.jsx)(`dt`,{className:`text-xs text-text-3`,children:n}),t[0]=n,t[1]=i);let a;t[2]===r?a=t[3]:(a=(0,K.jsx)(`dd`,{className:`ziffern m-0 text-lg`,children:r}),t[2]=r,t[3]=a);let o;return t[4]!==i||t[5]!==a?(o=(0,K.jsxs)(`div`,{className:`flex flex-col gap-1`,children:[i,a]}),t[4]=i,t[5]=a,t[6]=o):o=t[6],o}function Z(e){let t=(0,W.c)(48),{titel:n,modell:r,zwilling:a,laeuft:o,nutzer:s}=e,c=w();if(!r){let e;t[0]===n?e=t[1]:(e=(0,K.jsx)(`span`,{className:`schild text-sm text-text-3`,children:n}),t[0]=n,t[1]=e);let r;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(r=(0,K.jsx)(O,{children:`Frei`}),t[2]=r):r=t[2];let i;t[3]===e?i=t[4]:(i=(0,K.jsxs)(`div`,{className:`flex items-center justify-between`,children:[e,r]}),t[3]=e,t[4]=i);let a,o;t[5]===Symbol.for(`react.memo_cache_sentinel`)?(a=(0,K.jsx)(`h3`,{className:`font-anzeige text-xl font-semibold text-text-2`,children:`Nicht belegt`}),o=(0,K.jsx)(`p`,{className:`text-sm text-text-2`,children:`Nur belegen, wenn ein Modell hier echten Mehrwert bringt. Das Radar meldet Kandidaten.`}),t[5]=a,t[6]=o):(a=t[5],o=t[6]);let s;return t[7]===i?s=t[8]:(s=(0,K.jsxs)(`article`,{className:`flex flex-col gap-3.5 rounded-2xl border border-dashed border-linie-stark p-5`,children:[i,a,o]}),t[7]=i,t[8]=s),s}let l;t[9]!==r.ctx||t[10]!==r.parallel_slots?(l=r.ctx?Math.round(r.ctx/Math.max(1,r.parallel_slots)/1024):null,t[9]=r.ctx,t[10]=r.parallel_slots,t[11]=l):l=t[11];let u=l,f=!!r.capabilities?.vision||!!a,p;t[12]===n?p=t[13]:(p=(0,K.jsx)(`span`,{className:`schild text-sm text-text-3`,children:n}),t[12]=n,t[13]=p);let m=o?`gruen`:`grau`,h=o?`Geladen`:`Auf Abruf`,g;t[14]!==m||t[15]!==h?(g=(0,K.jsx)(O,{art:m,children:h}),t[14]=m,t[15]=h,t[16]=g):g=t[16];let _;t[17]!==p||t[18]!==g?(_=(0,K.jsxs)(`div`,{className:`flex items-center justify-between gap-2`,children:[p,g]}),t[17]=p,t[18]=g,t[19]=_):_=t[19];let v;t[20]===r.name?v=t[21]:(v=(0,K.jsx)(`h3`,{className:`ziffern m-0 text-lg font-medium break-all`,children:r.name}),t[20]=r.name,t[21]=v);let y;t[22]===r.size_bytes?y=t[23]:(y=d(r.size_bytes),t[22]=r.size_bytes,t[23]=y);let b;t[24]===y?b=t[25]:(b=(0,K.jsx)(X,{name:`Größe`,wert:y}),t[24]=y,t[25]=b);let x=u?`${u}k`:`–`,S;t[26]===x?S=t[27]:(S=(0,K.jsx)(X,{name:`Kontext`,wert:x}),t[26]=x,t[27]=S);let C=f?`ja`:`nein`,T;t[28]===C?T=t[29]:(T=(0,K.jsx)(X,{name:`Bilder`,wert:C}),t[28]=C,t[29]=T);let E;t[30]!==S||t[31]!==T||t[32]!==b?(E=(0,K.jsxs)(`dl`,{className:`m-0 grid grid-cols-3 gap-3`,children:[b,S,T]}),t[30]=S,t[31]=T,t[32]=b,t[33]=E):E=t[33];let D=r.spec_active?` Mit Draft-Beschleunigung.`:``,k=a?` Bilder sieht es über seinen Bild-Zwilling, der nur bei Bedarf lädt.`:``,A;t[34]!==s||t[35]!==D||t[36]!==k?(A=(0,K.jsxs)(`p`,{className:`text-sm text-text-2`,children:[`Genutzt von `,s,`.`,D,k]}),t[34]=s,t[35]=D,t[36]=k,t[37]=A):A=t[37];let j;t[38]!==c||t[39]!==o||t[40]!==r.name?(j=!o&&(0,K.jsx)(i,{variant:`outline`,size:`sm`,className:`mt-auto self-start`,onClick:()=>c.mutate(r.name),disabled:c.isPending,children:`Jetzt laden`}),t[38]=c,t[39]=o,t[40]=r.name,t[41]=j):j=t[41];let M;return t[42]!==E||t[43]!==A||t[44]!==j||t[45]!==_||t[46]!==v?(M=(0,K.jsxs)(`article`,{className:`flex flex-col gap-3.5 rounded-2xl border border-linie bg-panel p-5`,children:[_,v,E,A,j]}),t[42]=E,t[43]=A,t[44]=j,t[45]=_,t[46]=v,t[47]=M):M=t[47],M}function Q(e){let t=(0,W.c)(54),{nutzung:n,laedt:r,fehler:i}=e;if(!n){let e;return t[0]!==i||t[1]!==r?(e=(0,K.jsx)(A,{titel:`Wer nutzt die Modelle`,children:r?(0,K.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Wird gelesen …`}):(0,K.jsxs)(`p`,{role:`alert`,className:`text-[15px] text-rot-text`,children:[`Die Nutzung lässt sich gerade nicht lesen`,i?`: ${i}`:`.`]})}),t[0]=i,t[1]=r,t[2]=e):e=t[2],e}let a;t[3]===n.absender?a=t[4]:(a=Math.max(1,...n.absender.map(le)),t[3]=n.absender,t[4]=a);let o=a,s,c,l,u,d,p,m,h,g,_,v,y;if(t[5]!==o||t[6]!==n.absender||t[7]!==n.letzte_24h||t[8]!==n.tage){let e=n.letzte_24h.map(se),r=Math.max(1,...e);s=A,y=`Wer nutzt die Modelle`,t[21]===n.tage?c=t[22]:(c=(0,K.jsxs)(`span`,{className:`ziffern text-xs text-text-3`,children:[`letzte `,n.tage,` Tage`]}),t[21]=n.tage,t[22]=c),t[23]===n.absender.length?l=t[24]:(l=n.absender.length===0&&(0,K.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Keine Anfragen aufgezeichnet.`}),t[23]=n.absender.length,t[24]=l);let i;if(t[25]!==o||t[26]!==n.absender){let e;t[28]===o?e=t[29]:(e=e=>(0,K.jsxs)(`li`,{className:`flex flex-col gap-1.5`,children:[(0,K.jsxs)(`div`,{className:`flex justify-between gap-3 text-sm`,children:[(0,K.jsx)(`span`,{children:e.name}),(0,K.jsx)(`span`,{className:`ziffern`,children:f(e.anfragen)})]}),(0,K.jsx)(`div`,{className:`h-2 rounded-full bg-erhaben`,children:(0,K.jsx)(`div`,{className:`h-2 rounded-full bg-cyan`,style:{width:`${e.anfragen/o*100}%`}})})]},e.name),t[28]=o,t[29]=e),i=n.absender.map(e),t[25]=o,t[26]=n.absender,t[27]=i}else i=t[27];t[30]===i?u=t[31]:(u=(0,K.jsx)(`ul`,{className:`m-0 flex list-none flex-col gap-3.5 p-0`,children:i}),t[30]=i,t[31]=u),_=`flex flex-col gap-1.5`,t[32]===Symbol.for(`react.memo_cache_sentinel`)?(v=(0,K.jsx)(`span`,{className:`schild text-xs text-text-3`,children:`Anfragen je Stunde, letzte 24 h`}),t[32]=v):v=t[32],d=`0 0 240 64`,p=`h-16 w-full`,m=`img`,h=`Anfragen je Stunde in den letzten 24 Stunden`;let a;t[33]===r?a=t[34]:(a=(e,t)=>{let n=e?Math.max(3,e/r*50):2;return(0,K.jsx)(`rect`,{x:t*10+1.5,y:52-n,width:7,height:n,rx:1.5,fill:e?`var(--cyan)`:`var(--linie)`},t)},t[33]=r,t[34]=a),g=e.map(a),t[5]=o,t[6]=n.absender,t[7]=n.letzte_24h,t[8]=n.tage,t[9]=s,t[10]=c,t[11]=l,t[12]=u,t[13]=d,t[14]=p,t[15]=m,t[16]=h,t[17]=g,t[18]=_,t[19]=v,t[20]=y}else s=t[9],c=t[10],l=t[11],u=t[12],d=t[13],p=t[14],m=t[15],h=t[16],g=t[17],_=t[18],v=t[19],y=t[20];let b;t[35]===Symbol.for(`react.memo_cache_sentinel`)?(b=[0,6,12,18].map(oe),t[35]=b):b=t[35];let x;t[36]!==d||t[37]!==p||t[38]!==m||t[39]!==h||t[40]!==g?(x=(0,K.jsxs)(`svg`,{viewBox:d,className:p,role:m,"aria-label":h,children:[g,b]}),t[36]=d,t[37]=p,t[38]=m,t[39]=h,t[40]=g,t[41]=x):x=t[41];let S;t[42]!==x||t[43]!==_||t[44]!==v?(S=(0,K.jsxs)(`div`,{className:_,children:[v,x]}),t[42]=x,t[43]=_,t[44]=v,t[45]=S):S=t[45];let C;t[46]===Symbol.for(`react.memo_cache_sentinel`)?(C=(0,K.jsx)(`p`,{className:`text-xs text-text-3`,children:`Anfragen an llama-swap. NerdQuiz geht direkt dorthin, alles andere über MC2.`}),t[46]=C):C=t[46];let w;return t[47]!==s||t[48]!==c||t[49]!==l||t[50]!==u||t[51]!==S||t[52]!==y?(w=(0,K.jsxs)(s,{titel:y,rechts:c,children:[l,u,S,C]}),t[47]=s,t[48]=c,t[49]=l,t[50]=u,t[51]=S,t[52]=y,t[53]=w):w=t[53],w}function oe(e){return(0,K.jsx)(`text`,{x:e*10+5,y:63,textAnchor:`middle`,style:{fontSize:9,fill:`var(--text-3)`,fontFamily:`var(--font-mono)`},children:e},e)}function se(e){return Object.values(e.je_absender).reduce(ce,0)}function ce(e,t){return e+t}function le(e){return e.anfragen}var ue={bestanden:`gruen`,durchgefallen:`rot`,wartet:`cyan`,neu:`cyan`,getestet:`grau`,uebernommen:`gruen`,verworfen:`grau`},de={besser:`text-gruen`,schlechter:`text-rot-text`,gleich:`text-text-2`};function fe(e){let t=(0,W.c)(4),{test:n}=e,r,i;if(t[0]!==n.vergleich){i=Symbol.for(`react.early_return_sentinel`);bb0:{let e=Object.entries(n.vergleich??{});if(e.length===0){i=null;break bb0}let a;t[3]===Symbol.for(`react.memo_cache_sentinel`)?(a=(0,K.jsxs)(`div`,{role:`row`,className:`schild grid grid-cols-[minmax(0,1fr)_auto_auto] gap-x-4 pb-1 text-xs text-text-3`,children:[(0,K.jsx)(`span`,{role:`columnheader`,children:`Messwert`}),(0,K.jsx)(`span`,{role:`columnheader`,className:`text-right`,children:`Kandidat`}),(0,K.jsx)(`span`,{role:`columnheader`,className:`text-right`,children:`Heute`})]}),t[3]=a):a=t[3],r=(0,K.jsxs)(`div`,{role:`table`,"aria-label":`Messwerte gegen das heutige Modell`,className:`rounded-lg border border-linie bg-erhaben/50 px-3 py-2 text-sm`,children:[a,e.map(pe)]})}t[0]=n.vergleich,t[1]=r,t[2]=i}else r=t[1],i=t[2];return i===Symbol.for(`react.early_return_sentinel`)?r:i}function pe(e){let[t,n]=e;return(0,K.jsxs)(`div`,{role:`row`,className:`grid grid-cols-[minmax(0,1fr)_auto_auto] gap-x-4 border-t border-linie py-1`,children:[(0,K.jsx)(`span`,{role:`rowheader`,className:`text-text-2`,children:t}),(0,K.jsx)(`span`,{role:`cell`,className:`ziffern text-right ${de[n.urteil??``]??`text-text-2`}`,children:p(n.kandidat,n.einheit)}),(0,K.jsx)(`span`,{role:`cell`,className:`ziffern text-right text-text-3`,children:p(n.baseline,n.einheit)})]},t)}function me(e){let t=(0,W.c)(59),{k:n,test:r,heute:a}=e,o=m(),[s,l]=(0,G.useState)(null),u;t[0]===Symbol.for(`react.memo_cache_sentinel`)?(u=[`bestanden`,`wartet`,`neu`,`getestet`],t[0]=u):u=t[0];let d=u.includes(n.status),f=n.rolle===`hirn`,p;t[1]===n.name?p=t[2]:(p=(0,K.jsx)(`span`,{className:`ziffern text-[15px] font-medium`,children:n.name}),t[1]=n.name,t[2]=p);let h=f?`fürs Hirn`:`fürs Coden`,g;t[3]===h?g=t[4]:(g=(0,K.jsx)(O,{art:`cyan`,children:h}),t[3]=h,t[4]=g);let _;t[5]!==n.eng||t[6]!==n.groesse_gb||t[7]!==n.passt?(_=n.groesse_gb!=null&&(0,K.jsx)(O,{art:n.eng?`bernstein`:n.passt?`gruen`:`rot`,children:n.eng?`passt knapp`:n.passt?`passt`:`passt nicht`}),t[5]=n.eng,t[6]=n.groesse_gb,t[7]=n.passt,t[8]=_):_=t[8];let v=ue[n.status]??`grau`,y=c[n.status]??n.status,b;t[9]!==v||t[10]!==y?(b=(0,K.jsx)(O,{art:v,children:y}),t[9]=v,t[10]=y,t[11]=b):b=t[11];let x;t[12]!==p||t[13]!==g||t[14]!==_||t[15]!==b?(x=(0,K.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2`,children:[p,g,_,b]}),t[12]=p,t[13]=g,t[14]=_,t[15]=b,t[16]=x):x=t[16];let S;t[17]===n.groesse_gb?S=t[18]:(S=n.groesse_gb==null?``:`${n.groesse_gb.toLocaleString(`de-DE`)} GB. `,t[17]=n.groesse_gb,t[18]=S);let C;t[19]!==n.begruendung||t[20]!==S?(C=(0,K.jsxs)(`p`,{className:`text-sm text-text-2`,children:[S,n.begruendung]}),t[19]=n.begruendung,t[20]=S,t[21]=C):C=t[21];let w;t[22]!==n.status||t[23]!==r?(w=r&&[`bestanden`,`durchgefallen`].includes(n.status)&&(0,K.jsx)(fe,{test:r}),t[22]=n.status,t[23]=r,t[24]=w):w=t[24];let T;t[25]!==o.isPending||t[26]!==n.status||t[27]!==d?(T=d&&(0,K.jsxs)(`div`,{className:`flex flex-wrap gap-2`,children:[n.status===`bestanden`&&(0,K.jsx)(i,{size:`sm`,disabled:o.isPending,onClick:()=>l(`uebernehmen`),children:`Übernehmen`}),(0,K.jsx)(i,{variant:`outline`,size:`sm`,disabled:o.isPending,onClick:()=>l(`verwerfen`),children:`Verwerfen`})]}),t[25]=o.isPending,t[26]=n.status,t[27]=d,t[28]=T):T=t[28];let E=s===`uebernehmen`,D=`${n.name} übernehmen?`,k=f?`${n.name} wird Lucys Hirn${a?` und löst ${a} ab`:``}. NerdQuiz und OpenChamber nutzen ab dann ebenfalls das neue Modell. Hermes startet dafür kurz neu, Lucy ist etwa eine Minute nicht erreichbar. Das alte Modell bleibt auf der Platte.`:`${n.name} wird der Coder${a?` und löst ${a} ab`:``}. OpenChamber plant und baut ab dann damit. Das alte Modell bleibt auf der Platte.`,A;t[29]===Symbol.for(`react.memo_cache_sentinel`)?(A=()=>l(null),t[29]=A):A=t[29];let j;t[30]!==o||t[31]!==s||t[32]!==n.id||t[33]!==n.name?(j=()=>{s===`uebernehmen`&&(o.mutate({id:n.id,aktion:`uebernehmen`,name:n.name}),l(null))},t[30]=o,t[31]=s,t[32]=n.id,t[33]=n.name,t[34]=j):j=t[34];let M;t[35]!==o.isPending||t[36]!==E||t[37]!==D||t[38]!==k||t[39]!==j?(M=(0,K.jsx)(z,{offen:E,titel:D,text:k,knopf:`Übernehmen`,gesperrt:o.isPending,onNein:A,onJa:j}),t[35]=o.isPending,t[36]=E,t[37]=D,t[38]=k,t[39]=j,t[40]=M):M=t[40];let N=s===`verwerfen`,P=`${n.name} verwerfen?`,F;t[41]===Symbol.for(`react.memo_cache_sentinel`)?(F=()=>l(null),t[41]=F):F=t[41];let I;t[42]!==o||t[43]!==s||t[44]!==n.id||t[45]!==n.name?(I=()=>{s===`verwerfen`&&(o.mutate({id:n.id,aktion:`verwerfen`,name:n.name}),l(null))},t[42]=o,t[43]=s,t[44]=n.id,t[45]=n.name,t[46]=I):I=t[46];let L;t[47]!==o.isPending||t[48]!==N||t[49]!==P||t[50]!==I?(L=(0,K.jsx)(z,{offen:N,gefahr:!0,titel:P,text:`Die heruntergeladenen Dateien werden gelöscht, und das Radar testet dieses Modell nicht noch einmal.`,knopf:`Verwerfen`,gesperrt:o.isPending,onNein:F,onJa:I}),t[47]=o.isPending,t[48]=N,t[49]=P,t[50]=I,t[51]=L):L=t[51];let R;return t[52]!==C||t[53]!==w||t[54]!==T||t[55]!==M||t[56]!==L||t[57]!==x?(R=(0,K.jsxs)(`li`,{className:`flex flex-col gap-2 border-t border-linie py-3.5`,children:[x,C,w,T,M,L]}),t[52]=C,t[53]=w,t[54]=T,t[55]=M,t[56]=L,t[57]=x,t[58]=R):R=t[58],R}function he(e){let t=(0,W.c)(24),{hirn:n,coder:r}=e,a=M(),c=s();if(a.isError&&!a.data){let e=a.error instanceof o&&a.error.status===404,n;return t[0]!==e||t[1]!==a.error?(n=(0,K.jsx)(A,{titel:`Modell-Radar`,id:`radar`,className:`scroll-mt-4`,children:e?(0,K.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Das Radar ist auf dieser Box noch nicht eingerichtet.`}):(0,K.jsxs)(`p`,{role:`alert`,className:`text-[15px] text-rot-text`,children:[`Das Radar antwortet gerade nicht: `,a.error.message]})}),t[0]=e,t[1]=a.error,t[2]=n):n=t[2],n}let l=a.data,u;t[3]===c?u=t[4]:(u=()=>c.mutate(void 0),t[3]=c,t[4]=u);let d=c.isPending?`Sucht …`:`Jetzt suchen`,f;t[5]!==c.isPending||t[6]!==u||t[7]!==d?(f=(0,K.jsx)(i,{variant:`info`,size:`sm`,onClick:u,disabled:c.isPending,children:d}),t[5]=c.isPending,t[6]=u,t[7]=d,t[8]=f):f=t[8];let p;t[9]===l?p=t[10]:(p=l?.naechster_test?` Nächster Test: ${D(l.naechster_test)}.`:``,t[9]=l,t[10]=p);let m;t[11]===p?m=t[12]:(m=(0,K.jsxs)(`p`,{className:`-mt-2 text-sm text-text-2`,children:[`Testet nachts zwischen 00:30 und 02:30 selbst, höchstens einen Kandidaten pro Woche.`,p]}),t[11]=p,t[12]=m);let h;t[13]!==r||t[14]!==l||t[15]!==n?(h=l?l.kandidaten.length===0?(0,K.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Nichts Besseres in Sicht.`}):(0,K.jsx)(`ul`,{className:`m-0 flex list-none flex-col p-0`,children:l.kandidaten.map(e=>(0,K.jsx)(me,{k:e,test:l.tests.find(t=>t.kandidat===e.id),heute:e.rolle===`hirn`?n:r},e.id))}):(0,K.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Wird gelesen …`}),t[13]=r,t[14]=l,t[15]=n,t[16]=h):h=t[16];let g;t[17]===l?g=t[18]:(g=l&&l.tests.length>0&&(0,K.jsxs)(`div`,{className:`flex flex-col gap-2`,children:[(0,K.jsx)(`h3`,{className:`schild text-xs text-text-3`,children:`Letzte Tests`}),l.tests.slice(0,3).map(ge)]}),t[17]=l,t[18]=g);let _;return t[19]!==f||t[20]!==m||t[21]!==h||t[22]!==g?(_=(0,K.jsxs)(A,{titel:`Modell-Radar`,id:`radar`,className:`scroll-mt-4`,rechts:f,children:[m,h,g]}),t[19]=f,t[20]=m,t[21]=h,t[22]=g,t[23]=_):_=t[23],_}function ge(e){return(0,K.jsxs)(`p`,{className:`text-sm text-text-2`,children:[(0,K.jsx)(`span`,{className:`ziffern text-text-3`,children:D(e.datum)}),` · `,e.zusammenfassung]},e.id)}function $(){let e=(0,W.c)(33),t=E(),n=S(),[r,a]=(0,G.useState)(null),o,s,c,l,u;if(e[0]!==t.data||e[1]!==t.error||e[2]!==t.isError){u=Symbol.for(`react.early_return_sentinel`);bb0:{s=t.data?.kandidaten??[];let n=s.reduce(_e,0);if(t.isError&&!t.data){let n;e[8]===t.error.message?n=e[9]:(n=(0,K.jsx)(A,{titel:`Aufräumen`,children:(0,K.jsxs)(`p`,{role:`alert`,className:`text-[15px] text-rot-text`,children:[`Was sich aufräumen ließe, lässt sich gerade nicht lesen: `,t.error.message]})}),e[8]=t.error.message,e[9]=n),u=n;break bb0}o=A,c=`Aufräumen`,l=s.length>0?(0,K.jsxs)(`span`,{className:`ziffern text-sm text-text-2`,children:[`bis zu `,n.toLocaleString(`de-DE`,{maximumFractionDigits:0}),` GB frei`]}):void 0}e[0]=t.data,e[1]=t.error,e[2]=t.isError,e[3]=o,e[4]=s,e[5]=c,e[6]=l,e[7]=u}else o=e[3],s=e[4],c=e[5],l=e[6],u=e[7];if(u!==Symbol.for(`react.early_return_sentinel`))return u;let d;e[10]===Symbol.for(`react.memo_cache_sentinel`)?(d=(0,K.jsx)(`p`,{className:`-mt-2 text-sm text-text-2`,children:`Modelle und Ordner, die gerade niemand nutzt. Nichts davon wird von selbst gelöscht, das entscheidest du je Eintrag.`}),e[10]=d):d=e[10];let f;e[11]!==s||e[12]!==t.isPending||e[13]!==n.isPending?(f=t.isPending?(0,K.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Wird gelesen …`}):s.length===0?(0,K.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Nichts aufzuräumen.`}):(0,K.jsx)(`ul`,{className:`m-0 flex list-none flex-col p-0`,children:s.map(e=>(0,K.jsxs)(`li`,{className:`flex flex-wrap items-center justify-between gap-3 border-t border-linie py-3`,children:[(0,K.jsxs)(`span`,{className:`flex min-w-0 flex-1 flex-col gap-1`,children:[(0,K.jsxs)(`span`,{className:`flex flex-wrap items-center gap-2`,children:[(0,K.jsx)(`span`,{className:`ziffern text-sm break-all`,children:e.name}),(0,K.jsx)(O,{children:e.art===`eintrag`?`Eintrag`:`Ordner`})]}),(0,K.jsx)(`span`,{className:`text-sm text-text-2`,children:e.hinweis})]}),(0,K.jsxs)(`span`,{className:`flex items-center gap-3`,children:[(0,K.jsxs)(`span`,{className:`ziffern text-sm text-text-2`,children:[e.groesse_gb.toLocaleString(`de-DE`),` GB`]}),(0,K.jsx)(i,{variant:`gefahr`,size:`sm`,disabled:n.isPending,onClick:()=>a(e),children:`Löschen`})]})]},`${e.art}-${e.name}`))}),e[11]=s,e[12]=t.isPending,e[13]=n.isPending,e[14]=f):f=e[14];let p=r!==null,m=`${r?.name??``} löschen?`,h;e[15]===r?.groesse_gb?h=e[16]:(h=r?.groesse_gb.toLocaleString(`de-DE`)??``,e[15]=r?.groesse_gb,e[16]=h);let g=`${r?.hinweis??``} Es werden ${h} GB frei. Das lässt sich nicht rückgängig machen — zurück ginge es nur mit einem neuen Download.`,_;e[17]===Symbol.for(`react.memo_cache_sentinel`)?(_=()=>a(null),e[17]=_):_=e[17];let v;e[18]!==r||e[19]!==n?(v=()=>{r&&n.mutate(r),a(null)},e[18]=r,e[19]=n,e[20]=v):v=e[20];let y;e[21]!==n.isPending||e[22]!==v||e[23]!==p||e[24]!==m||e[25]!==g?(y=(0,K.jsx)(z,{offen:p,gefahr:!0,titel:m,text:g,knopf:`Endgültig löschen`,gesperrt:n.isPending,onNein:_,onJa:v}),e[21]=n.isPending,e[22]=v,e[23]=p,e[24]=m,e[25]=g,e[26]=y):y=e[26];let b;return e[27]!==o||e[28]!==c||e[29]!==l||e[30]!==y||e[31]!==f?(b=(0,K.jsxs)(o,{titel:c,rechts:l,children:[d,f,y]}),e[27]=o,e[28]=c,e[29]=l,e[30]=y,e[31]=f,e[32]=b):b=e[32],b}function _e(e,t){return e+t.groesse_gb}function ve(){let e=(0,W.c)(74),t=x(),n=j(),r=M(),a=T(),[o,s]=(0,G.useState)(!1),c;e[0]===Symbol.for(`react.memo_cache_sentinel`)?(c={select:Se},e[0]=c):c=e[0];let l=V(c),u=!!t.data&&!n.isPending&&!r.isPending&&!a.isPending,f,p;if(e[1]!==u||e[2]!==l?(f=()=>{u&&l===`radar`&&document.getElementById(`radar`)?.scrollIntoView({block:`start`})},p=[u,l],e[1]=u,e[2]=l,e[3]=f,e[4]=p):(f=e[3],p=e[4]),(0,G.useEffect)(f,p),t.isPending){let t;return e[5]===Symbol.for(`react.memo_cache_sentinel`)?(t=(0,K.jsx)(k,{text:`Modelle werden gelesen …`}),e[5]=t):t=e[5],t}if(!t.data){let n=`Die Modell-Liste fehlt: ${t.error?.message??`keine Daten`}`,r;return e[6]===n?r=e[7]:(r=(0,K.jsx)(k,{fehler:!0,text:n}),e[6]=n,e[7]=r),r}let m=t.data.models,h=t.data.running,g,_,v,y,b,S,C,w,E,O,N,P;if(e[8]!==h||e[9]!==m||e[10]!==n.data||e[11]!==n.error?.message||e[12]!==n.isPending){let t=m.find(xe);_=m.find(be);let r;e[25]===m?r=e[26]:(r=(e,t)=>e?m.find(n=>n!==e&&n.aliases.includes(t)&&n.filename===e.filename):void 0,e[25]=m,e[26]=r);let a=r,o=a(t,`vision`);v=a(_,`coder-bild`);let c=m.filter(e=>![t,_,o,v].includes(e)),l;e[27]===n.data?.zuletzt_geladen?l=e[28]:(l=n.data?.zuletzt_geladen??{},e[27]=n.data?.zuletzt_geladen,e[28]=l);let u=l;e[29]!==h||e[30]!==m?(N=(0,K.jsx)(J,{modelle:m,laufend:h}),e[29]=h,e[30]=m,e[31]=N):N=e[31],e[32]===Symbol.for(`react.memo_cache_sentinel`)?(P=(0,K.jsx)(te,{alsPanel:!0}),e[32]=P):P=e[32];let f;e[33]===Symbol.for(`react.memo_cache_sentinel`)?(f=(0,K.jsx)(Z,{titel:`Dritte Rolle`,laeuft:!1,nutzer:``}),e[33]=f):f=e[33],y=(0,K.jsxs)(`section`,{"aria-label":`Rollen`,className:`grid gap-5 md:grid-cols-3 md:gap-6`,children:[(0,K.jsx)(Z,{titel:`Hirn`,modell:t,zwilling:o,laeuft:!!t&&h.includes(t.name),nutzer:`Lucy, NerdQuiz und OpenChamber für Kleinkram`}),(0,K.jsx)(Z,{titel:`Coder`,modell:_,zwilling:v,laeuft:!!_&&h.includes(_.name),nutzer:`OpenChamber zum Planen und Bauen und Lucys Delegation`}),f]});let p=n.error?.message??null,x;e[34]!==n.data||e[35]!==n.isPending||e[36]!==p?(x=(0,K.jsx)(Q,{nutzung:n.data,laedt:n.isPending,fehler:p}),e[34]=n.data,e[35]=n.isPending,e[36]=p,e[37]=x):x=e[37];let T=t?.name,k=_?.name,j;e[38]!==T||e[39]!==k?(j=(0,K.jsx)(he,{hirn:T,coder:k}),e[38]=T,e[39]=k,e[40]=j):j=e[40],e[41]!==x||e[42]!==j?(b=(0,K.jsxs)(`div`,{className:`grid gap-5 md:gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.3fr)]`,children:[x,j]}),e[41]=x,e[42]=j,e[43]=b):b=e[43],g=A,w=`Weitere Einträge`,e[44]===Symbol.for(`react.memo_cache_sentinel`)?(E=(0,K.jsxs)(i,{variant:`outline`,size:`sm`,onClick:()=>s(!0),children:[(0,K.jsx)(U,{"aria-hidden":!0}),` Modelle selbst suchen`]}),e[44]=E):E=e[44],O=c.length===0&&(0,K.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Keine weiteren Einträge.`}),S=`m-0 flex list-none flex-col p-0`;let M;e[45]!==h||e[46]!==u?(M=e=>(0,K.jsxs)(`li`,{className:`grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3 border-t border-linie py-2.5 sm:grid-cols-[minmax(0,1fr)_120px_160px]`,children:[(0,K.jsxs)(`span`,{className:`min-w-0`,children:[(0,K.jsx)(`span`,{className:`ziffern block text-sm break-all`,children:e.name}),(0,K.jsx)(`span`,{className:`text-xs text-text-3`,children:e.aliases.length?e.aliases.join(`, `):`ohne Rolle`})]}),(0,K.jsx)(`span`,{className:`ziffern text-sm text-text-2`,children:d(e.size_bytes)}),(0,K.jsx)(`span`,{className:`ziffern hidden text-xs text-text-3 sm:block`,children:h.includes(e.name)?`geladen`:u[e.name]?`zuletzt ${D(u[e.name])}`:`länger nicht geladen`})]},e.name),e[45]=h,e[46]=u,e[47]=M):M=e[47],C=c.map(M),e[8]=h,e[9]=m,e[10]=n.data,e[11]=n.error?.message,e[12]=n.isPending,e[13]=g,e[14]=_,e[15]=v,e[16]=y,e[17]=b,e[18]=S,e[19]=C,e[20]=w,e[21]=E,e[22]=O,e[23]=N,e[24]=P}else g=e[13],_=e[14],v=e[15],y=e[16],b=e[17],S=e[18],C=e[19],w=e[20],E=e[21],O=e[22],N=e[23],P=e[24];let F;e[48]!==S||e[49]!==C?(F=(0,K.jsx)(`ul`,{className:S,children:C}),e[48]=S,e[49]=C,e[50]=F):F=e[50];let I;e[51]!==g||e[52]!==F||e[53]!==w||e[54]!==E||e[55]!==O?(I=(0,K.jsxs)(g,{titel:w,rechts:E,children:[O,F]}),e[51]=g,e[52]=F,e[53]=w,e[54]=E,e[55]=O,e[56]=I):I=e[56];let L;e[57]===Symbol.for(`react.memo_cache_sentinel`)?(L=(0,K.jsx)($,{}),e[57]=L):L=e[57];let R;e[58]===Symbol.for(`react.memo_cache_sentinel`)?(R=()=>s(!1),e[58]=R):R=e[58];let z;e[59]!==_||e[60]!==v?(z=_?{name:_.name,zwilling:!!v,weitere:_.aliases.filter(ye)}:void 0,e[59]=_,e[60]=v,e[61]=z):z=e[61];let B;e[62]===z?B=e[63]:(B={coder:z},e[62]=z,e[63]=B);let ee;e[64]!==o||e[65]!==B?(ee=(0,K.jsx)(re,{offen:o,onSchliessen:R,rollenJetzt:B}),e[64]=o,e[65]=B,e[66]=ee):ee=e[66];let H;return e[67]!==y||e[68]!==b||e[69]!==I||e[70]!==ee||e[71]!==N||e[72]!==P?(H=(0,K.jsxs)(K.Fragment,{children:[N,P,y,b,I,L,ee]}),e[67]=y,e[68]=b,e[69]=I,e[70]=ee,e[71]=N,e[72]=P,e[73]=H):H=e[73],H}function ye(e){return e.toLowerCase()!==`coder`}function be(e){return N(e)===`coder`}function xe(e){return N(e)===`hirn`}function Se(e){return e.hash}export{ve as ModelleSeite}; \ No newline at end of file diff --git a/frontend/dist/assets/Protokollfenster-BQilNnAU.js b/frontend/dist/assets/Protokollfenster-BQilNnAU.js deleted file mode 100644 index 931ad99..0000000 --- a/frontend/dist/assets/Protokollfenster-BQilNnAU.js +++ /dev/null @@ -1 +0,0 @@ -import{n as e,r as t}from"./dist-BaE5-hhJ.js";import{a as n,i as r,n as i,r as a,t as o}from"./sheet-iGGgLZLg.js";var s=e(),c=t();function l(e){let t=(0,s.c)(14),{offen:l,titel:u,text:d,onSchliessen:f}=e,p;t[0]===f?p=t[1]:(p=e=>!e&&f(),t[0]=f,t[1]=p);let m;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(m=(0,c.jsx)(n,{className:`schild text-lg`,children:`Protokoll`}),t[2]=m):m=t[2];let h;t[3]===u?h=t[4]:(h=(0,c.jsxs)(r,{children:[m,(0,c.jsx)(a,{className:`text-text-2`,children:u})]}),t[3]=u,t[4]=h);let g;t[5]===d?g=t[6]:(g=(0,c.jsx)(`pre`,{className:`ziffern mx-4 mb-4 flex-1 overflow-auto rounded-lg border border-linie bg-[#0b0d0f] p-3 text-xs leading-relaxed whitespace-pre-wrap text-text-2`,children:d}),t[5]=d,t[6]=g);let _;t[7]!==h||t[8]!==g?(_=(0,c.jsxs)(i,{side:`right`,className:`border-linie bg-panel data-[side=right]:w-full data-[side=right]:sm:max-w-2xl`,children:[h,g]}),t[7]=h,t[8]=g,t[9]=_):_=t[9];let v;return t[10]!==l||t[11]!==p||t[12]!==_?(v=(0,c.jsx)(o,{open:l,onOpenChange:p,children:_}),t[10]=l,t[11]=p,t[12]=_,t[13]=v):v=t[13],v}export{l as Protokollfenster}; \ No newline at end of file diff --git a/frontend/dist/assets/Protokollfenster-C5MkHrP6.js b/frontend/dist/assets/Protokollfenster-C5MkHrP6.js new file mode 100644 index 0000000..dc79abb --- /dev/null +++ b/frontend/dist/assets/Protokollfenster-C5MkHrP6.js @@ -0,0 +1 @@ +import{c as e,s as t}from"./button-BheHBEml.js";import{a as n,i as r,n as i,r as a,t as o}from"./sheet-CKp82pOA.js";var s=t(),c=e();function l(e){let t=(0,s.c)(14),{offen:l,titel:u,text:d,onSchliessen:f}=e,p;t[0]===f?p=t[1]:(p=e=>!e&&f(),t[0]=f,t[1]=p);let m;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(m=(0,c.jsx)(n,{className:`schild text-lg`,children:`Protokoll`}),t[2]=m):m=t[2];let h;t[3]===u?h=t[4]:(h=(0,c.jsxs)(r,{children:[m,(0,c.jsx)(a,{className:`text-text-2`,children:u})]}),t[3]=u,t[4]=h);let g;t[5]===d?g=t[6]:(g=(0,c.jsx)(`pre`,{className:`ziffern mx-4 mb-4 flex-1 overflow-auto rounded-lg border border-linie bg-[#0b0d0f] p-3 text-xs leading-relaxed whitespace-pre-wrap text-text-2`,children:d}),t[5]=d,t[6]=g);let _;t[7]!==h||t[8]!==g?(_=(0,c.jsxs)(i,{side:`right`,className:`border-linie bg-panel data-[side=right]:w-full data-[side=right]:sm:max-w-2xl`,children:[h,g]}),t[7]=h,t[8]=g,t[9]=_):_=t[9];let v;return t[10]!==l||t[11]!==p||t[12]!==_?(v=(0,c.jsx)(o,{open:l,onOpenChange:p,children:_}),t[10]=l,t[11]=p,t[12]=_,t[13]=v):v=t[13],v}export{l as Protokollfenster}; \ No newline at end of file diff --git a/frontend/dist/assets/Updates-D2MUBOp8.js b/frontend/dist/assets/Updates-D2MUBOp8.js new file mode 100644 index 0000000..c4900aa --- /dev/null +++ b/frontend/dist/assets/Updates-D2MUBOp8.js @@ -0,0 +1,2 @@ +import{c as e,i as t,o as n,p as r,s as i,t as a,u as o}from"./button-BheHBEml.js";import{A as s,D as c,E as l,L as u,O as d,T as f,c as p,h as m,j as h,n as g,p as _,r as ee,t as v,w as te}from"./index-SRwQwEnh.js";import{t as y}from"./Bestaetigen-CE_3b0PK.js";import{a as b,i as x,n as S,s as ne,t as re}from"./Auftraege-C3JDjG9Z.js";var C={name:`chevron-right`,size:24,node:[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]};C.node;var w=n(C),T=i(),ie=r(o(),1),E=e(),D=[{id:`os`,name:`Betriebssystem`},{id:`engine`,name:`Motor (llama.cpp)`},{id:`swap`,name:`llama-swap`},{id:`hermes`,name:`Hermes`}],O={os:`Die Ubuntu-Pakete werden eingespielt, danach prüft die Box, ob die Modelle noch antworten. Das kann einige Minuten dauern.`,engine:`Der Motor wird ersetzt, llama-swap startet dafür neu und die Modelle sind kurz nicht erreichbar. Klappt die Prüfung danach nicht, holt die Box den alten Stand von selbst zurück.`,swap:`llama-swap wird ersetzt und startet neu, die Modelle sind kurz nicht erreichbar. Klappt die Prüfung danach nicht, holt die Box den alten Stand von selbst zurück.`,hermes:`Vorher wird gesichert. Dann wird Hermes aktualisiert und startet neu — Lucy ist dabei ein paar Minuten nicht erreichbar.`};function k(e){let n=(0,T.c)(7),{breit:r}=e,i=r===void 0?`w-16`:r,a;n[0]===i?a=n[1]:(a=t(`inline-block h-3.5 rounded bg-linie motion-safe:animate-pulse`,i),n[0]=i,n[1]=a);let o;n[2]===a?o=n[3]:(o=(0,E.jsx)(`span`,{"aria-hidden":!0,className:a}),n[2]=a,n[3]=o);let s;n[4]===Symbol.for(`react.memo_cache_sentinel`)?(s=(0,E.jsx)(`span`,{className:`sr-only`,children:`wird geprüft`}),n[4]=s):s=n[4];let c;return n[5]===o?c=n[6]:(c=(0,E.jsxs)(`span`,{className:`inline-flex items-center`,children:[o,s]}),n[5]=o,n[6]=c),c}function ae(e){let n=(0,T.c)(13),{text:r}=e,[i,a]=(0,ie.useState)(!1),o;n[0]===r?o=n[1]:(o=r.split(` +`).length>3||r.length>240,n[0]=r,n[1]=o);let s=o,c=s&&!i&&`line-clamp-3`,l;n[2]===c?l=n[3]:(l=t(`m-0 whitespace-pre-line`,c),n[2]=c,n[3]=l);let u;n[4]!==l||n[5]!==r?(u=(0,E.jsx)(`p`,{className:l,children:r}),n[4]=l,n[5]=r,n[6]=u):u=n[6];let d;n[7]!==s||n[8]!==i?(d=s&&(0,E.jsx)(`button`,{type:`button`,"aria-expanded":i,onClick:()=>a(!i),className:`-mb-2 inline-flex h-11 items-center text-sm text-cyan hover:underline`,children:i?`Weniger zeigen`:`Ganze Zusammenfassung zeigen`}),n[7]=s,n[8]=i,n[9]=d):d=n[9];let f;return n[10]!==u||n[11]!==d?(f=(0,E.jsxs)(`div`,{className:`flex flex-col items-start`,children:[u,d]}),n[10]=u,n[11]=d,n[12]=f):f=n[12],f}function oe(e){let t=(0,T.c)(13),{d:n}=e,r;t[0]===n.packages?r=t[1]:(r=n.packages??[],t[0]=n.packages,t[1]=r);let i=r,a,o;if(t[2]!==n.held_back||t[3]!==i){o=Symbol.for(`react.early_return_sentinel`);bb0:{let e=n.held_back??[];if(i.length===0&&e.length===0){o=null;break bb0}let r;t[6]===Symbol.for(`react.memo_cache_sentinel`)?(r=(0,E.jsx)(w,{"aria-hidden":!0,className:`size-4 transition-transform group-open:rotate-90`}),t[6]=r):r=t[6];let s=i.length===1?`Das Paket ansehen`:`Alle ${i.length} Pakete ansehen`,c;t[7]===s?c=t[8]:(c=(0,E.jsxs)(`summary`,{className:`inline-flex h-11 cursor-pointer items-center gap-1 text-sm text-cyan hover:underline`,children:[r,s]}),t[7]=s,t[8]=c);let l;t[9]===i?l=t[10]:(l=i.map(j),t[9]=i,t[10]=l);let u;t[11]===l?u=t[12]:(u=(0,E.jsx)(`ul`,{className:`ziffern m-0 flex max-h-64 list-none flex-col gap-1 overflow-auto rounded-md border border-linie bg-[#0b0d0f] p-2.5 text-xs`,children:l}),t[11]=l,t[12]=u),a=(0,E.jsxs)(`details`,{className:`group`,children:[c,u,e.length>0&&(0,E.jsxs)(`p`,{className:`mt-2 text-xs text-text-3`,children:[`Dazu `,e.length===1?`kommt ein Paket`:`kommen ${e.length} Pakete`,`, die Ubuntu noch zurückhält (`,e.map(A).join(`, `),`). Die kommen von selbst, wenn Ubuntu sie freigibt.`]})]})}t[2]=n.held_back,t[3]=i,t[4]=a,t[5]=o}else a=t[4],o=t[5];return o===Symbol.for(`react.early_return_sentinel`)?a:o}function A(e){return e.name}function j(e){return(0,E.jsxs)(`li`,{className:`break-all`,children:[(0,E.jsx)(`span`,{className:`text-foreground`,children:e.name}),` `,(0,E.jsxs)(`span`,{className:`text-text-3`,children:[e.current,` → `,e.candidate]})]},e.name)}function M(e){let t=(0,T.c)(6),{d:n}=e;if(n.action_needed===!1){let e;return t[0]===Symbol.for(`react.memo_cache_sentinel`)?(e=(0,E.jsx)(g,{art:`gruen`,children:`Nichts zu tun`}),t[0]=e):e=t[0],e}if(n.action_needed!==!0)return null;let r;t[1]===Symbol.for(`react.memo_cache_sentinel`)?(r=(0,E.jsx)(g,{art:`bernstein`,children:`Du musst etwas tun`}),t[1]=r):r=t[1];let i;t[2]===n.action_text?i=t[3]:(i=n.action_text&&(0,E.jsx)(`span`,{className:`text-bernstein`,children:n.action_text}),t[2]=n.action_text,t[3]=i);let a;return t[4]===i?a=t[5]:(a=(0,E.jsxs)(`span`,{className:`flex flex-col items-start gap-1.5`,children:[r,i]}),t[4]=i,t[5]=a),a}function se(e){let t=e?.trim();return t?t.startsWith(`(Zusammenfassung nicht verfügbar`)?`Die Zusammenfassung ist gerade nicht verfügbar.`:t:null}function N(e){let t=(0,T.c)(63),{id:n,name:r,details:i,hatUpdate:o,neuLautStart:s,fest:c,gesperrt:l,onStart:u,onFreigeben:d}=e,f=i.data,p=i.isPending,m=i.isError?i.error.message:f?.error,h;t[0]!==f||t[1]!==n?(h=f?ne(n,f):null,t[0]=f,t[1]=n,t[2]=h):h=t[2];let g=h,_=o?g?.neu??s??null:null,ee=f?.summary,v;t[3]===ee?v=t[4]:(v=se(ee),t[3]=ee,t[4]=v);let te=v,y=g&&g.laeuft!==`–`?g.laeuft:m?`unbekannt`:`–`,x;t[5]!==p||t[6]!==y?(x=p?(0,E.jsx)(k,{}):(0,E.jsx)(E.Fragment,{children:y}),t[5]=p,t[6]=y,t[7]=x):x=t[7];let S=x,re;t[8]!==m||t[9]!==p||t[10]!==_?(re=p?(0,E.jsx)(k,{breit:`w-20`}):_?(0,E.jsx)(`span`,{className:`text-cyan`,children:_}):m?(0,E.jsx)(`span`,{className:`text-bernstein`,children:`nicht geprüft`}):(0,E.jsx)(`span`,{className:`text-text-3`,children:`–`}),t[8]=m,t[9]=p,t[10]=_,t[11]=re):re=t[11];let C=re,w;if(p){let e;t[12]===Symbol.for(`react.memo_cache_sentinel`)?(e=(0,E.jsx)(`span`,{className:`text-text-3`,children:`Wird geprüft …`}),t[12]=e):e=t[12],w=e}else if(m&&!o){let e;t[13]===m?e=t[14]:(e=b(m),t[13]=m,t[14]=e);let n;t[15]===e?n=t[16]:(n=(0,E.jsx)(`span`,{className:`text-bernstein`,children:e}),t[15]=e,t[16]=n),w=n}else if(!o)w=`Aktuell.`;else{let e;t[17]===m?e=t[18]:(e=m&&(0,E.jsx)(`span`,{className:`text-bernstein`,children:b(m)}),t[17]=m,t[18]=e);let r;t[19]===f?r=t[20]:(r=f&&(0,E.jsx)(M,{d:f}),t[19]=f,t[20]=r);let i;t[21]!==f||t[22]!==n||t[23]!==te?(i=n===`os`?(0,E.jsxs)(E.Fragment,{children:[(0,E.jsx)(`span`,{children:f?.count?`${f.count} ${f.count===1?`Paket`:`Pakete`} aus Ubuntu.`:`Neue Pakete aus Ubuntu.`}),f&&(0,E.jsx)(oe,{d:f})]}):te?(0,E.jsx)(ae,{text:te}):null,t[21]=f,t[22]=n,t[23]=te,t[24]=i):i=t[24];let a;t[25]!==e||t[26]!==r||t[27]!==i?(a=(0,E.jsxs)(`div`,{className:`flex flex-col items-start gap-2`,children:[e,r,i]}),t[25]=e,t[26]=r,t[27]=i,t[28]=a):a=t[28],w=a}let ie=p||void 0,D;t[29]===r?D=t[30]:(D=(0,E.jsx)(`span`,{role:`rowheader`,className:`text-[15px] font-semibold`,children:r}),t[29]=r,t[30]=D);let O;t[31]!==m||t[32]!==p||t[33]!==_||t[34]!==C?(O=!p&&(_||m)&&(0,E.jsxs)(E.Fragment,{children:[` `,_?`→`:`·`,` `,C]}),t[31]=m,t[32]=p,t[33]=_,t[34]=C,t[35]=O):O=t[35];let A;t[36]!==S||t[37]!==O?(A=(0,E.jsxs)(`span`,{role:`cell`,className:`ziffern -mt-1 text-sm text-text-2 md:hidden`,children:[S,O]}),t[36]=S,t[37]=O,t[38]=A):A=t[38];let j;t[39]===S?j=t[40]:(j=(0,E.jsx)(`span`,{role:`cell`,className:`ziffern hidden text-sm text-text-2 md:block`,children:S}),t[39]=S,t[40]=j);let N;t[41]===C?N=t[42]:(N=(0,E.jsx)(`span`,{role:`cell`,className:`ziffern hidden text-sm md:block`,children:C}),t[41]=C,t[42]=N);let P;t[43]!==c||t[44]!==r?(P=c&&(0,E.jsxs)(`span`,{className:`mb-1.5 block text-bernstein`,children:[`Festgehalten seit `,c.seit,` auf `,c.version,`: `,c.grund,`. Der Sonntags-Lauf überspringt `,r,`.`]}),t[43]=c,t[44]=r,t[45]=P):P=t[45];let F;t[46]!==P||t[47]!==w?(F=(0,E.jsxs)(`div`,{role:`cell`,className:`text-sm leading-relaxed text-text-2`,children:[P,w]}),t[46]=P,t[47]=w,t[48]=F):F=t[48];let I;t[49]!==c||t[50]!==l||t[51]!==o||t[52]!==d||t[53]!==u?(I=(c||o)&&(0,E.jsxs)(`span`,{role:`cell`,className:`flex flex-wrap gap-2 md:flex-col md:items-end`,children:[c&&(0,E.jsx)(a,{variant:`outline`,size:`sm`,onClick:d,children:`Freigeben`}),o&&(0,E.jsx)(a,{variant:`outline`,size:`sm`,onClick:u,disabled:l,children:`Aktualisieren`})]}),t[49]=c,t[50]=l,t[51]=o,t[52]=d,t[53]=u,t[54]=I):I=t[54];let L;return t[55]!==j||t[56]!==N||t[57]!==F||t[58]!==I||t[59]!==ie||t[60]!==D||t[61]!==A?(L=(0,E.jsxs)(`div`,{role:`row`,"aria-busy":ie,className:`grid grid-cols-1 gap-x-4 gap-y-2 border-t border-linie py-3.5 md:grid-cols-[170px_100px_170px_minmax(0,1fr)_auto] md:items-start`,children:[D,A,j,N,F,I]}),t[55]=j,t[56]=N,t[57]=F,t[58]=I,t[59]=ie,t[60]=D,t[61]=A,t[62]=L):L=t[62],L}var P={rot:`rot`,gelb:`bernstein`,gruen:`gruen`,info:`cyan`,grau:`grau`},F={rot:`bg-rot`,gelb:`bg-bernstein`,gruen:`bg-gruen`,info:`bg-cyan`,grau:`bg-text-3`};function I(e){let t=(0,T.c)(19),{lauf:n}=e,r;t[0]===n.start?r=t[1]:(r=h(n.start),t[0]=n.start,t[1]=r);let i;t[2]===r?i=t[3]:(i=(0,E.jsx)(`span`,{className:`ziffern col-start-1 row-start-1 text-sm text-text-3`,children:r}),t[2]=r,t[3]=i);let a;t[4]===n.anlass?a=t[5]:(a=(0,E.jsx)(`span`,{className:`col-span-2 row-start-2 min-w-0 text-[15px] break-words @sm:col-span-1 @sm:col-start-2 @sm:row-start-1`,children:n.anlass}),t[4]=n.anlass,t[5]=a);let o=P[n.stufe],s;t[6]!==n.titel||t[7]!==o?(s=(0,E.jsx)(`span`,{className:`col-start-2 row-start-1 justify-self-end @sm:col-start-3`,children:(0,E.jsx)(g,{art:o,children:n.titel})}),t[6]=n.titel,t[7]=o,t[8]=s):s=t[8];let c;t[9]!==i||t[10]!==a||t[11]!==s?(c=(0,E.jsxs)(`div`,{className:`grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-1 @sm:grid-cols-[112px_minmax(0,1fr)_auto]`,children:[i,a,s]}),t[9]=i,t[10]=a,t[11]=s,t[12]=c):c=t[12];let l;t[13]!==n.stufe||t[14]!==n.zeilen?(l=n.stufe!==`grau`&&n.zeilen.length>0&&(0,E.jsx)(`ul`,{className:`m-0 flex list-none flex-col gap-1 p-0 @sm:pl-[124px]`,children:n.zeilen.map(L)}),t[13]=n.stufe,t[14]=n.zeilen,t[15]=l):l=t[15];let u;return t[16]!==c||t[17]!==l?(u=(0,E.jsxs)(`li`,{className:`flex flex-col gap-2 border-t border-linie py-3`,children:[c,l]}),t[16]=c,t[17]=l,t[18]=u):u=t[18],u}function L(e){return(0,E.jsxs)(`li`,{className:`flex items-baseline gap-2 text-sm leading-snug text-text-2`,children:[(0,E.jsx)(`span`,{"aria-hidden":!0,className:t(`size-1.5 shrink-0 -translate-y-px rounded-full`,F[e.stufe])}),(0,E.jsxs)(`span`,{className:`min-w-0 break-words`,children:[(0,E.jsx)(`span`,{className:`text-foreground`,children:e.baustein}),` · `,e.text]})]},`${e.baustein}-${e.text}`)}function ce(e){return e.mtime?h(new Date(e.mtime*1e3).toISOString()):e.snapshot}function R(){let e=(0,T.c)(124),t=f(),n=m(),r=d(),i=te(),o=c(),u=p(),h=_(),g=s(),[b,x]=(0,ie.useState)(null),ne;e[0]===t.data?.updates.bausteine?ne=e[1]:(ne=new Map((t.data?.updates.bausteine??[]).map(me)),e[0]=t.data?.updates.bausteine,e[1]=ne);let C=ne,w=new Map((t.data?.updates.festgehalten??[]).map(pe)),k;e[2]===Symbol.for(`react.memo_cache_sentinel`)?(k=D.map(fe),e[2]=k):k=e[2];let ae=l(k),oe;e[3]!==ae||e[4]!==C?(oe=(e,t)=>S(e.id,ae[t].data)??C.has(e.id),e[3]=ae,e[4]=C,e[5]=oe):oe=e[5];let A=D.map(oe),j;e[6]===n.data?.jobs?j=e[7]:(j=n.data?.jobs??[],e[6]=n.data?.jobs,e[7]=j);let M=j,se;e[8]!==M||e[9]!==t.data?.zustand?(se=M.some(de)||(t.data?.zustand.update_laeuft??!1),e[8]=M,e[9]=t.data?.zustand,e[10]=se):se=e[10];let P=se,F;e[11]===M?F=e[12]:(F=M.some(ue),e[11]=M,e[12]=F);let I=F,L=P||o.isPending,R=i.data?.available??!1,z;if(t.isPending){let t;e[13]===Symbol.for(`react.memo_cache_sentinel`)?(t=(0,E.jsx)(ee,{text:`Updates werden gelesen …`}),e[13]=t):t=e[13],z=t}else if(t.isError&&!t.data){let n=`Die Box antwortet nicht: ${t.error.message}`,r;e[14]===n?r=e[15]:(r=(0,E.jsx)(ee,{fehler:!0,text:n}),e[14]=n,e[15]=r),z=r}else{let t;e[16]===Symbol.for(`react.memo_cache_sentinel`)?(t=(0,E.jsxs)(`div`,{role:`row`,className:`schild hidden grid-cols-[170px_100px_170px_minmax(0,1fr)_auto] gap-x-4 pb-2 text-xs font-semibold text-text-3 md:grid`,children:[(0,E.jsx)(`span`,{role:`columnheader`,children:`Baustein`}),(0,E.jsx)(`span`,{role:`columnheader`,children:`Läuft`}),(0,E.jsx)(`span`,{role:`columnheader`,children:`Neu`}),(0,E.jsx)(`span`,{role:`columnheader`,children:`Was sich ändert`}),(0,E.jsx)(`span`,{role:`columnheader`,className:`sr-only`,children:`Aktion`})]}),e[16]=t):t=e[16],z=(0,E.jsxs)(`div`,{role:`table`,"aria-label":`Bausteine und ihre Updates`,className:`flex flex-col`,children:[t,D.map((e,t)=>(0,E.jsx)(N,{id:e.id,name:e.name,details:ae[t],hatUpdate:A[t],neuLautStart:C.get(e.id),fest:w.get(e.id),gesperrt:L,onStart:()=>x({art:`einzeln`,id:e.id,name:e.name}),onFreigeben:()=>{let t=w.get(e.id);t&&x({art:`freigeben`,id:e.id,name:e.name,grund:t.grund})}},e.id))]})}let B;if(r.isPending){let t;e[17]===Symbol.for(`react.memo_cache_sentinel`)?(t=(0,E.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Wird gelesen …`}),e[17]=t):t=e[17],B=t}else if(r.isError&&!r.data){let t;e[18]===r.error.message?t=e[19]:(t=(0,E.jsxs)(`p`,{role:`alert`,className:`text-[15px] text-rot-text`,children:[`Der Verlauf lässt sich gerade nicht lesen: `,r.error.message]}),e[18]=r.error.message,e[19]=t),B=t}else if(r.data?.laeufe.length){let t;e[21]===r.data.laeufe?t=e[22]:(t=r.data.laeufe.map(le),e[21]=r.data.laeufe,e[22]=t);let n;e[23]===t?n=e[24]:(n=(0,E.jsx)(`ol`,{className:`@container m-0 flex list-none flex-col p-0`,children:t}),e[23]=t,e[24]=n),B=n}else{let t;e[20]===Symbol.for(`react.memo_cache_sentinel`)?(t=(0,E.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Noch keine Update-Läufe aufgezeichnet.`}),e[20]=t):t=e[20],B=t}let V;if(i.isPending){let t;e[25]===Symbol.for(`react.memo_cache_sentinel`)?(t=(0,E.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Wird gelesen …`}),e[25]=t):t=e[25],V=t}else if(i.isError&&!i.data){let t;e[26]===i.error.message?t=e[27]:(t=(0,E.jsxs)(`p`,{role:`alert`,className:`text-[15px] text-rot-text`,children:[`Die Sicherungen lassen sich gerade nicht lesen: `,i.error.message]}),e[26]=i.error.message,e[27]=t),V=t}else if(i.data?.backups.length){let t;if(e[29]!==i.data.backups||e[30]!==g.isPending||e[31]!==R){let n;e[33]!==g.isPending||e[34]!==R?(n=e=>(0,E.jsxs)(`li`,{className:`flex items-center justify-between gap-3 border-t border-linie py-2.5`,children:[(0,E.jsx)(`span`,{className:`ziffern text-sm`,children:ce(e)}),(0,E.jsxs)(`span`,{className:`flex items-center gap-3`,children:[(0,E.jsxs)(`span`,{className:`ziffern text-xs text-text-3`,children:[e.size_mb.toLocaleString(`de-DE`),` MB`]}),(0,E.jsx)(a,{variant:`ghost`,size:`sm`,disabled:!R||g.isPending,onClick:()=>x({art:`zurueck`,datei:e.file,zeit:ce(e)}),children:`Zurückspielen`})]})]},e.file),e[33]=g.isPending,e[34]=R,e[35]=n):n=e[35],t=i.data.backups.slice(0,5).map(n),e[29]=i.data.backups,e[30]=g.isPending,e[31]=R,e[32]=t}else t=e[32];let n;e[36]===t?n=e[37]:(n=(0,E.jsx)(`ul`,{className:`m-0 flex list-none flex-col p-0`,children:t}),e[36]=t,e[37]=n),V=n}else{let t;e[28]===Symbol.for(`react.memo_cache_sentinel`)?(t=(0,E.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Noch keine Sicherung vorhanden.`}),e[28]=t):t=e[28],V=t}let he=v,H;e[38]===o?H=e[39]:(H=()=>o.mutate(`pruefen`),e[38]=o,e[39]=H);let ge=o.isPending||I,_e=I?`Sucht …`:`Nach Neuem suchen`,U;e[40]!==_e||e[41]!==H||e[42]!==ge?(U=(0,E.jsx)(a,{variant:`outline`,onClick:H,disabled:ge,children:_e}),e[40]=_e,e[41]=H,e[42]=ge,e[43]=U):U=e[43];let ve=a,W;e[44]===Symbol.for(`react.memo_cache_sentinel`)?(W=()=>x({art:`alle`}),e[44]=W):W=e[44];let ye=L||!A.some(Boolean),G;e[45]!==ve||e[46]!==W||e[47]!==ye?(G=(0,E.jsx)(ve,{onClick:W,disabled:ye,children:`Alles jetzt aktualisieren`}),e[45]=ve,e[46]=W,e[47]=ye,e[48]=G):G=e[48];let K;e[49]!==U||e[50]!==G?(K=(0,E.jsxs)(`div`,{className:`flex flex-wrap gap-2.5`,children:[U,G]}),e[49]=U,e[50]=G,e[51]=K):K=e[51];let be;e[52]===Symbol.for(`react.memo_cache_sentinel`)?(be=(0,E.jsx)(`p`,{className:`-mt-2 text-[15px] text-text-2`,children:`Jeden Sonntag um 04:30 automatisch. Vorher wird gesichert, danach geprüft.`}),e[52]=be):be=e[52];let q;e[53]===P?q=e[54]:(q=P&&(0,E.jsx)(`p`,{role:`status`,className:`text-[15px] text-cyan`,children:`Gerade läuft ein Update. Weitere lassen sich erst starten, wenn es fertig ist.`}),e[53]=P,e[54]=q);let xe;e[55]===Symbol.for(`react.memo_cache_sentinel`)?(xe=(0,E.jsx)(re,{}),e[55]=xe):xe=e[55];let Se;e[56]!==he||e[57]!==K||e[58]!==q||e[59]!==z?(Se=(0,E.jsxs)(he,{titel:`Updates`,rechts:K,children:[be,q,z,xe]}),e[56]=he,e[57]=K,e[58]=q,e[59]=z,e[60]=Se):Se=e[60];let J;e[61]===B?J=e[62]:(J=(0,E.jsx)(v,{titel:`Verlauf`,children:B}),e[61]=B,e[62]=J);let Y;e[63]===h?Y=e[64]:(Y=()=>h.mutate(void 0),e[63]=h,e[64]=Y);let Ce=h.isPending?`Sichert …`:`Jetzt sichern`,X;e[65]!==h.isPending||e[66]!==Y||e[67]!==Ce?(X=(0,E.jsx)(a,{variant:`outline`,size:`sm`,onClick:Y,disabled:h.isPending,children:Ce}),e[65]=h.isPending,e[66]=Y,e[67]=Ce,e[68]=X):X=e[68];let we;e[69]===Symbol.for(`react.memo_cache_sentinel`)?(we=(0,E.jsx)(`p`,{className:`-mt-2 text-[15px] text-text-2`,children:`Täglich um 03:35, mit Kopie auf einem zweiten Gerät.`}),e[69]=we):we=e[69];let Z;e[70]!==i.data||e[71]!==R?(Z=i.data&&!R&&(0,E.jsx)(`p`,{className:`text-sm text-text-3`,children:`Zurückspielen geht nur auf der Box selbst.`}),e[70]=i.data,e[71]=R,e[72]=Z):Z=e[72];let Q;e[73]!==V||e[74]!==X||e[75]!==Z?(Q=(0,E.jsxs)(v,{titel:`Sicherungen`,rechts:X,children:[we,Z,V]}),e[73]=V,e[74]=X,e[75]=Z,e[76]=Q):Q=e[76];let Te;e[77]!==J||e[78]!==Q?(Te=(0,E.jsxs)(`div`,{className:`grid gap-5 md:gap-6 lg:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]`,children:[J,Q]}),e[77]=J,e[78]=Q,e[79]=Te):Te=e[79];let Ee=b?.art===`alle`,De;e[80]===Symbol.for(`react.memo_cache_sentinel`)?(De=()=>x(null),e[80]=De):De=e[80];let Oe;e[81]!==b?.art||e[82]!==o?(Oe=()=>{b?.art===`alle`&&o.mutate(`alle`),x(null)},e[81]=b?.art,e[82]=o,e[83]=Oe):Oe=e[83];let ke;e[84]!==L||e[85]!==Ee||e[86]!==Oe?(ke=(0,E.jsx)(y,{offen:Ee,titel:`Alles jetzt aktualisieren?`,text:`Motor, llama-swap, Hermes und Betriebssystem werden nacheinander aktualisiert, soweit es Neues gibt, und danach geprüft. Motor und llama-swap holen bei Problemen ihren alten Stand von selbst zurück, vor Hermes wird gesichert. Lucy ist dabei ein paar Minuten nicht erreichbar.`,knopf:`Aktualisieren`,gesperrt:L,onNein:De,onJa:Oe}),e[84]=L,e[85]=Ee,e[86]=Oe,e[87]=ke):ke=e[87];let Ae=b?.art===`einzeln`,je=b?.art===`einzeln`?`${b.name} aktualisieren?`:``,Me=b?.art===`einzeln`?O[b.id]??``:``,Ne;e[88]===Symbol.for(`react.memo_cache_sentinel`)?(Ne=()=>x(null),e[88]=Ne):Ne=e[88];let Pe;e[89]!==b||e[90]!==o?(Pe=()=>{b?.art===`einzeln`&&o.mutate(b.id),x(null)},e[89]=b,e[90]=o,e[91]=Pe):Pe=e[91];let Fe;e[92]!==L||e[93]!==Ae||e[94]!==je||e[95]!==Me||e[96]!==Pe?(Fe=(0,E.jsx)(y,{offen:Ae,titel:je,text:Me,knopf:`Aktualisieren`,gesperrt:L,onNein:Ne,onJa:Pe}),e[92]=L,e[93]=Ae,e[94]=je,e[95]=Me,e[96]=Pe,e[97]=Fe):Fe=e[97];let Ie=b?.art===`freigeben`,Le=b?.art===`freigeben`?`${b.name} freigeben?`:``,Re=b?.art===`freigeben`?`Festgehalten wurde ${b.name}, weil: ${b.grund}. Nach dem Freigeben versucht der nächste Sonntags-Lauf das Update wieder.`:``,ze;e[98]===Symbol.for(`react.memo_cache_sentinel`)?(ze=()=>x(null),e[98]=ze):ze=e[98];let Be;e[99]!==b||e[100]!==u?(Be=()=>{b?.art===`freigeben`&&u.mutate(b.id),x(null)},e[99]=b,e[100]=u,e[101]=Be):Be=e[101];let Ve;e[102]!==u.isPending||e[103]!==Ie||e[104]!==Le||e[105]!==Re||e[106]!==Be?(Ve=(0,E.jsx)(y,{offen:Ie,titel:Le,text:Re,knopf:`Freigeben`,gesperrt:u.isPending,onNein:ze,onJa:Be}),e[102]=u.isPending,e[103]=Ie,e[104]=Le,e[105]=Re,e[106]=Be,e[107]=Ve):Ve=e[107];let He=b?.art===`zurueck`,Ue=`Die Einstellungen von Hermes und llama-swap werden auf den Stand von ${b?.art===`zurueck`?b.zeit:``} gesetzt. Vorher wird der jetzige Stand gesichert. Die Dienste starten danach neu.`,We;e[108]===Symbol.for(`react.memo_cache_sentinel`)?(We=()=>x(null),e[108]=We):We=e[108];let Ge;e[109]!==b||e[110]!==g?(Ge=()=>{b?.art===`zurueck`&&g.mutate(b.datei),x(null)},e[109]=b,e[110]=g,e[111]=Ge):Ge=e[111];let $;e[112]!==He||e[113]!==Ue||e[114]!==Ge||e[115]!==g.isPending?($=(0,E.jsx)(y,{offen:He,gefahr:!0,titel:`Sicherung zurückspielen?`,text:Ue,knopf:`Zurückspielen`,gesperrt:g.isPending,onNein:We,onJa:Ge}),e[112]=He,e[113]=Ue,e[114]=Ge,e[115]=g.isPending,e[116]=$):$=e[116];let Ke;return e[117]!==Se||e[118]!==Te||e[119]!==ke||e[120]!==Fe||e[121]!==Ve||e[122]!==$?(Ke=(0,E.jsxs)(E.Fragment,{children:[Se,Te,ke,Fe,Ve,$]}),e[117]=Se,e[118]=Te,e[119]=ke,e[120]=Fe,e[121]=Ve,e[122]=$,e[123]=Ke):Ke=e[123],Ke}function le(e){return(0,E.jsx)(I,{lauf:e},e.start)}function ue(e){return x(e)&&u(e)}function de(e){return e.group===`maintenance`&&u(e)}function fe(e){return e.id}function pe(e){return[e.baustein,e]}function me(e){return[e.id,e.neu]}export{R as UpdatesSeite}; \ No newline at end of file diff --git a/frontend/dist/assets/Updates-U5rB4cSU.js b/frontend/dist/assets/Updates-U5rB4cSU.js deleted file mode 100644 index 399d0d6..0000000 --- a/frontend/dist/assets/Updates-U5rB4cSU.js +++ /dev/null @@ -1,2 +0,0 @@ -import{i as e,n as t,o as n,r,t as i}from"./dist-BaE5-hhJ.js";import{C as a,D as o,E as s,J as c,N as l,S as u,T as d,f,n as p,r as m,s as ee,t as h,u as te,w as ne,x as re,z as g}from"./index-DSxwfDVg.js";import{Bestaetigen as _}from"./Bestaetigen-kFure1oW.js";import{a as v,i as y,n as b,s as ie,t as ae}from"./Auftraege-C5bKfsD1.js";var x={name:`chevron-right`,size:24,node:[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]};x.node;var S=c(x),C=t(),oe=n(e(),1),w=r(),T=[{id:`os`,name:`Betriebssystem`},{id:`engine`,name:`Motor (llama.cpp)`},{id:`swap`,name:`llama-swap`},{id:`hermes`,name:`Hermes`}],E={os:`Die Ubuntu-Pakete werden eingespielt, danach prüft die Box, ob die Modelle noch antworten. Das kann einige Minuten dauern.`,engine:`Der Motor wird ersetzt, llama-swap startet dafür neu und die Modelle sind kurz nicht erreichbar. Klappt die Prüfung danach nicht, holt die Box den alten Stand von selbst zurück.`,swap:`llama-swap wird ersetzt und startet neu, die Modelle sind kurz nicht erreichbar. Klappt die Prüfung danach nicht, holt die Box den alten Stand von selbst zurück.`,hermes:`Vorher wird gesichert. Dann wird Hermes aktualisiert und startet neu — Lucy ist dabei ein paar Minuten nicht erreichbar.`};function D(e){let t=(0,C.c)(7),{breit:n}=e,r=n===void 0?`w-16`:n,a;t[0]===r?a=t[1]:(a=i(`inline-block h-3.5 rounded bg-linie motion-safe:animate-pulse`,r),t[0]=r,t[1]=a);let o;t[2]===a?o=t[3]:(o=(0,w.jsx)(`span`,{"aria-hidden":!0,className:a}),t[2]=a,t[3]=o);let s;t[4]===Symbol.for(`react.memo_cache_sentinel`)?(s=(0,w.jsx)(`span`,{className:`sr-only`,children:`wird geprüft`}),t[4]=s):s=t[4];let c;return t[5]===o?c=t[6]:(c=(0,w.jsxs)(`span`,{className:`inline-flex items-center`,children:[o,s]}),t[5]=o,t[6]=c),c}function se(e){let t=(0,C.c)(13),{text:n}=e,[r,a]=(0,oe.useState)(!1),o;t[0]===n?o=t[1]:(o=n.split(` -`).length>3||n.length>240,t[0]=n,t[1]=o);let s=o,c=s&&!r&&`line-clamp-3`,l;t[2]===c?l=t[3]:(l=i(`m-0 whitespace-pre-line`,c),t[2]=c,t[3]=l);let u;t[4]!==l||t[5]!==n?(u=(0,w.jsx)(`p`,{className:l,children:n}),t[4]=l,t[5]=n,t[6]=u):u=t[6];let d;t[7]!==s||t[8]!==r?(d=s&&(0,w.jsx)(`button`,{type:`button`,"aria-expanded":r,onClick:()=>a(!r),className:`-mb-2 inline-flex h-11 items-center text-sm text-cyan hover:underline`,children:r?`Weniger zeigen`:`Ganze Zusammenfassung zeigen`}),t[7]=s,t[8]=r,t[9]=d):d=t[9];let f;return t[10]!==u||t[11]!==d?(f=(0,w.jsxs)(`div`,{className:`flex flex-col items-start`,children:[u,d]}),t[10]=u,t[11]=d,t[12]=f):f=t[12],f}function ce(e){let t=(0,C.c)(13),{d:n}=e,r;t[0]===n.packages?r=t[1]:(r=n.packages??[],t[0]=n.packages,t[1]=r);let i=r,a,o;if(t[2]!==n.held_back||t[3]!==i){o=Symbol.for(`react.early_return_sentinel`);bb0:{let e=n.held_back??[];if(i.length===0&&e.length===0){o=null;break bb0}let r;t[6]===Symbol.for(`react.memo_cache_sentinel`)?(r=(0,w.jsx)(S,{"aria-hidden":!0,className:`size-4 transition-transform group-open:rotate-90`}),t[6]=r):r=t[6];let s=i.length===1?`Das Paket ansehen`:`Alle ${i.length} Pakete ansehen`,c;t[7]===s?c=t[8]:(c=(0,w.jsxs)(`summary`,{className:`inline-flex h-11 cursor-pointer items-center gap-1 text-sm text-cyan hover:underline`,children:[r,s]}),t[7]=s,t[8]=c);let l;t[9]===i?l=t[10]:(l=i.map(k),t[9]=i,t[10]=l);let u;t[11]===l?u=t[12]:(u=(0,w.jsx)(`ul`,{className:`ziffern m-0 flex max-h-64 list-none flex-col gap-1 overflow-auto rounded-md border border-linie bg-[#0b0d0f] p-2.5 text-xs`,children:l}),t[11]=l,t[12]=u),a=(0,w.jsxs)(`details`,{className:`group`,children:[c,u,e.length>0&&(0,w.jsxs)(`p`,{className:`mt-2 text-xs text-text-3`,children:[`Dazu `,e.length===1?`kommt ein Paket`:`kommen ${e.length} Pakete`,`, die Ubuntu noch zurückhält (`,e.map(O).join(`, `),`). Die kommen von selbst, wenn Ubuntu sie freigibt.`]})]})}t[2]=n.held_back,t[3]=i,t[4]=a,t[5]=o}else a=t[4],o=t[5];return o===Symbol.for(`react.early_return_sentinel`)?a:o}function O(e){return e.name}function k(e){return(0,w.jsxs)(`li`,{className:`break-all`,children:[(0,w.jsx)(`span`,{className:`text-foreground`,children:e.name}),` `,(0,w.jsxs)(`span`,{className:`text-text-3`,children:[e.current,` → `,e.candidate]})]},e.name)}function A(e){let t=(0,C.c)(6),{d:n}=e;if(n.action_needed===!1){let e;return t[0]===Symbol.for(`react.memo_cache_sentinel`)?(e=(0,w.jsx)(p,{art:`gruen`,children:`Nichts zu tun`}),t[0]=e):e=t[0],e}if(n.action_needed!==!0)return null;let r;t[1]===Symbol.for(`react.memo_cache_sentinel`)?(r=(0,w.jsx)(p,{art:`bernstein`,children:`Du musst etwas tun`}),t[1]=r):r=t[1];let i;t[2]===n.action_text?i=t[3]:(i=n.action_text&&(0,w.jsx)(`span`,{className:`text-bernstein`,children:n.action_text}),t[2]=n.action_text,t[3]=i);let a;return t[4]===i?a=t[5]:(a=(0,w.jsxs)(`span`,{className:`flex flex-col items-start gap-1.5`,children:[r,i]}),t[4]=i,t[5]=a),a}function le(e){let t=e?.trim();return t?t.startsWith(`(Zusammenfassung nicht verfügbar`)?`Die Zusammenfassung ist gerade nicht verfügbar.`:t:null}function j(e){let t=(0,C.c)(63),{id:n,name:r,details:i,hatUpdate:a,neuLautStart:o,fest:s,gesperrt:c,onStart:l,onFreigeben:u}=e,d=i.data,f=i.isPending,p=i.isError?i.error.message:d?.error,m;t[0]!==d||t[1]!==n?(m=d?ie(n,d):null,t[0]=d,t[1]=n,t[2]=m):m=t[2];let ee=m,h=a?ee?.neu??o??null:null,te=d?.summary,ne;t[3]===te?ne=t[4]:(ne=le(te),t[3]=te,t[4]=ne);let re=ne,_=ee&&ee.laeuft!==`–`?ee.laeuft:p?`unbekannt`:`–`,y;t[5]!==f||t[6]!==_?(y=f?(0,w.jsx)(D,{}):(0,w.jsx)(w.Fragment,{children:_}),t[5]=f,t[6]=_,t[7]=y):y=t[7];let b=y,ae;t[8]!==p||t[9]!==f||t[10]!==h?(ae=f?(0,w.jsx)(D,{breit:`w-20`}):h?(0,w.jsx)(`span`,{className:`text-cyan`,children:h}):p?(0,w.jsx)(`span`,{className:`text-bernstein`,children:`nicht geprüft`}):(0,w.jsx)(`span`,{className:`text-text-3`,children:`–`}),t[8]=p,t[9]=f,t[10]=h,t[11]=ae):ae=t[11];let x=ae,S;if(f){let e;t[12]===Symbol.for(`react.memo_cache_sentinel`)?(e=(0,w.jsx)(`span`,{className:`text-text-3`,children:`Wird geprüft …`}),t[12]=e):e=t[12],S=e}else if(p&&!a){let e;t[13]===p?e=t[14]:(e=v(p),t[13]=p,t[14]=e);let n;t[15]===e?n=t[16]:(n=(0,w.jsx)(`span`,{className:`text-bernstein`,children:e}),t[15]=e,t[16]=n),S=n}else if(!a)S=`Aktuell.`;else{let e;t[17]===p?e=t[18]:(e=p&&(0,w.jsx)(`span`,{className:`text-bernstein`,children:v(p)}),t[17]=p,t[18]=e);let r;t[19]===d?r=t[20]:(r=d&&(0,w.jsx)(A,{d}),t[19]=d,t[20]=r);let i;t[21]!==d||t[22]!==n||t[23]!==re?(i=n===`os`?(0,w.jsxs)(w.Fragment,{children:[(0,w.jsx)(`span`,{children:d?.count?`${d.count} ${d.count===1?`Paket`:`Pakete`} aus Ubuntu.`:`Neue Pakete aus Ubuntu.`}),d&&(0,w.jsx)(ce,{d})]}):re?(0,w.jsx)(se,{text:re}):null,t[21]=d,t[22]=n,t[23]=re,t[24]=i):i=t[24];let a;t[25]!==e||t[26]!==r||t[27]!==i?(a=(0,w.jsxs)(`div`,{className:`flex flex-col items-start gap-2`,children:[e,r,i]}),t[25]=e,t[26]=r,t[27]=i,t[28]=a):a=t[28],S=a}let oe=f||void 0,T;t[29]===r?T=t[30]:(T=(0,w.jsx)(`span`,{role:`rowheader`,className:`text-[15px] font-semibold`,children:r}),t[29]=r,t[30]=T);let E;t[31]!==p||t[32]!==f||t[33]!==h||t[34]!==x?(E=!f&&(h||p)&&(0,w.jsxs)(w.Fragment,{children:[` `,h?`→`:`·`,` `,x]}),t[31]=p,t[32]=f,t[33]=h,t[34]=x,t[35]=E):E=t[35];let O;t[36]!==b||t[37]!==E?(O=(0,w.jsxs)(`span`,{role:`cell`,className:`ziffern -mt-1 text-sm text-text-2 md:hidden`,children:[b,E]}),t[36]=b,t[37]=E,t[38]=O):O=t[38];let k;t[39]===b?k=t[40]:(k=(0,w.jsx)(`span`,{role:`cell`,className:`ziffern hidden text-sm text-text-2 md:block`,children:b}),t[39]=b,t[40]=k);let j;t[41]===x?j=t[42]:(j=(0,w.jsx)(`span`,{role:`cell`,className:`ziffern hidden text-sm md:block`,children:x}),t[41]=x,t[42]=j);let M;t[43]!==s||t[44]!==r?(M=s&&(0,w.jsxs)(`span`,{className:`mb-1.5 block text-bernstein`,children:[`Festgehalten seit `,s.seit,` auf `,s.version,`: `,s.grund,`. Der Sonntags-Lauf überspringt `,r,`.`]}),t[43]=s,t[44]=r,t[45]=M):M=t[45];let N;t[46]!==M||t[47]!==S?(N=(0,w.jsxs)(`div`,{role:`cell`,className:`text-sm leading-relaxed text-text-2`,children:[M,S]}),t[46]=M,t[47]=S,t[48]=N):N=t[48];let P;t[49]!==s||t[50]!==c||t[51]!==a||t[52]!==u||t[53]!==l?(P=(s||a)&&(0,w.jsxs)(`span`,{role:`cell`,className:`flex flex-wrap gap-2 md:flex-col md:items-end`,children:[s&&(0,w.jsx)(g,{variant:`outline`,size:`sm`,onClick:u,children:`Freigeben`}),a&&(0,w.jsx)(g,{variant:`outline`,size:`sm`,onClick:l,disabled:c,children:`Aktualisieren`})]}),t[49]=s,t[50]=c,t[51]=a,t[52]=u,t[53]=l,t[54]=P):P=t[54];let F;return t[55]!==k||t[56]!==j||t[57]!==N||t[58]!==P||t[59]!==oe||t[60]!==T||t[61]!==O?(F=(0,w.jsxs)(`div`,{role:`row`,"aria-busy":oe,className:`grid grid-cols-1 gap-x-4 gap-y-2 border-t border-linie py-3.5 md:grid-cols-[170px_100px_170px_minmax(0,1fr)_auto] md:items-start`,children:[T,O,k,j,N,P]}),t[55]=k,t[56]=j,t[57]=N,t[58]=P,t[59]=oe,t[60]=T,t[61]=O,t[62]=F):F=t[62],F}var M={rot:`rot`,gelb:`bernstein`,gruen:`gruen`,info:`cyan`,grau:`grau`},N={rot:`bg-rot`,gelb:`bg-bernstein`,gruen:`bg-gruen`,info:`bg-cyan`,grau:`bg-text-3`};function P(e){let t=(0,C.c)(19),{lauf:n}=e,r;t[0]===n.start?r=t[1]:(r=o(n.start),t[0]=n.start,t[1]=r);let i;t[2]===r?i=t[3]:(i=(0,w.jsx)(`span`,{className:`ziffern col-start-1 row-start-1 text-sm text-text-3`,children:r}),t[2]=r,t[3]=i);let a;t[4]===n.anlass?a=t[5]:(a=(0,w.jsx)(`span`,{className:`col-span-2 row-start-2 min-w-0 text-[15px] break-words @sm:col-span-1 @sm:col-start-2 @sm:row-start-1`,children:n.anlass}),t[4]=n.anlass,t[5]=a);let s=M[n.stufe],c;t[6]!==n.titel||t[7]!==s?(c=(0,w.jsx)(`span`,{className:`col-start-2 row-start-1 justify-self-end @sm:col-start-3`,children:(0,w.jsx)(p,{art:s,children:n.titel})}),t[6]=n.titel,t[7]=s,t[8]=c):c=t[8];let l;t[9]!==i||t[10]!==a||t[11]!==c?(l=(0,w.jsxs)(`div`,{className:`grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-1 @sm:grid-cols-[112px_minmax(0,1fr)_auto]`,children:[i,a,c]}),t[9]=i,t[10]=a,t[11]=c,t[12]=l):l=t[12];let u;t[13]!==n.stufe||t[14]!==n.zeilen?(u=n.stufe!==`grau`&&n.zeilen.length>0&&(0,w.jsx)(`ul`,{className:`m-0 flex list-none flex-col gap-1 p-0 @sm:pl-[124px]`,children:n.zeilen.map(F)}),t[13]=n.stufe,t[14]=n.zeilen,t[15]=u):u=t[15];let d;return t[16]!==l||t[17]!==u?(d=(0,w.jsxs)(`li`,{className:`flex flex-col gap-2 border-t border-linie py-3`,children:[l,u]}),t[16]=l,t[17]=u,t[18]=d):d=t[18],d}function F(e){return(0,w.jsxs)(`li`,{className:`flex items-baseline gap-2 text-sm leading-snug text-text-2`,children:[(0,w.jsx)(`span`,{"aria-hidden":!0,className:i(`size-1.5 shrink-0 -translate-y-px rounded-full`,N[e.stufe])}),(0,w.jsxs)(`span`,{className:`min-w-0 break-words`,children:[(0,w.jsx)(`span`,{className:`text-foreground`,children:e.baustein}),` · `,e.text]})]},`${e.baustein}-${e.text}`)}function ue(e){return e.mtime?o(new Date(e.mtime*1e3).toISOString()):e.snapshot}function I(){let e=(0,C.c)(124),t=u(),n=f(),r=d(),i=re(),o=ne(),c=ee(),l=te(),p=s(),[v,y]=(0,oe.useState)(null),ie;e[0]===t.data?.updates.bausteine?ie=e[1]:(ie=new Map((t.data?.updates.bausteine??[]).map(ge)),e[0]=t.data?.updates.bausteine,e[1]=ie);let x=ie,S=new Map((t.data?.updates.festgehalten??[]).map(he)),D;e[2]===Symbol.for(`react.memo_cache_sentinel`)?(D=T.map(me),e[2]=D):D=e[2];let se=a(D),ce;e[3]!==se||e[4]!==x?(ce=(e,t)=>b(e.id,se[t].data)??x.has(e.id),e[3]=se,e[4]=x,e[5]=ce):ce=e[5];let O=T.map(ce),k;e[6]===n.data?.jobs?k=e[7]:(k=n.data?.jobs??[],e[6]=n.data?.jobs,e[7]=k);let A=k,le;e[8]!==A||e[9]!==t.data?.zustand?(le=A.some(pe)||(t.data?.zustand.update_laeuft??!1),e[8]=A,e[9]=t.data?.zustand,e[10]=le):le=e[10];let M=le,N;e[11]===A?N=e[12]:(N=A.some(fe),e[11]=A,e[12]=N);let P=N,F=M||o.isPending,I=i.data?.available??!1,L;if(t.isPending){let t;e[13]===Symbol.for(`react.memo_cache_sentinel`)?(t=(0,w.jsx)(m,{text:`Updates werden gelesen …`}),e[13]=t):t=e[13],L=t}else if(t.isError&&!t.data){let n=`Die Box antwortet nicht: ${t.error.message}`,r;e[14]===n?r=e[15]:(r=(0,w.jsx)(m,{fehler:!0,text:n}),e[14]=n,e[15]=r),L=r}else{let t;e[16]===Symbol.for(`react.memo_cache_sentinel`)?(t=(0,w.jsxs)(`div`,{role:`row`,className:`schild hidden grid-cols-[170px_100px_170px_minmax(0,1fr)_auto] gap-x-4 pb-2 text-xs font-semibold text-text-3 md:grid`,children:[(0,w.jsx)(`span`,{role:`columnheader`,children:`Baustein`}),(0,w.jsx)(`span`,{role:`columnheader`,children:`Läuft`}),(0,w.jsx)(`span`,{role:`columnheader`,children:`Neu`}),(0,w.jsx)(`span`,{role:`columnheader`,children:`Was sich ändert`}),(0,w.jsx)(`span`,{role:`columnheader`,className:`sr-only`,children:`Aktion`})]}),e[16]=t):t=e[16],L=(0,w.jsxs)(`div`,{role:`table`,"aria-label":`Bausteine und ihre Updates`,className:`flex flex-col`,children:[t,T.map((e,t)=>(0,w.jsx)(j,{id:e.id,name:e.name,details:se[t],hatUpdate:O[t],neuLautStart:x.get(e.id),fest:S.get(e.id),gesperrt:F,onStart:()=>y({art:`einzeln`,id:e.id,name:e.name}),onFreigeben:()=>{let t=S.get(e.id);t&&y({art:`freigeben`,id:e.id,name:e.name,grund:t.grund})}},e.id))]})}let R;if(r.isPending){let t;e[17]===Symbol.for(`react.memo_cache_sentinel`)?(t=(0,w.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Wird gelesen …`}),e[17]=t):t=e[17],R=t}else if(r.isError&&!r.data){let t;e[18]===r.error.message?t=e[19]:(t=(0,w.jsxs)(`p`,{role:`alert`,className:`text-[15px] text-rot-text`,children:[`Der Verlauf lässt sich gerade nicht lesen: `,r.error.message]}),e[18]=r.error.message,e[19]=t),R=t}else if(r.data?.laeufe.length){let t;e[21]===r.data.laeufe?t=e[22]:(t=r.data.laeufe.map(de),e[21]=r.data.laeufe,e[22]=t);let n;e[23]===t?n=e[24]:(n=(0,w.jsx)(`ol`,{className:`@container m-0 flex list-none flex-col p-0`,children:t}),e[23]=t,e[24]=n),R=n}else{let t;e[20]===Symbol.for(`react.memo_cache_sentinel`)?(t=(0,w.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Noch keine Update-Läufe aufgezeichnet.`}),e[20]=t):t=e[20],R=t}let z;if(i.isPending){let t;e[25]===Symbol.for(`react.memo_cache_sentinel`)?(t=(0,w.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Wird gelesen …`}),e[25]=t):t=e[25],z=t}else if(i.isError&&!i.data){let t;e[26]===i.error.message?t=e[27]:(t=(0,w.jsxs)(`p`,{role:`alert`,className:`text-[15px] text-rot-text`,children:[`Die Sicherungen lassen sich gerade nicht lesen: `,i.error.message]}),e[26]=i.error.message,e[27]=t),z=t}else if(i.data?.backups.length){let t;if(e[29]!==i.data.backups||e[30]!==p.isPending||e[31]!==I){let n;e[33]!==p.isPending||e[34]!==I?(n=e=>(0,w.jsxs)(`li`,{className:`flex items-center justify-between gap-3 border-t border-linie py-2.5`,children:[(0,w.jsx)(`span`,{className:`ziffern text-sm`,children:ue(e)}),(0,w.jsxs)(`span`,{className:`flex items-center gap-3`,children:[(0,w.jsxs)(`span`,{className:`ziffern text-xs text-text-3`,children:[e.size_mb.toLocaleString(`de-DE`),` MB`]}),(0,w.jsx)(g,{variant:`ghost`,size:`sm`,disabled:!I||p.isPending,onClick:()=>y({art:`zurueck`,datei:e.file,zeit:ue(e)}),children:`Zurückspielen`})]})]},e.file),e[33]=p.isPending,e[34]=I,e[35]=n):n=e[35],t=i.data.backups.slice(0,5).map(n),e[29]=i.data.backups,e[30]=p.isPending,e[31]=I,e[32]=t}else t=e[32];let n;e[36]===t?n=e[37]:(n=(0,w.jsx)(`ul`,{className:`m-0 flex list-none flex-col p-0`,children:t}),e[36]=t,e[37]=n),z=n}else{let t;e[28]===Symbol.for(`react.memo_cache_sentinel`)?(t=(0,w.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Noch keine Sicherung vorhanden.`}),e[28]=t):t=e[28],z=t}let _e=h,B;e[38]===o?B=e[39]:(B=()=>o.mutate(`pruefen`),e[38]=o,e[39]=B);let ve=o.isPending||P,ye=P?`Sucht …`:`Nach Neuem suchen`,V;e[40]!==ye||e[41]!==B||e[42]!==ve?(V=(0,w.jsx)(g,{variant:`outline`,onClick:B,disabled:ve,children:ye}),e[40]=ye,e[41]=B,e[42]=ve,e[43]=V):V=e[43];let be=g,H;e[44]===Symbol.for(`react.memo_cache_sentinel`)?(H=()=>y({art:`alle`}),e[44]=H):H=e[44];let xe=F||!O.some(Boolean),U;e[45]!==be||e[46]!==H||e[47]!==xe?(U=(0,w.jsx)(be,{onClick:H,disabled:xe,children:`Alles jetzt aktualisieren`}),e[45]=be,e[46]=H,e[47]=xe,e[48]=U):U=e[48];let W;e[49]!==V||e[50]!==U?(W=(0,w.jsxs)(`div`,{className:`flex flex-wrap gap-2.5`,children:[V,U]}),e[49]=V,e[50]=U,e[51]=W):W=e[51];let Se;e[52]===Symbol.for(`react.memo_cache_sentinel`)?(Se=(0,w.jsx)(`p`,{className:`-mt-2 text-[15px] text-text-2`,children:`Jeden Sonntag um 04:30 automatisch. Vorher wird gesichert, danach geprüft.`}),e[52]=Se):Se=e[52];let G;e[53]===M?G=e[54]:(G=M&&(0,w.jsx)(`p`,{role:`status`,className:`text-[15px] text-cyan`,children:`Gerade läuft ein Update. Weitere lassen sich erst starten, wenn es fertig ist.`}),e[53]=M,e[54]=G);let Ce;e[55]===Symbol.for(`react.memo_cache_sentinel`)?(Ce=(0,w.jsx)(ae,{}),e[55]=Ce):Ce=e[55];let K;e[56]!==_e||e[57]!==W||e[58]!==G||e[59]!==L?(K=(0,w.jsxs)(_e,{titel:`Updates`,rechts:W,children:[Se,G,L,Ce]}),e[56]=_e,e[57]=W,e[58]=G,e[59]=L,e[60]=K):K=e[60];let q;e[61]===R?q=e[62]:(q=(0,w.jsx)(h,{titel:`Verlauf`,children:R}),e[61]=R,e[62]=q);let J;e[63]===l?J=e[64]:(J=()=>l.mutate(void 0),e[63]=l,e[64]=J);let we=l.isPending?`Sichert …`:`Jetzt sichern`,Y;e[65]!==l.isPending||e[66]!==J||e[67]!==we?(Y=(0,w.jsx)(g,{variant:`outline`,size:`sm`,onClick:J,disabled:l.isPending,children:we}),e[65]=l.isPending,e[66]=J,e[67]=we,e[68]=Y):Y=e[68];let Te;e[69]===Symbol.for(`react.memo_cache_sentinel`)?(Te=(0,w.jsx)(`p`,{className:`-mt-2 text-[15px] text-text-2`,children:`Täglich um 03:35, mit Kopie auf einem zweiten Gerät.`}),e[69]=Te):Te=e[69];let X;e[70]!==i.data||e[71]!==I?(X=i.data&&!I&&(0,w.jsx)(`p`,{className:`text-sm text-text-3`,children:`Zurückspielen geht nur auf der Box selbst.`}),e[70]=i.data,e[71]=I,e[72]=X):X=e[72];let Z;e[73]!==z||e[74]!==Y||e[75]!==X?(Z=(0,w.jsxs)(h,{titel:`Sicherungen`,rechts:Y,children:[Te,X,z]}),e[73]=z,e[74]=Y,e[75]=X,e[76]=Z):Z=e[76];let Q;e[77]!==q||e[78]!==Z?(Q=(0,w.jsxs)(`div`,{className:`grid gap-5 md:gap-6 lg:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]`,children:[q,Z]}),e[77]=q,e[78]=Z,e[79]=Q):Q=e[79];let Ee=v?.art===`alle`,De;e[80]===Symbol.for(`react.memo_cache_sentinel`)?(De=()=>y(null),e[80]=De):De=e[80];let Oe;e[81]!==v?.art||e[82]!==o?(Oe=()=>{v?.art===`alle`&&o.mutate(`alle`),y(null)},e[81]=v?.art,e[82]=o,e[83]=Oe):Oe=e[83];let ke;e[84]!==F||e[85]!==Ee||e[86]!==Oe?(ke=(0,w.jsx)(_,{offen:Ee,titel:`Alles jetzt aktualisieren?`,text:`Motor, llama-swap, Hermes und Betriebssystem werden nacheinander aktualisiert, soweit es Neues gibt, und danach geprüft. Motor und llama-swap holen bei Problemen ihren alten Stand von selbst zurück, vor Hermes wird gesichert. Lucy ist dabei ein paar Minuten nicht erreichbar.`,knopf:`Aktualisieren`,gesperrt:F,onNein:De,onJa:Oe}),e[84]=F,e[85]=Ee,e[86]=Oe,e[87]=ke):ke=e[87];let Ae=v?.art===`einzeln`,je=v?.art===`einzeln`?`${v.name} aktualisieren?`:``,Me=v?.art===`einzeln`?E[v.id]??``:``,Ne;e[88]===Symbol.for(`react.memo_cache_sentinel`)?(Ne=()=>y(null),e[88]=Ne):Ne=e[88];let Pe;e[89]!==v||e[90]!==o?(Pe=()=>{v?.art===`einzeln`&&o.mutate(v.id),y(null)},e[89]=v,e[90]=o,e[91]=Pe):Pe=e[91];let Fe;e[92]!==F||e[93]!==Ae||e[94]!==je||e[95]!==Me||e[96]!==Pe?(Fe=(0,w.jsx)(_,{offen:Ae,titel:je,text:Me,knopf:`Aktualisieren`,gesperrt:F,onNein:Ne,onJa:Pe}),e[92]=F,e[93]=Ae,e[94]=je,e[95]=Me,e[96]=Pe,e[97]=Fe):Fe=e[97];let Ie=v?.art===`freigeben`,Le=v?.art===`freigeben`?`${v.name} freigeben?`:``,Re=v?.art===`freigeben`?`Festgehalten wurde ${v.name}, weil: ${v.grund}. Nach dem Freigeben versucht der nächste Sonntags-Lauf das Update wieder.`:``,ze;e[98]===Symbol.for(`react.memo_cache_sentinel`)?(ze=()=>y(null),e[98]=ze):ze=e[98];let Be;e[99]!==v||e[100]!==c?(Be=()=>{v?.art===`freigeben`&&c.mutate(v.id),y(null)},e[99]=v,e[100]=c,e[101]=Be):Be=e[101];let Ve;e[102]!==c.isPending||e[103]!==Ie||e[104]!==Le||e[105]!==Re||e[106]!==Be?(Ve=(0,w.jsx)(_,{offen:Ie,titel:Le,text:Re,knopf:`Freigeben`,gesperrt:c.isPending,onNein:ze,onJa:Be}),e[102]=c.isPending,e[103]=Ie,e[104]=Le,e[105]=Re,e[106]=Be,e[107]=Ve):Ve=e[107];let He=v?.art===`zurueck`,Ue=`Die Einstellungen von Hermes und llama-swap werden auf den Stand von ${v?.art===`zurueck`?v.zeit:``} gesetzt. Vorher wird der jetzige Stand gesichert. Die Dienste starten danach neu.`,We;e[108]===Symbol.for(`react.memo_cache_sentinel`)?(We=()=>y(null),e[108]=We):We=e[108];let Ge;e[109]!==v||e[110]!==p?(Ge=()=>{v?.art===`zurueck`&&p.mutate(v.datei),y(null)},e[109]=v,e[110]=p,e[111]=Ge):Ge=e[111];let $;e[112]!==He||e[113]!==Ue||e[114]!==Ge||e[115]!==p.isPending?($=(0,w.jsx)(_,{offen:He,gefahr:!0,titel:`Sicherung zurückspielen?`,text:Ue,knopf:`Zurückspielen`,gesperrt:p.isPending,onNein:We,onJa:Ge}),e[112]=He,e[113]=Ue,e[114]=Ge,e[115]=p.isPending,e[116]=$):$=e[116];let Ke;return e[117]!==K||e[118]!==Q||e[119]!==ke||e[120]!==Fe||e[121]!==Ve||e[122]!==$?(Ke=(0,w.jsxs)(w.Fragment,{children:[K,Q,ke,Fe,Ve,$]}),e[117]=K,e[118]=Q,e[119]=ke,e[120]=Fe,e[121]=Ve,e[122]=$,e[123]=Ke):Ke=e[123],Ke}function de(e){return(0,w.jsx)(P,{lauf:e},e.start)}function fe(e){return y(e)&&l(e)}function pe(e){return e.group===`maintenance`&&l(e)}function me(e){return e.id}function he(e){return[e.baustein,e]}function ge(e){return[e.id,e.neu]}export{I as UpdatesSeite}; \ No newline at end of file diff --git a/frontend/dist/assets/button-BheHBEml.js b/frontend/dist/assets/button-BheHBEml.js new file mode 100644 index 0000000..4367290 --- /dev/null +++ b/frontend/dist/assets/button-BheHBEml.js @@ -0,0 +1 @@ +var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,n)=>{let r={};for(var i in e)t(r,i,{get:e[i],enumerable:!0});return n||t(r,Symbol.toStringTag,{value:`Module`}),r},c=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},l=(n,r,o)=>(o=n==null?{}:e(i(n)),c(r||!n||!n.__esModule||!a.call(n,`default`)?t(o,`default`,{value:n,enumerable:!0}):o,n)),u=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.for(`react.view_transition`),m=Symbol.iterator;function h(e){return typeof e!=`object`||!e?null:(e=m&&e[m]||e[`@@iterator`],typeof e==`function`?e:null)}var g={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},_=Object.assign,v={};function y(e,t,n){this.props=e,this.context=t,this.refs=v,this.updater=n||g}y.prototype.isReactComponent={},y.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},y.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function b(){}b.prototype=y.prototype;function x(e,t,n){this.props=e,this.context=t,this.refs=v,this.updater=n||g}var S=x.prototype=new b;S.constructor=x,_(S,y.prototype),S.isPureReactComponent=!0;var C=Array.isArray;function w(){}var T={H:null,A:null,T:null,S:null},E=Object.prototype.hasOwnProperty;function D(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function O(e,t){return D(e.type,t,e.props)}function k(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function A(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var ee=/\/+/g;function j(e,t){return typeof e==`object`&&e&&e.key!=null?A(``+e.key):t.toString(36)}function te(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(w,w):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function M(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,M(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+j(e,0):a,C(o)?(i=``,c!=null&&(i=c.replace(ee,`$&/`)+`/`),M(o,r,i,``,function(e){return e})):o!=null&&(k(o)&&(o=O(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(ee,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(C(e))for(var u=0;u{t.exports=u()})),f=o((e=>{var t=d();function n(e){var t=`https://react.dev/errors/`+e;if(1{function n(){if(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<`u`&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE==`function`)try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=f()})),m=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.fragment`);function r(e,n,r){var i=null;if(r!==void 0&&(i=``+r),n.key!==void 0&&(i=``+n.key),`key`in n)for(var a in r={},n)a!==`key`&&(r[a]=n[a]);else r=n;return n=r.ref,{$$typeof:t,type:e,key:i,ref:n===void 0?null:n,props:r}}e.Fragment=n,e.jsx=r,e.jsxs=r})),h=o(((e,t)=>{t.exports=m()})),g=o((e=>{var t=d().__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;e.c=function(e){return t.H.useMemoCache(e)}})),_=o(((e,t)=>{t.exports=g()})),v=l(d(),1),y=e=>e?.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).toLowerCase();function b(e,t,n=[]){if(t==null)throw Error(`[lucide]: iconNode is required when icon name is used`);return{name:y(e),size:24,node:t,...n.length>0?{aliases:n}:{}}}var x=e=>{let t=``,n=!1;for(let r of e){if(r===`-`||r===`_`||r<=` `){n=t.length>0;continue}t.length===0?t+=r.toLowerCase():t+=n?r.toUpperCase():r,n=!1}return t},S=e=>{let t=x(e);return t.charAt(0).toUpperCase()+t.slice(1)},C=(...e)=>e.filter((e,t,n)=>!!e&&e.trim()!==``&&n.indexOf(e)===t).join(` `).trim(),w={xmlns:`http://www.w3.org/2000/svg`,width:24,height:24,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":2,"stroke-linecap":`round`,"stroke-linejoin":`round`};function T(e){return e!=null}function E(e,t={}){let n=t.attributeNames??{},r=e=>n[e]??e,i=e.size??e.width??w.width,a=e.size??e.height??w.height,o=e.aliases?.filter(e=>typeof e==`string`&&e.trim()!==``).map(e=>`lucide-${e}`)??[],s=[...e.name?[`lucide-${e.name}`]:[],...o],c=t.className?.split(` `).filter(Boolean)??[],l=t.includeDefaultClasses===!1?C(...c):C(`lucide`,...s,...c),u=t.absoluteStrokeWidth?Number(t.strokeWidth??w[`stroke-width`])*Number(e.size??e.width??w.width)/Number(t.size??t.width??w.width):t.strokeWidth??w[`stroke-width`];return[`svg`,{...Object.entries(w).reduce((e,[t,n])=>(e[r(t)]=n,e),{}),...`color`in t&&t.color&&{[r(`stroke`)]:t.color},...`size`in t&&T(t.size)&&{[r(`width`)]:t.size,[r(`height`)]:t.size},...`width`in t&&T(t.width)&&{[r(`width`)]:t.width},...`height`in t&&T(t.height)&&{[r(`height`)]:t.height},[r(`stroke-width`)]:u,...l&&{[r(`class`)]:l},[r(`viewBox`)]:`0 0 ${i} ${a}`,...t.hasA11yProp===!1?{[r(`aria-hidden`)]:`true`}:{},...`attributes`in t&&t.attributes},e.node.map(e=>{let[n,i,a]=e,o=t.nonScalingStroke?{[r(`vector-effect`)]:`non-scaling-stroke`,...i}:i;return a?[n,o,a]:[n,o]})]}function D(e,t={}){return E(e,{...t,attributeNames:{...t.attributeNames,class:`className`,"stroke-width":`strokeWidth`,"stroke-linecap":`strokeLinecap`,"stroke-linejoin":`strokeLinejoin`,"vector-effect":`vectorEffect`}})}var O=e=>{for(let t in e)if(t.startsWith(`aria-`)||t===`role`||t===`title`)return!0;return!1},k=(0,v.createContext)({}),A=()=>(0,v.useContext)(k),ee=(0,v.forwardRef)(({color:e,size:t,width:n,height:r,strokeWidth:i,absoluteStrokeWidth:a,nonScalingStroke:o,className:s=``,children:c,iconNode:l=[],icon:u={node:l,aliases:[],size:24},...d},f)=>{let{size:p=24,strokeWidth:m=2,absoluteStrokeWidth:h=!1,nonScalingStroke:g=!1,color:_=`currentColor`,className:y=``}=A()??{},b=!!c||O(d),[x,S,w=[]]=D(u,{color:e??_,width:n??t??p,height:r??t??p,strokeWidth:i??m,absoluteStrokeWidth:a??h,nonScalingStroke:o??g,className:C(y,s),hasA11yProp:b,attributes:d});return(0,v.createElement)(x,{ref:f,...S},[...w.map(([e,t])=>(0,v.createElement)(e,t)),...Array.isArray(c)?c:[c]])});function j(e,t=[],n=[]){let r=typeof e==`string`?b(e,t,n):e,i=(0,v.forwardRef)(({className:e,...t},n)=>(0,v.createElement)(ee,{ref:n,icon:r,className:e,...t}));return r.name&&(i.displayName=S(r.name)),i}var te={name:`x`,size:24,node:[[`path`,{d:`M18 6 6 18`,key:`1bl5f8`}],[`path`,{d:`m6 6 12 12`,key:`d8bk6v`}]]};te.node;var M=j(te),N=_(),P=48,F=(e,t=0)=>{let n=new Int32Array(e.length);for(let r=0;r{let t=new Int32Array(e.length+1);for(let n=0;n{let t=new Int32Array(e.length),n=0;for(let r=0;r>>1^-(i&1),t[r]=n}return t},ne=384,re=[],R=I(F(`E0500002005282000000002000150000020021820000011200000003022202000300004200120000200420001200021200301200010400162000010000220021010:2192001200220012000220012000200200200400010200040000000000400200108200110100000022010313000162002000020020012020080213000228200000000082000000000120002000120020020040101020300130001001010`)),ie=I(F(`:11111111211111119311546544411119731869:671397415686432441111111111161114151214313433415:78311132233313187211117221449443411141111151152226611131111112212518142224214215421421542142424242516171151615616347111111111197911327451111111111111111111113134714133513411111311111111111111111111112444411111342312715245411117:3`)),z=`@containerabcdefghinlmoprstunderlineviawzccentlignnimatespectuto-colsrowsaglorightnessckdrop-sisbcontrastfiltergrayscalehue-rotateinvertopacityslurrightnessaturateepia-coniclinearpositionradialsizeockurrderttom-belrstxyespacing-xyaretoursorlnt-umnsendspantartainentrasteividerop-shadowurationcorationlay-xyasendillexontromlter-featuresstretchapr-xyayscaleidow-colsrowsue-rotatedentlinesetvert-beringsxyeshadoweiadingftnest-clamp-imageabein-lrstxyskx--b-coniclpositionrsizet-x-y-fromto-fromto-inearfromto-fromto-adialfromto-fromtofromtofromtofromtoblockhinlinew-screenesblockhinlinewbjectpacityrutlinederigin-offsetbelrstxyesrspective-originaceholderioghtng-offsettateundedw-xyz-belrstlreseslr-endspantartaturatecepiahizekewpace-taleroll-xyz-barmpbelrstxyesbelrstxyes-thumbrackadowrink-xyxyartrokeabextora-shadowpckingnsformitionlate-xyz-offsetill-changeoom`,ae=(()=>{let e=R.length-1,t=new Int32Array(e);for(let n=e-1;n>=0;n--){let e=1,r=n+1;for(let i=R[n];iE?F@03263243255B:032523853:0325B:8GA032542HE?3257D>032585:0325B:;0328B:032`),ce=F(`012113445661666666789111:5;;;;;;;;444;;;:62999<1161=62>>?61:21@ABCD4446996:64E:::;:?::64:F114GHHIHHHHIHH1HH1HH1HHHHHH::EEJ4444::EE4444441691;644444114244444:;K6666555555555555555999666664444444444444444444444226?6:66644L9M?D:111::::6DE199`),le=L(`020200202020020020020020020020200200200200200200200002020202001003040106000200200200200200200200200200200200200200200200200200200200200200200200200200200200020020020020020020002020200202002002002002002002002002002020002002020020200220200200200200200200200200200200020020020000200020002000200200200020020020002000200200200020020202002020202000200200020022000200200020020002002000200220002002000200W0Z00020020002002020002002000200g0j0002002000200200020020002002000200200020020002000200002000002002002002002000200020000200002002002002002002002020020020200200200200200200200200202020020020020020020020020002002002020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020020002002002002002002000200200200200200200200200200020202020002000200020002002002002000020200200`),ue=(()=>{let e=new Int32Array(319).fill(-1),t=L(`02422242:222222242224222242442222222422222244442242226224222426222422442462222422622222222626222462242622422622422424242422222222422222222242422222222222222222622442224222222222222224424442262222222222222222222226224222424242422224422422422222`),n=L(`02222222222222222222202222222222222222222222221422222222222222222222222222222222222Y\\222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222221422222222222222222222222222222222222222Ŀł222222222222222222`);for(let r=0;r