fix: 500er bei register/update_model/install-recipe, Download & favicon

- write_config schreibt atomar (tmp + os.replace) -> llama-swap (-watch-config)
  sieht nie eine halb geschriebene config.yaml; PermissionError wird in eine
  klare Klartext-Meldung uebersetzt (Hinweis auf chown)
- generischer Exception-Handler: unerwartete Fehler kommen als lesbare JSON-
  Meldung im UI an statt als nackter 500 (Anfaenger-Diagnose)
- Download: nutzt die `hf`-CLI aus dem venv (Dienst-PATH kennt venv/bin nicht)
  + HF_HUB_DISABLE_XET=1 statt HF_XET_HIGH_PERFORMANCE (XET-Haenger bei ~6 MB)
- huggingface_hub als Dependency ergaenzt (liefert `hf`)
- favicon.ico: themed SVG (Link-Tag + Route) -> kein 404 mehr

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-21 17:53:49 +02:00
parent 3a4de152b0
commit f07a8440b2
5 changed files with 51 additions and 6 deletions
+21 -1
View File
@@ -17,7 +17,7 @@ Neue Bereiche kommen als routers/<bereich>.py + static/js/panels/<bereich>.js da
from pathlib import Path
from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse, JSONResponse
from fastapi.responses import FileResponse, JSONResponse, Response
from fastapi.staticfiles import StaticFiles
from routers import jobs, maintenance, models, system, cookbook, integration, news
@@ -52,9 +52,29 @@ def index():
return FileResponse(_STATIC / "index.html")
_FAVICON = (
b"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'>"
b"<rect width='32' height='32' rx='7' fill='#0f1720'/>"
b"<circle cx='16' cy='16' r='8' fill='none' stroke='#2dd4bf' stroke-width='3'/>"
b"<circle cx='16' cy='16' r='2.5' fill='#2dd4bf'/></svg>"
)
@app.get("/favicon.ico")
def favicon():
return Response(content=_FAVICON, media_type="image/svg+xml")
app.mount("/static", StaticFiles(directory=_STATIC), name="static")
@app.exception_handler(HTTPException)
def _http_exc(_req, exc: HTTPException):
return JSONResponse(status_code=exc.status_code, content={"error": exc.detail})
@app.exception_handler(Exception)
def _any_exc(_req, exc: Exception):
"""Unerwartete Fehler als lesbare Meldung ans (vertrauenswuerdige LAN-)UI geben,
statt nur einen generischen 500 ohne Hinweis. Erleichtert Anfaengern die Diagnose."""
return JSONResponse(status_code=500, content={"error": str(exc) or exc.__class__.__name__})
+14 -1
View File
@@ -6,6 +6,8 @@ Helfer rund um llama-swap und dessen config.yaml.
sodass Kommentare und Quotes erhalten bleiben.
"""
import os
import httpx
from config import CONFIG_PATH, LLAMA_SWAP_URL, yaml
@@ -29,6 +31,17 @@ def read_config() -> dict:
def write_config(cfg: dict) -> None:
"""Schreibt die config.yaml atomar (tmp-Datei + os.replace), damit llama-swap
mit -watch-config nie eine halb geschriebene Datei sieht. Fehlende Schreibrechte
werden in eine klare Meldung uebersetzt statt als roher Traceback zu landen."""
try:
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
with CONFIG_PATH.open("w", encoding="utf-8") as f:
tmp = CONFIG_PATH.with_name(CONFIG_PATH.name + ".tmp")
with tmp.open("w", encoding="utf-8") as f:
yaml.dump(cfg, f)
os.replace(tmp, CONFIG_PATH)
except PermissionError as exc:
raise PermissionError(
f"Mission Control darf '{CONFIG_PATH}' nicht schreiben. "
f"Einmalig Besitz uebergeben: sudo chown -R hitonabi:hitonabi {CONFIG_PATH.parent}"
) from exc
+1
View File
@@ -3,3 +3,4 @@ uvicorn[standard]>=0.29
httpx>=0.27
ruamel.yaml>=0.18
psutil>=5.9.0
huggingface_hub>=0.34 # liefert die `hf`-CLI fuer Modell-Downloads
+12 -2
View File
@@ -19,11 +19,20 @@ from llamaswap import _swap_get, read_config, write_config
from hw_math import extract_params_b, max_ctx_for, estimate_memory_gb
import re
import os
import sys
import psutil
router = APIRouter(prefix="/api", dependencies=[Depends(auth)])
def _hf_bin() -> str:
"""Pfad zur `hf`-CLI. Bevorzugt die im venv installierte (neben dem laufenden
Python), da der Dienst-PATH das venv/bin meist nicht enthaelt. Faellt sonst auf
ein global installiertes `hf` zurueck."""
cand = os.path.join(os.path.dirname(sys.executable), "hf")
return cand if os.path.exists(cand) else "hf"
# ---------------------------------------------------------------------------
# Request-Modelle
# ---------------------------------------------------------------------------
@@ -139,8 +148,9 @@ def download(req: DownloadReq):
sub = req.subdir or req.repo.split("/")[-1]
target = MODELS_DIR / sub
target.mkdir(parents=True, exist_ok=True)
args = ["hf", "download", req.repo, req.file, "--local-dir", str(target)]
env = {"HF_XET_HIGH_PERFORMANCE": "1"}
args = [_hf_bin(), "download", req.repo, req.file, "--local-dir", str(target)]
# XET deaktivieren: mit aktivem XET haengt der Download reproduzierbar bei ~6 MB (siehe CLAUDE.md).
env = {"HF_HUB_DISABLE_XET": "1"}
if req.hf_token:
env["HF_TOKEN"] = req.hf_token
job_id = start_job(args, f"download {req.repo}/{req.file}", env=env)
+1
View File
@@ -4,6 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mission Control</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%230f1720'/%3E%3Ccircle cx='16' cy='16' r='8' fill='none' stroke='%232dd4bf' stroke-width='3'/%3E%3Ccircle cx='16' cy='16' r='2.5' fill='%232dd4bf'/%3E%3C/svg%3E">
<link rel="stylesheet" href="/static/css/base.css">
<link rel="stylesheet" href="/static/css/components.css">
</head>