fix(ux): Idiotensicherheit — 8 Audit-Fixes

Behebung der drei kritischsten UX-Schwachstellen aus dem Produkt-Audit:

1. ConnectPanel: Steps 2-4 (Modellauswahl, Tool-Picker, Config-Snippet,
   Vision) werden blockiert wenn noch kein Modell installiert ist.
   Stattdessen klarer Hinweis "Erst im Cookbook ein Modell holen" mit
   direktem CTA. Verhindert dass Anfänger ein Snippet mit Fake-"coder"
   kopieren, bevor ein echtes Modell existiert.

2. OverviewPanel: preload() schluckte Fehler still (catch {}). Zeigt
   jetzt einen verständlichen Toast wenn die Engine nicht erreichbar ist.
   Schnellstart ist kontextsensitiv: ohne Modelle zeigt er Onboarding-
   Schritte (Modell holen → Verbinden → Guide lesen) statt sinnloser
   Aktionen wie "Speicher freigeben".

3. CookbookPanel Profi-Suche: doDownload() nutzte zwei sequenzielle
   API-Calls (download + register) — wenn der zweite scheiterte, blieb
   das System in einem inkonsistenten Halbzustand. Jetzt ein einziger
   atomarer Aufruf an /api/cookbook/install-model (wie Recipe/Discover).
   Backend-Erweiterung: InstallModelReq akzeptiert jetzt 'file' und
   'ctx' um Nutzer-Auswahl direkt zu übergeben.

4. Cookbook Section 1 als "Hier starten" markiert ("1 · Anwendungsfall
   wählen & installieren" mit erklärendem Sub-Text).

5. Post-Download-Toasts: Alle drei Install-Pfade sagen nun einheitlich
   "Modell erscheint danach automatisch im Modelle-Tab" statt nur
   "siehe Aktivität".

6. too_tight-Alternative: Bei Modellen/Setups die den Speicher übersteigen
   wird jetzt eine konkrete Alternative (kleinere Datei / passendes Rezept)
   verlinkt statt nur "Trotzdem installieren".

7. Duplikat extract_params_b aus cookbook.py entfernt — importiert jetzt
   aus hw_math (Single Source of Truth).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-23 08:31:34 +02:00
parent a93751fb01
commit c33fe57433
5 changed files with 213 additions and 152 deletions
+8 -16
View File
@@ -14,7 +14,7 @@ import psutil
from ruamel.yaml.scalarstring import LiteralScalarString
from auth import auth
from hw_math import evaluate_fit, max_ctx_for
from hw_math import evaluate_fit, max_ctx_for, extract_params_b
from config import (MODELS_DIR, CMD_TEMPLATE, DEFAULT_TTL, HF_DOWNLOAD_ENV, USER_RECIPES_PATH,
DISCOVER_CACHE_PATH, DISCOVER_TTL, hf_bin)
from llamaswap import read_config, write_config, model_id_from_path, set_role_alias
@@ -68,6 +68,8 @@ class InstallModelReq(BaseModel):
role: str
params_b: float
quant: str = "Q4_K_M"
file: str | None = None # konkrete GGUF-Datei; None → _pick_gguf wählt automatisch
ctx: int | None = None # None → max_ctx_for (Hardware-Optimum)
hf_token: str | None = None
@@ -85,18 +87,6 @@ class UserRecipeReq(BaseModel):
icon: str = "box"
models: list[UserRecipeModelReq]
def extract_params_b(repo_id: str) -> float:
"""Extrahiert die Parametergröße (in Milliarden) aus dem Repo-Namen."""
# z.B. Qwen2.5-Coder-32B -> 32
# 8x7B -> 56 (MoE)
moe = re.search(r"(\d+)x(\d+(?:\.\d+)?)[bB]", repo_id)
if moe:
return float(moe.group(1)) * float(moe.group(2))
m = re.search(r"(\d+(?:\.\d+)?)[bB](?![a-zA-Z])", repo_id)
if m:
return float(m.group(1))
return 7.0 # Fallback
def extract_quant(filename: str) -> str:
m = re.search(r"(Q\d_[A-Z0-9_]+|IQ\d_[A-Z0-9_]+|FP16|BF16)", filename, re.IGNORECASE)
return m.group(1).upper() if m else "Q4_K_M"
@@ -531,8 +521,10 @@ def upgrades():
@router.post("/install-model")
def install_model(req: InstallModelReq):
"""Ein einzelnes Modell installieren (Download + Einpflegen unter 'role', optimaler ctx)."""
file = _pick_gguf(req.repo, req.quant)
"""Ein einzelnes Modell installieren (Download + Einpflegen unter 'role', optimaler ctx).
Wenn 'file' übergeben wird, wird genau diese GGUF-Datei geladen (Profi-Suche-Auswahl);
andernfalls wählt _pick_gguf automatisch die beste Variante für den gewünschten Quant."""
file = req.file or _pick_gguf(req.repo, req.quant)
if not file:
raise HTTPException(404, "Keine GGUF-Datei im Repo gefunden.")
mmproj = _pick_mmproj(req.repo)
@@ -550,7 +542,7 @@ def install_model(req: InstallModelReq):
JOBS[jid]["result_path"] = str(target / file)
attach_download_progress(jid, str(target), hf_file_size(req.repo, file))
cfg = read_config()
ctx = max_ctx_for(req.params_b, req.quant, ram_gb)
ctx = req.ctx if req.ctx else max_ctx_for(req.params_b, req.quant, ram_gb)
path = str(target / file)
cmd = CMD_TEMPLATE.replace("{model}", path).replace("{ctx}", str(ctx))
if mmproj: