modelle: Aufraeumen auf Knopfdruck, geteilte Gewichte bleiben beim Loeschen liegen
Ampel / ampel (push) Failing after 21s
Ampel / ampel (push) Failing after 21s
Neuer Bereich "Aufraeumen" auf der Modelle-Seite: Eintraege ohne heutige Rolle und Ordner, auf die kein Eintrag zeigt, mit Groesse und Erklaerung (Rueckweg, Reserve, alte Drafts). Geloescht wird nur auf Knopfdruck mit Rueckfrage. delete_model loescht keine Dateien mehr, die ein anderer Eintrag nennt - seit den Bild-Zwillingen haette sonst das Loeschen eines Zwillings den Coder mitgerissen. Der Projektor des Hirn-Zwillings liegt jetzt neben den Hirn-Gewichten, damit der Original-Ordner loeschbar bleibt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
133a491aca
commit
42363229d4
@@ -8,6 +8,8 @@ Box-Wart-Schnittstellen (Umbau 09/2026): alles, was der Cockpit-Startbildschirm
|
||||
GET /api/zeitplan Heute gelaufen / demnächst geplant
|
||||
GET /api/updates/verlauf Was die letzten Update-Läufe wirklich gebracht haben
|
||||
POST /api/updates/festgehalten/{b}/freigeben Festgehaltenen Baustein wieder freigeben
|
||||
GET /api/modelle/aufraeumen Was auf der Modell-Platte ungenutzt Platz belegt
|
||||
POST /api/modelle/aufraeumen/loeschen Einen Kandidaten löschen (nur auf Knopfdruck)
|
||||
|
||||
Dünn: die Logik liegt in services/waechter.py, modell_nutzung.py, zeitplan.py, update_verlauf.py.
|
||||
"""
|
||||
@@ -21,7 +23,9 @@ from zoneinfo import ZoneInfo
|
||||
import psutil
|
||||
from config import MODELS_DIR
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from services import (
|
||||
aufraeumen,
|
||||
backup,
|
||||
llamaswap,
|
||||
maintenance,
|
||||
@@ -220,3 +224,21 @@ def festgehalten_freigeben(baustein: str) -> dict:
|
||||
if not update_verlauf.freigeben(baustein):
|
||||
raise HTTPException(status_code=404, detail="Dieser Baustein ist nicht festgehalten.")
|
||||
return {"ok": True, "text": update_verlauf.FREIGEGEBEN_TEXT}
|
||||
|
||||
|
||||
class LoeschAuftrag(BaseModel):
|
||||
art: str # "eintrag" | "ordner"
|
||||
name: str
|
||||
|
||||
|
||||
@router.get("/modelle/aufraeumen")
|
||||
def aufraeumen_liste() -> dict:
|
||||
return {"kandidaten": aufraeumen.kandidaten()}
|
||||
|
||||
|
||||
@router.post("/modelle/aufraeumen/loeschen")
|
||||
def aufraeumen_loeschen(auftrag: LoeschAuftrag) -> dict:
|
||||
ergebnis = aufraeumen.loeschen(auftrag.art, auftrag.name)
|
||||
if not ergebnis.get("ok"):
|
||||
raise HTTPException(status_code=409, detail=ergebnis.get("detail") or "Löschen ging nicht.")
|
||||
return ergebnis
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
Aufräumen der Modell-Platte (Box-Wart, Umbau 09/2026): was belegt Platz, ohne gebraucht zu werden?
|
||||
|
||||
Zwei Arten von Kandidaten:
|
||||
• Einträge ohne Rolle llama-swap-Einträge, die keine der heutigen Rollen tragen (Hirn, Coder, ihre
|
||||
Bild-Zwillinge, Einbettung, Reranker) und in keiner immer-warmen Gruppe stehen.
|
||||
• Ordner ohne Eintrag Unterordner von MODELS_DIR, auf die kein Eintrag mehr zeigt (alte Originale,
|
||||
Reserve-Modelle, Drafts aus früheren Versuchen).
|
||||
|
||||
Gelöscht wird nur auf Knopfdruck des Nutzers (mit Rückfrage in der Oberfläche), nie automatisch
|
||||
(User-Entscheid 23.09.: „nach der Umstellung löschen“ — die Entscheidung je Modell bleibt beim Nutzer).
|
||||
Ein Eintrag wird aus der Config genommen, danach verschwinden nur Ordner, auf die NICHTS mehr zeigt:
|
||||
Die Bild-Zwillinge teilen sich ihre Gewichte mit Hirn und Coder, geteilte Dateien bleiben immer liegen.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from config import MODELS_DIR
|
||||
|
||||
from services import llamaswap
|
||||
|
||||
AKTIVE_ROLLEN = {"hermes", "fast", "coder", "heavy", "vision", "coder-bild", "embed", "reranker"}
|
||||
SYSTEM_ORDNER = {"mc2-backups", "radar", "pruefstand-cache", "lost+found"}
|
||||
AUF_DER_BOX = os.name == "posix" # auf dem Windows-PC (Entwicklung) wird nie gelöscht
|
||||
|
||||
# Was der Nutzer über bekannte Ordner wissen sollte, bevor er löscht (Stand 24.09.2026).
|
||||
HINWEISE = {
|
||||
"Qwen3.6-35B-A3B-MTP-GGUF": "Original des Hirns vor der Uncensored-Variante (17.09.) — der Rückweg, falls das Hirn "
|
||||
"einmal Probleme macht.",
|
||||
"Qwen3.8-27B-GGUF": "Original des Coders vor der Uncensored-Variante (17.09.) — der Rückweg für den Coder.",
|
||||
"Nemotron-3.5-Lightning-30B-A3B-GGUF": "Reserve-Hirn aus dem Prüfstand vom 17.09. (gleich gut bewertet, "
|
||||
"nie in Betrieb).",
|
||||
"DFlash-drafts": "Draft-Modelle aus früheren Versuchen, heute ungenutzt.",
|
||||
"drafts": "Draft-Modelle aus früheren Versuchen, heute ungenutzt.",
|
||||
"gpt-oss-120b": "Früheres heavy-Modell, hat seit dem 04.09. keine Rolle mehr.",
|
||||
"Qwen3-VL-30B-A3B-Instruct": "Früheres Bild-Modell, seit dem 24.09. durch die Bild-Zwillinge abgelöst.",
|
||||
"Muse-Glimmer-30B": "Debugger — die Rolle entfällt (Entscheid 23.09.: höchstens Hirn und Coder).",
|
||||
}
|
||||
|
||||
|
||||
def _ordner_von(pfad: str) -> str | None:
|
||||
"""Oberster Ordner unter MODELS_DIR, in dem eine Datei liegt (oder None, wenn sie woanders liegt)."""
|
||||
if not os.path.isabs(pfad):
|
||||
return None
|
||||
try:
|
||||
rel = Path(pfad).resolve().relative_to(Path(MODELS_DIR).resolve())
|
||||
except (ValueError, OSError):
|
||||
return None
|
||||
return rel.parts[0] if len(rel.parts) > 1 else None
|
||||
|
||||
|
||||
def _genutzte_ordner(models: dict) -> dict[str, set[str]]:
|
||||
"""Ordner → Einträge, deren Befehl eine Datei darin nennt (Gewichte, Projektor, Draft)."""
|
||||
genutzt: dict[str, set[str]] = {}
|
||||
for name, spec in models.items():
|
||||
if not isinstance(spec, dict):
|
||||
continue
|
||||
for wort in str(spec.get("cmd") or "").split(): # jedes Wort, das unter MODELS_DIR liegt
|
||||
if (ordner := _ordner_von(wort.strip("'\""))):
|
||||
genutzt.setdefault(ordner, set()).add(name)
|
||||
return genutzt
|
||||
|
||||
|
||||
def _groesse(pfad: Path) -> int:
|
||||
summe = 0
|
||||
for wurzel, _, dateien in os.walk(pfad):
|
||||
for datei in dateien:
|
||||
try:
|
||||
summe += os.path.getsize(os.path.join(wurzel, datei))
|
||||
except OSError:
|
||||
pass
|
||||
return summe
|
||||
|
||||
|
||||
def _immer_warm(cfg: dict) -> set[str]:
|
||||
return {m for g in (cfg.get("groups") or {}).values()
|
||||
if isinstance(g, dict) and (g.get("persistent") or g.get("persist")) for m in g.get("members") or []}
|
||||
|
||||
|
||||
def kandidaten() -> list[dict]:
|
||||
"""Alles, was sich löschen ließe — größte zuerst. Löscht nichts."""
|
||||
cfg = llamaswap.read_config()
|
||||
models = cfg.get("models") or {}
|
||||
warm = _immer_warm(cfg)
|
||||
genutzt = _genutzte_ordner(models)
|
||||
liste: list[dict] = []
|
||||
for name, spec in models.items():
|
||||
aliase = {str(a).lower() for a in (spec or {}).get("aliases") or []}
|
||||
if aliase & AKTIVE_ROLLEN or name in warm:
|
||||
continue
|
||||
eigene = {o for o, wer in genutzt.items() if wer == {name}} # nur von diesem Eintrag genutzt
|
||||
groesse = sum(_groesse(Path(MODELS_DIR) / o) for o in eigene)
|
||||
rolle = f"Rolle „{', '.join(sorted(aliase))}“ gibt es nicht mehr." if aliase else "Hat keine Rolle."
|
||||
liste.append({"art": "eintrag", "name": name, "groesse_gb": round(groesse / 1e9, 1),
|
||||
"hinweis": HINWEISE.get(name, rolle)})
|
||||
try:
|
||||
ordner = sorted(p for p in Path(MODELS_DIR).iterdir() if p.is_dir())
|
||||
except OSError:
|
||||
ordner = []
|
||||
for pfad in ordner:
|
||||
if pfad.name in genutzt or pfad.name in SYSTEM_ORDNER or pfad.name.startswith("."):
|
||||
continue
|
||||
liste.append({"art": "ordner", "name": pfad.name, "groesse_gb": round(_groesse(pfad) / 1e9, 1),
|
||||
"hinweis": HINWEISE.get(pfad.name, "Kein Eintrag nutzt diesen Ordner.")})
|
||||
return sorted(liste, key=lambda k: -k["groesse_gb"])
|
||||
|
||||
|
||||
def _ordner_loeschen(name: str) -> int:
|
||||
"""Einen Ordner direkt unter MODELS_DIR löschen; gibt die freigewordenen Bytes zurück."""
|
||||
wurzel = Path(MODELS_DIR).resolve()
|
||||
pfad = (wurzel / name).resolve()
|
||||
if pfad.parent != wurzel or not pfad.is_dir() or name in SYSTEM_ORDNER or name.startswith("."):
|
||||
raise ValueError(f"{name} ist kein löschbarer Modell-Ordner.")
|
||||
groesse = _groesse(pfad)
|
||||
shutil.rmtree(pfad)
|
||||
return groesse
|
||||
|
||||
|
||||
def loeschen(art: str, name: str) -> dict:
|
||||
"""Knopf „Löschen“: nur, was gerade als Kandidat gilt. Gibt {ok, text} oder {ok: False, detail} zurück."""
|
||||
if not any(k["art"] == art and k["name"] == name for k in kandidaten()):
|
||||
return {"ok": False, "detail": f"{name} ist nicht (mehr) zum Löschen frei."}
|
||||
if not AUF_DER_BOX:
|
||||
return {"ok": False, "detail": "Löschen geht nur auf der Box."}
|
||||
frei = 0
|
||||
try:
|
||||
if art == "ordner":
|
||||
frei = _ordner_loeschen(name)
|
||||
else:
|
||||
cfg = llamaswap.read_config()
|
||||
models = cfg.get("models") or {}
|
||||
vorher = _genutzte_ordner(models)
|
||||
del models[name]
|
||||
for gruppe in (cfg.get("groups") or {}).values():
|
||||
if isinstance(gruppe, dict) and name in (gruppe.get("members") or []):
|
||||
gruppe["members"] = [m for m in gruppe["members"] if m != name]
|
||||
llamaswap.write_config(cfg)
|
||||
nachher = _genutzte_ordner(models)
|
||||
for ordner in sorted(set(vorher) - set(nachher)): # nur Ordner, auf die jetzt nichts mehr zeigt
|
||||
frei += _ordner_loeschen(ordner)
|
||||
except (OSError, ValueError) as e:
|
||||
return {"ok": False, "detail": f"Löschen ging nicht: {e}"}
|
||||
return {"ok": True, "text": f"{name} gelöscht, {frei / 1e9:.1f} GB frei."}
|
||||
@@ -532,7 +532,13 @@ def delete_model(model_id: str) -> bool:
|
||||
|
||||
model_spec = models[model_id] or {}
|
||||
cmd = str(model_spec.get("cmd", "")).strip()
|
||||
if (m := _PATH_RE.search(cmd)):
|
||||
# Seit 24.09.2026 teilen sich die Bild-Zwillinge ihre Gewichte (und ggf. Projektoren) mit Hirn und Coder.
|
||||
# Dateien, die ein anderer Eintrag noch nennt, bleiben liegen — sonst risse das Löschen eines Zwillings
|
||||
# den Coder mit. Dann wird nur der Eintrag entfernt.
|
||||
andere = " ".join(str((s or {}).get("cmd", "")) for mid, s in models.items() if mid != model_id)
|
||||
if (m := _PATH_RE.search(cmd)) and m.group(1).replace("'", "").replace('"', "") in andere:
|
||||
m = None
|
||||
if m:
|
||||
path = m.group(1).replace("'", "").replace('"', "")
|
||||
if path:
|
||||
# 1. Haupt-GGUF-Datei löschen
|
||||
@@ -561,7 +567,7 @@ def delete_model(model_id: str) -> bool:
|
||||
mmproj_match = re.search(r'--mmproj\s+[\'"]?([^\s\'"]+)[\'"]?', cmd)
|
||||
if mmproj_match:
|
||||
m_path = mmproj_match.group(1)
|
||||
if os.path.exists(m_path):
|
||||
if os.path.exists(m_path) and m_path not in andere:
|
||||
try:
|
||||
os.remove(m_path)
|
||||
except Exception:
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Aufräumen der Modell-Platte: nur Ungenutztes, nur auf Knopfdruck, geteilte Dateien bleiben."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from services import aufraeumen, llamaswap
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platte(tmp_path, monkeypatch):
|
||||
"""Nachbau der Box: Hirn + Bild-Zwilling teilen Gewichte, der Projektor liegt im Original-Ordner."""
|
||||
for ordner, datei, groesse in (("Hirn-GGUF", "hirn.gguf", 3000), ("Original-GGUF", "mmproj.gguf", 500),
|
||||
("Coder-GGUF", "coder.gguf", 2000), ("Alt-GGUF", "alt.gguf", 4000),
|
||||
("Muse-GGUF", "muse.gguf", 1500), ("Reserve-GGUF", "reserve.gguf", 2500),
|
||||
("radar", "log.txt", 10), (".cache", "x", 10)):
|
||||
(tmp_path / ordner).mkdir()
|
||||
(tmp_path / ordner / datei).write_bytes(b"x" * groesse)
|
||||
m = str(tmp_path)
|
||||
cfg = {"models": {
|
||||
"hirn": {"cmd": f"llama-server -m {m}/Hirn-GGUF/hirn.gguf", "aliases": ["hermes", "fast"]},
|
||||
"hirn-bild": {"cmd": f"llama-server -m {m}/Hirn-GGUF/hirn.gguf --mmproj {m}/Original-GGUF/mmproj.gguf",
|
||||
"aliases": ["vision"]},
|
||||
"coder": {"cmd": f"llama-server -m {m}/Coder-GGUF/coder.gguf", "aliases": ["coder"]},
|
||||
"alt": {"cmd": f"llama-server -m {m}/Alt-GGUF/alt.gguf"},
|
||||
"muse": {"cmd": f"llama-server -m {m}/Muse-GGUF/muse.gguf", "aliases": ["debugger"]},
|
||||
}, "groups": {"brains": {"persistent": True, "members": ["hirn"]}}}
|
||||
monkeypatch.setattr(aufraeumen, "MODELS_DIR", tmp_path)
|
||||
monkeypatch.setattr(aufraeumen, "AUF_DER_BOX", True)
|
||||
monkeypatch.setattr(llamaswap, "read_config", lambda: cfg)
|
||||
monkeypatch.setattr(llamaswap, "write_config", lambda c: None)
|
||||
return tmp_path, cfg
|
||||
|
||||
|
||||
def test_kandidaten_sind_rollenlose_eintraege_und_verwaiste_ordner(platte):
|
||||
namen = {(k["art"], k["name"]) for k in aufraeumen.kandidaten()}
|
||||
assert namen == {("eintrag", "alt"), ("eintrag", "muse"), ("ordner", "Reserve-GGUF")}
|
||||
# Der Original-Ordner ist KEIN Kandidat: der Bild-Zwilling nutzt seinen Projektor.
|
||||
muse = next(k for k in aufraeumen.kandidaten() if k["name"] == "muse")
|
||||
assert "debugger" in muse["hinweis"]
|
||||
|
||||
|
||||
def test_eintrag_loeschen_nimmt_ihn_aus_der_config_und_seinen_ordner_mit(platte):
|
||||
wurzel, cfg = platte
|
||||
ergebnis = aufraeumen.loeschen("eintrag", "alt")
|
||||
assert ergebnis["ok"] and "alt gelöscht" in ergebnis["text"]
|
||||
assert "alt" not in cfg["models"] and not (wurzel / "Alt-GGUF").exists()
|
||||
assert (wurzel / "Hirn-GGUF" / "hirn.gguf").exists() and (wurzel / "Coder-GGUF").exists()
|
||||
|
||||
|
||||
def test_nur_aktuelle_kandidaten_und_nie_systemordner(platte):
|
||||
wurzel, _ = platte
|
||||
assert not aufraeumen.loeschen("ordner", "Hirn-GGUF")["ok"] # wird genutzt
|
||||
assert not aufraeumen.loeschen("ordner", "radar")["ok"] # Systemordner
|
||||
assert not aufraeumen.loeschen("ordner", "../etwas")["ok"]
|
||||
assert (wurzel / "Hirn-GGUF").exists() and (wurzel / "radar").exists()
|
||||
assert aufraeumen.loeschen("ordner", "Reserve-GGUF")["ok"] and not (wurzel / "Reserve-GGUF").exists()
|
||||
|
||||
|
||||
def test_delete_model_laesst_geteilte_gewichte_liegen(platte):
|
||||
"""Das Löschen des Bild-Zwillings darf die Gewichte des Hirns nicht mitnehmen (24.09.)."""
|
||||
wurzel, cfg = platte
|
||||
assert llamaswap.delete_model("hirn-bild") is True
|
||||
assert "hirn-bild" not in cfg["models"]
|
||||
assert (wurzel / "Hirn-GGUF" / "hirn.gguf").exists()
|
||||
# Im Zweifel liegen lassen: der Ordner taucht danach als verwaist im Aufräumen auf.
|
||||
assert Path(wurzel / "Original-GGUF" / "mmproj.gguf").exists()
|
||||
assert ("ordner", "Original-GGUF") in {(k["art"], k["name"]) for k in aufraeumen.kandidaten()}
|
||||
@@ -33,10 +33,11 @@ models:
|
||||
# auch speculative.n_max=0 je Anfrage hilft nicht). Das MC2-Gateway schickt nur Anfragen mit einem Bild im
|
||||
# aktuellen Schritt hierher; ältere Bilder ersetzt es durch eine Beschreibung von hier, damit der Rest der
|
||||
# Aufgabe beim schnellen Hirn mit Draft bleibt. Probe 24.09.: 8/8 Bildmerkmale, Bildschirm-Aufgabe im
|
||||
# Agenten-Ablauf richtig, 67,9 t/s @13k (Hirn mit Draft 105,8). Projektor aus dem Original-Repo
|
||||
# (Qwen3.6-35B-A3B-MTP-GGUF) — er passt zur abliterierten Variante. Löst Qwen3-VL als `vision` ab.
|
||||
# Agenten-Ablauf richtig, 67,9 t/s @13k (Hirn mit Draft 105,8). Projektor stammt aus dem Original-Repo
|
||||
# (Qwen3.6-35B-A3B-MTP-GGUF), passt zur abliterierten Variante und liegt als Kopie neben deren Gewichten —
|
||||
# so bleibt der Original-Ordner im Aufräumen löschbar. Löst Qwen3-VL als `vision` ab.
|
||||
cmd: |
|
||||
llama-server -m /srv/models/Qwen3.6-35B-A3B-Uncensored-GGUF/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 65536 -ngl 999 -fa on --load-mode none --mmproj /srv/models/Qwen3.6-35B-A3B-MTP-GGUF/mmproj-BF16.gguf --jinja --parallel 1 -cram 8192 -ctk q8_0 -ctv q8_0 --reasoning-budget 2048
|
||||
llama-server -m /srv/models/Qwen3.6-35B-A3B-Uncensored-GGUF/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 65536 -ngl 999 -fa on --load-mode none --mmproj /srv/models/Qwen3.6-35B-A3B-Uncensored-GGUF/mmproj-BF16.gguf --jinja --parallel 1 -cram 8192 -ctk q8_0 -ctv q8_0 --reasoning-budget 2048
|
||||
ttl: 900
|
||||
aliases:
|
||||
- vision
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{c as e,f as t,i as n,t as r,u as i}from"./button-Dd8hfusv.js";import{A as a,L as o,f as s,j as c,y as l}from"./index-BRMEvo2A.js";import{a as u,i as d,n as f,r as p,t as m}from"./sheet-DWG2Q04V.js";var h=t(i(),1),g=e();function _({offen:e,onSchliessen:t}){let i=o(),_=s(),[v,y]=(0,h.useState)(null),[b,x]=(0,h.useState)(null),S=l(v);async function C(e){x(null);try{let t=await a(`/api/maintenance/restart`,{service:e});t.ok?c(`erfolg`,`${e} startet neu.`):c(`fehler`,t.err||`${e} ließ sich nicht neu starten.`),i.invalidateQueries({queryKey:[`dienste`]})}catch(e){c(`fehler`,e.message)}}return(0,g.jsx)(m,{open:e,onOpenChange:e=>!e&&t(),children:(0,g.jsxs)(f,{side:`right`,className:`w-full gap-3 overflow-y-auto border-linie bg-panel sm:max-w-2xl`,children:[(0,g.jsxs)(d,{children:[(0,g.jsx)(u,{className:`schild text-lg`,children:`Dienste und Protokolle`}),(0,g.jsx)(p,{className:`text-text-2`,children:`Welcher Dienst läuft, was er zuletzt geschrieben hat.`})]}),(0,g.jsx)(`ul`,{className:`m-0 flex list-none flex-col px-4 pb-2`,children:(_.data?.services??[]).map(e=>(0,g.jsxs)(`li`,{className:`flex flex-wrap items-center justify-between gap-2 border-t border-linie py-2.5`,children:[(0,g.jsxs)(`span`,{className:`flex min-w-0 items-center gap-2.5`,children:[(0,g.jsx)(`span`,{className:n(`size-2.5 shrink-0 rounded-full`,e.ok?`bg-gruen`:`bg-rot`),"aria-hidden":!0}),(0,g.jsxs)(`span`,{className:`min-w-0`,children:[(0,g.jsx)(`span`,{className:`block text-[15px]`,children:e.name}),(0,g.jsxs)(`span`,{className:`ziffern text-xs text-text-3`,children:[e.unit,` · `,e.ok?`läuft`:`antwortet nicht`]})]})]}),(0,g.jsxs)(`span`,{className:`flex gap-2`,children:[(0,g.jsx)(r,{variant:v===e.unit?`info`:`ghost`,size:`sm`,onClick:()=>y(e.unit),children:`Protokoll`}),b===e.unit?(0,g.jsx)(r,{variant:`gefahr`,size:`sm`,onClick:()=>C(e.unit),children:`Wirklich neu starten?`}):(0,g.jsx)(r,{variant:`outline`,size:`sm`,onClick:()=>x(e.unit),children:`Neu starten`})]})]},e.unit))}),v&&(0,g.jsx)(`pre`,{className:`ziffern mx-4 mb-4 max-h-[50vh] overflow-auto rounded-lg border border-linie bg-[#0b0d0f] p-3 text-xs leading-relaxed whitespace-pre-wrap text-text-2`,children:S.isFetching?`Wird gelesen …`:S.data?.text||S.data?.err||`Kein Protokoll.`})]})})}export{_ as Dienste};
|
||||
import{c as e,f as t,i as n,t as r,u as i}from"./button-Dd8hfusv.js";import{M as a,N as o,m as s,x as c,z as l}from"./index-D17zMI0S.js";import{a as u,i as d,n as f,r as p,t as m}from"./sheet-DWG2Q04V.js";var h=t(i(),1),g=e();function _({offen:e,onSchliessen:t}){let i=l(),_=s(),[v,y]=(0,h.useState)(null),[b,x]=(0,h.useState)(null),S=c(v);async function C(e){x(null);try{let t=await a(`/api/maintenance/restart`,{service:e});t.ok?o(`erfolg`,`${e} startet neu.`):o(`fehler`,t.err||`${e} ließ sich nicht neu starten.`),i.invalidateQueries({queryKey:[`dienste`]})}catch(e){o(`fehler`,e.message)}}return(0,g.jsx)(m,{open:e,onOpenChange:e=>!e&&t(),children:(0,g.jsxs)(f,{side:`right`,className:`w-full gap-3 overflow-y-auto border-linie bg-panel sm:max-w-2xl`,children:[(0,g.jsxs)(d,{children:[(0,g.jsx)(u,{className:`schild text-lg`,children:`Dienste und Protokolle`}),(0,g.jsx)(p,{className:`text-text-2`,children:`Welcher Dienst läuft, was er zuletzt geschrieben hat.`})]}),(0,g.jsx)(`ul`,{className:`m-0 flex list-none flex-col px-4 pb-2`,children:(_.data?.services??[]).map(e=>(0,g.jsxs)(`li`,{className:`flex flex-wrap items-center justify-between gap-2 border-t border-linie py-2.5`,children:[(0,g.jsxs)(`span`,{className:`flex min-w-0 items-center gap-2.5`,children:[(0,g.jsx)(`span`,{className:n(`size-2.5 shrink-0 rounded-full`,e.ok?`bg-gruen`:`bg-rot`),"aria-hidden":!0}),(0,g.jsxs)(`span`,{className:`min-w-0`,children:[(0,g.jsx)(`span`,{className:`block text-[15px]`,children:e.name}),(0,g.jsxs)(`span`,{className:`ziffern text-xs text-text-3`,children:[e.unit,` · `,e.ok?`läuft`:`antwortet nicht`]})]})]}),(0,g.jsxs)(`span`,{className:`flex gap-2`,children:[(0,g.jsx)(r,{variant:v===e.unit?`info`:`ghost`,size:`sm`,onClick:()=>y(e.unit),children:`Protokoll`}),b===e.unit?(0,g.jsx)(r,{variant:`gefahr`,size:`sm`,onClick:()=>C(e.unit),children:`Wirklich neu starten?`}):(0,g.jsx)(r,{variant:`outline`,size:`sm`,onClick:()=>x(e.unit),children:`Neu starten`})]})]},e.unit))}),v&&(0,g.jsx)(`pre`,{className:`ziffern mx-4 mb-4 max-h-[50vh] overflow-auto rounded-lg border border-linie bg-[#0b0d0f] p-3 text-xs leading-relaxed whitespace-pre-wrap text-text-2`,children:S.isFetching?`Wird gelesen …`:S.data?.text||S.data?.err||`Kein Protokoll.`})]})})}export{_ as Dienste};
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
import{c as e,f as t,t as n,u as r}from"./button-Dd8hfusv.js";import{A as i,F as a,I as o,L as s,j as c,k as l}from"./index-BRMEvo2A.js";import{a as u,i as d,n as f,r as p,t as m}from"./sheet-DWG2Q04V.js";var h=t(r(),1),g=e();function _({offen:e,onSchliessen:t}){let r=s(),_=o({queryKey:[`geheimnisse`],queryFn:()=>l(`/api/maintenance/geheimnisse`),enabled:e}),[v,y]=(0,h.useState)(``);async function b(e){try{await i(`/api/maintenance/geheimnisse`,{schluessel:`hf_token`,wert:e}),c(`erfolg`,e?`Hugging-Face-Zugang gespeichert.`:`Hugging-Face-Zugang gelöscht.`),y(``),r.invalidateQueries({queryKey:[`geheimnisse`]})}catch(e){c(`fehler`,e.message)}}let x=_.data;return(0,g.jsx)(m,{open:e,onOpenChange:e=>!e&&t(),children:(0,g.jsxs)(f,{side:`right`,className:`w-full gap-3 overflow-y-auto border-linie bg-panel 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`,children:[x?.hf_token_gesetzt?`Ist hinterlegt.`:`Fehlt.`,` Nötig für gesperrte Modelle und schnellere Downloads.`,x?.hf_token_aus_env?` Er kommt aus der Dienst-Einstellung und lässt sich hier nicht löschen.`:``]}),(0,g.jsxs)(`form`,{className:`flex gap-2`,onSubmit:e=>{e.preventDefault(),v.trim()&&b(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()||x?.schreibbar===!1,children:`Speichern`})]}),x?.hf_token_gesetzt&&!x.hf_token_aus_env&&(0,g.jsx)(n,{variant:`ghost`,size:`sm`,className:`self-start`,onClick:()=>b(null),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)(a,{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};
|
||||
import{c as e,f as t,t as n,u as r}from"./button-Dd8hfusv.js";import{L as i,M as a,N as o,R as s,j as c,z as l}from"./index-D17zMI0S.js";import{a as u,i as d,n as f,r as p,t as m}from"./sheet-DWG2Q04V.js";var h=t(r(),1),g=e();function _({offen:e,onSchliessen:t}){let r=l(),_=s({queryKey:[`geheimnisse`],queryFn:()=>c(`/api/maintenance/geheimnisse`),enabled:e}),[v,y]=(0,h.useState)(``);async function b(e){try{await a(`/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)}}let x=_.data;return(0,g.jsx)(m,{open:e,onOpenChange:e=>!e&&t(),children:(0,g.jsxs)(f,{side:`right`,className:`w-full gap-3 overflow-y-auto border-linie bg-panel 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`,children:[x?.hf_token_gesetzt?`Ist hinterlegt.`:`Fehlt.`,` Nötig für gesperrte Modelle und schnellere Downloads.`,x?.hf_token_aus_env?` Er kommt aus der Dienst-Einstellung und lässt sich hier nicht löschen.`:``]}),(0,g.jsxs)(`form`,{className:`flex gap-2`,onSubmit:e=>{e.preventDefault(),v.trim()&&b(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()||x?.schreibbar===!1,children:`Speichern`})]}),x?.hf_token_gesetzt&&!x.hf_token_aus_env&&(0,g.jsx)(n,{variant:`ghost`,size:`sm`,className:`self-start`,onClick:()=>b(null),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)(i,{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};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{c as e,s as t}from"./button-Dd8hfusv.js";import{F as n,N as r,P as i}from"./index-BRMEvo2A.js";import{a,i as o,n as s,r as c,t as l}from"./sheet-DWG2Q04V.js";var u=t(),d=e();function f(e){let t=(0,u.c)(17),{verbindung:f,onDienste:p,onEinstellungen:m,onSchliessen:h}=e,g;t[0]===h?g=t[1]:(g=e=>!e&&h(),t[0]=h,t[1]=g);let _;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(_=(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]=_):_=t[2];let v;t[3]===Symbol.for(`react.memo_cache_sentinel`)?(v=(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)(n,{className:`size-5`,"aria-hidden":!0}),` Hermes-Dashboard öffnen`]}),t[3]=v):v=t[3];let y;t[4]===Symbol.for(`react.memo_cache_sentinel`)?(y=(0,d.jsx)(i,{className:`size-5`,"aria-hidden":!0}),t[4]=y):y=t[4];let b;t[5]===p?b=t[6]:(b=(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:[y,` Dienste und Protokolle`]}),t[5]=p,t[6]=b);let x;t[7]===Symbol.for(`react.memo_cache_sentinel`)?(x=(0,d.jsx)(r,{className:`size-5`,"aria-hidden":!0}),t[7]=x):x=t[7];let S;t[8]===m?S=t[9]:(S=(0,d.jsxs)(`button`,{type:`button`,onClick:m,className:`flex h-12 items-center gap-3 rounded-lg border border-linie px-4 text-left text-base`,children:[x,` Einstellungen`]}),t[8]=m,t[9]=S);let C;t[10]!==b||t[11]!==S||t[12]!==f?(C=(0,d.jsxs)(s,{side:`bottom`,className:`border-linie bg-panel pb-[calc(1rem+env(safe-area-inset-bottom))]`,children:[_,(0,d.jsxs)(`div`,{className:`flex flex-col gap-2 px-4`,children:[f,v,b,S]})]}),t[10]=b,t[11]=S,t[12]=f,t[13]=C):C=t[13];let w;return t[14]!==g||t[15]!==C?(w=(0,d.jsx)(l,{open:!0,onOpenChange:g,children:C}),t[14]=g,t[15]=C,t[16]=w):w=t[16],w}export{f as MehrMenue};
|
||||
import{c as e,s as t}from"./button-Dd8hfusv.js";import{F as n,I as r,L as i}from"./index-D17zMI0S.js";import{a,i as o,n as s,r as c,t as l}from"./sheet-DWG2Q04V.js";var u=t(),d=e();function f(e){let t=(0,u.c)(17),{verbindung:f,onDienste:p,onEinstellungen:m,onSchliessen:h}=e,g;t[0]===h?g=t[1]:(g=e=>!e&&h(),t[0]=h,t[1]=g);let _;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(_=(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]=_):_=t[2];let v;t[3]===Symbol.for(`react.memo_cache_sentinel`)?(v=(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]=v):v=t[3];let y;t[4]===Symbol.for(`react.memo_cache_sentinel`)?(y=(0,d.jsx)(r,{className:`size-5`,"aria-hidden":!0}),t[4]=y):y=t[4];let b;t[5]===p?b=t[6]:(b=(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:[y,` Dienste und Protokolle`]}),t[5]=p,t[6]=b);let x;t[7]===Symbol.for(`react.memo_cache_sentinel`)?(x=(0,d.jsx)(n,{className:`size-5`,"aria-hidden":!0}),t[7]=x):x=t[7];let S;t[8]===m?S=t[9]:(S=(0,d.jsxs)(`button`,{type:`button`,onClick:m,className:`flex h-12 items-center gap-3 rounded-lg border border-linie px-4 text-left text-base`,children:[x,` Einstellungen`]}),t[8]=m,t[9]=S);let C;t[10]!==b||t[11]!==S||t[12]!==f?(C=(0,d.jsxs)(s,{side:`bottom`,className:`border-linie bg-panel pb-[calc(1rem+env(safe-area-inset-bottom))]`,children:[_,(0,d.jsxs)(`div`,{className:`flex flex-col gap-2 px-4`,children:[f,v,b,S]})]}),t[10]=b,t[11]=S,t[12]=f,t[13]=C):C=t[13];let w;return t[14]!==g||t[15]!==C?(w=(0,d.jsx)(l,{open:!0,onOpenChange:g,children:C}),t[14]=g,t[15]=C,t[16]=w):w=t[16],w}export{f as MehrMenue};
|
||||
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -7,7 +7,7 @@
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>MC2 · Box-Wart</title>
|
||||
<script type="module" crossorigin src="/assets/index-BRMEvo2A.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-D17zMI0S.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/button-Dd8hfusv.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-C3amg33Y.css">
|
||||
</head>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { api, post } from "./api"
|
||||
import { melden } from "./meldungen"
|
||||
import { stromVerbunden } from "./strom"
|
||||
import type {
|
||||
AktionsErgebnis, Job, Modell, Nutzung, Radar, Sicherung, Start,
|
||||
AktionsErgebnis, AufraeumKandidat, Job, Modell, Nutzung, Radar, Sicherung, Start,
|
||||
} from "./typen"
|
||||
|
||||
const takt = (ms: number) => () => (stromVerbunden() ? ms * 5 : ms)
|
||||
@@ -146,6 +146,19 @@ export const useUpdateStarten = () =>
|
||||
neuLaden: ["jobs", "start"],
|
||||
})
|
||||
|
||||
export const useAufraeumen = () =>
|
||||
useQuery({
|
||||
queryKey: ["aufraeumen"],
|
||||
queryFn: () => api<{ kandidaten: AufraeumKandidat[] }>("/api/modelle/aufraeumen"),
|
||||
staleTime: 60_000,
|
||||
})
|
||||
|
||||
export const useAufraeumenLoeschen = () =>
|
||||
useAktion(
|
||||
(k: AufraeumKandidat) => post<AktionsErgebnis>("/api/modelle/aufraeumen/loeschen", { art: k.art, name: k.name }),
|
||||
{ erfolg: (_, e) => (e as AktionsErgebnis).text ?? "Gelöscht.", neuLaden: ["aufraeumen", "models", "start"] },
|
||||
)
|
||||
|
||||
export const useFreigeben = () =>
|
||||
useAktion(
|
||||
(baustein: string) => post<AktionsErgebnis>(`/api/updates/festgehalten/${encodeURIComponent(baustein)}/freigeben`),
|
||||
|
||||
@@ -82,6 +82,14 @@ export interface Festgehalten {
|
||||
grund: string
|
||||
}
|
||||
|
||||
/** Etwas auf der Modell-Platte, das ungenutzt Platz belegt (Box-Wart, Aufräumen). */
|
||||
export interface AufraeumKandidat {
|
||||
art: "eintrag" | "ordner"
|
||||
name: string
|
||||
groesse_gb: number
|
||||
hinweis: string
|
||||
}
|
||||
|
||||
export interface AktionsErgebnis {
|
||||
ok: boolean
|
||||
text?: string
|
||||
|
||||
@@ -5,11 +5,12 @@ import { Chip, type ChipArt, Zustandsfeld } from "@/components/cockpit/Chip"
|
||||
import { Panel } from "@/components/cockpit/Panel"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
useModellLaden, useModelle, useNutzung, useRadar, useRadarAktion, useRadarSuche, useStart,
|
||||
useAufraeumen, useAufraeumenLoeschen, useModellLaden, useModelle, useNutzung, useRadar, useRadarAktion,
|
||||
useRadarSuche, useStart,
|
||||
} from "@/lib/abfragen"
|
||||
import { messwert, RADAR_STATUS_TEXT, rolleVon } from "@/lib/anzeige"
|
||||
import { useLetzteMetrik } from "@/lib/strom"
|
||||
import type { Modell, Nutzung, RadarKandidat, RadarTest } from "@/lib/typen"
|
||||
import type { AufraeumKandidat, Modell, Nutzung, RadarKandidat, RadarTest } from "@/lib/typen"
|
||||
import { flugplanZeit, gb, gbText, zahl } from "@/lib/zeit"
|
||||
import { ModellSuche } from "./ModellSuche"
|
||||
|
||||
@@ -273,6 +274,65 @@ function RadarPanel({ hirn, coder }: { hirn: string | undefined; coder: string |
|
||||
)
|
||||
}
|
||||
|
||||
/** Was ungenutzt Platz belegt. Gelöscht wird nur auf Knopfdruck, mit Rückfrage — Löschen ist endgültig. */
|
||||
function AufraeumenPanel() {
|
||||
const liste = useAufraeumen()
|
||||
const loeschen = useAufraeumenLoeschen()
|
||||
const [frage, setFrage] = useState<AufraeumKandidat | null>(null)
|
||||
const kandidaten = liste.data?.kandidaten ?? []
|
||||
const summe = kandidaten.reduce((s, k) => s + k.groesse_gb, 0)
|
||||
if (liste.isError) return null
|
||||
return (
|
||||
<Panel
|
||||
titel="Aufräumen"
|
||||
rechts={kandidaten.length > 0
|
||||
? <span className="ziffern text-sm text-text-2">bis zu {summe.toLocaleString("de-DE", { maximumFractionDigits: 0 })} GB frei</span>
|
||||
: undefined}
|
||||
>
|
||||
<p className="-mt-2 text-sm text-text-2">
|
||||
Modelle und Ordner, die gerade niemand nutzt. Nichts davon wird von selbst gelöscht, das entscheidest du je Eintrag.
|
||||
</p>
|
||||
{liste.isPending ? (
|
||||
<p className="text-[15px] text-text-2">Wird gelesen …</p>
|
||||
) : kandidaten.length === 0 ? (
|
||||
<p className="text-[15px] text-text-2">Nichts aufzuräumen.</p>
|
||||
) : (
|
||||
<ul className="m-0 flex list-none flex-col p-0">
|
||||
{kandidaten.map((k) => (
|
||||
<li key={`${k.art}-${k.name}`} className="flex flex-wrap items-center justify-between gap-3 border-t border-linie py-3">
|
||||
<span className="flex min-w-0 flex-1 flex-col gap-1">
|
||||
<span className="flex flex-wrap items-center gap-2">
|
||||
<span className="ziffern text-sm break-all">{k.name}</span>
|
||||
<Chip>{k.art === "eintrag" ? "Eintrag" : "Ordner"}</Chip>
|
||||
</span>
|
||||
<span className="text-sm text-text-2">{k.hinweis}</span>
|
||||
</span>
|
||||
<span className="flex items-center gap-3">
|
||||
<span className="ziffern text-sm text-text-2">{k.groesse_gb.toLocaleString("de-DE")} GB</span>
|
||||
<Button variant="gefahr" size="sm" disabled={loeschen.isPending} onClick={() => setFrage(k)}>
|
||||
Löschen
|
||||
</Button>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<Bestaetigen
|
||||
offen={frage !== null}
|
||||
gefahr
|
||||
titel={`${frage?.name ?? ""} löschen?`}
|
||||
text={`${frage?.hinweis ?? ""} Es werden ${frage?.groesse_gb.toLocaleString("de-DE") ?? ""} GB frei. Das lässt sich nicht rückgängig machen — zurück ginge es nur mit einem neuen Download.`}
|
||||
knopf="Endgültig löschen"
|
||||
onNein={() => setFrage(null)}
|
||||
onJa={() => {
|
||||
if (frage) loeschen.mutate(frage)
|
||||
setFrage(null)
|
||||
}}
|
||||
/>
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
|
||||
export function ModelleSeite() {
|
||||
const modelle = useModelle()
|
||||
const nutzung = useNutzung()
|
||||
@@ -324,6 +384,7 @@ export function ModelleSeite() {
|
||||
))}
|
||||
</ul>
|
||||
</Panel>
|
||||
<AufraeumenPanel />
|
||||
<ModellSuche offen={suche} onSchliessen={() => setSuche(false)} />
|
||||
</>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user