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:
@@ -17,7 +17,7 @@ Neue Bereiche kommen als routers/<bereich>.py + static/js/panels/<bereich>.js da
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException
|
||||||
from fastapi.responses import FileResponse, JSONResponse
|
from fastapi.responses import FileResponse, JSONResponse, Response
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from routers import jobs, maintenance, models, system, cookbook, integration, news
|
from routers import jobs, maintenance, models, system, cookbook, integration, news
|
||||||
@@ -52,9 +52,29 @@ def index():
|
|||||||
return FileResponse(_STATIC / "index.html")
|
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.mount("/static", StaticFiles(directory=_STATIC), name="static")
|
||||||
|
|
||||||
|
|
||||||
@app.exception_handler(HTTPException)
|
@app.exception_handler(HTTPException)
|
||||||
def _http_exc(_req, exc: HTTPException):
|
def _http_exc(_req, exc: HTTPException):
|
||||||
return JSONResponse(status_code=exc.status_code, content={"error": exc.detail})
|
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__})
|
||||||
|
|||||||
+16
-3
@@ -6,6 +6,8 @@ Helfer rund um llama-swap und dessen config.yaml.
|
|||||||
sodass Kommentare und Quotes erhalten bleiben.
|
sodass Kommentare und Quotes erhalten bleiben.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from config import CONFIG_PATH, LLAMA_SWAP_URL, yaml
|
from config import CONFIG_PATH, LLAMA_SWAP_URL, yaml
|
||||||
@@ -29,6 +31,17 @@ def read_config() -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def write_config(cfg: dict) -> None:
|
def write_config(cfg: dict) -> None:
|
||||||
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
"""Schreibt die config.yaml atomar (tmp-Datei + os.replace), damit llama-swap
|
||||||
with CONFIG_PATH.open("w", encoding="utf-8") as f:
|
mit -watch-config nie eine halb geschriebene Datei sieht. Fehlende Schreibrechte
|
||||||
yaml.dump(cfg, f)
|
werden in eine klare Meldung uebersetzt statt als roher Traceback zu landen."""
|
||||||
|
try:
|
||||||
|
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
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
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ uvicorn[standard]>=0.29
|
|||||||
httpx>=0.27
|
httpx>=0.27
|
||||||
ruamel.yaml>=0.18
|
ruamel.yaml>=0.18
|
||||||
psutil>=5.9.0
|
psutil>=5.9.0
|
||||||
|
huggingface_hub>=0.34 # liefert die `hf`-CLI fuer Modell-Downloads
|
||||||
|
|||||||
+12
-2
@@ -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
|
from hw_math import extract_params_b, max_ctx_for, estimate_memory_gb
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import psutil
|
import psutil
|
||||||
|
|
||||||
router = APIRouter(prefix="/api", dependencies=[Depends(auth)])
|
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
|
# Request-Modelle
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -139,8 +148,9 @@ def download(req: DownloadReq):
|
|||||||
sub = req.subdir or req.repo.split("/")[-1]
|
sub = req.subdir or req.repo.split("/")[-1]
|
||||||
target = MODELS_DIR / sub
|
target = MODELS_DIR / sub
|
||||||
target.mkdir(parents=True, exist_ok=True)
|
target.mkdir(parents=True, exist_ok=True)
|
||||||
args = ["hf", "download", req.repo, req.file, "--local-dir", str(target)]
|
args = [_hf_bin(), "download", req.repo, req.file, "--local-dir", str(target)]
|
||||||
env = {"HF_XET_HIGH_PERFORMANCE": "1"}
|
# 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:
|
if req.hf_token:
|
||||||
env["HF_TOKEN"] = req.hf_token
|
env["HF_TOKEN"] = req.hf_token
|
||||||
job_id = start_job(args, f"download {req.repo}/{req.file}", env=env)
|
job_id = start_job(args, f"download {req.repo}/{req.file}", env=env)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Mission Control</title>
|
<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/base.css">
|
||||||
<link rel="stylesheet" href="/static/css/components.css">
|
<link rel="stylesheet" href="/static/css/components.css">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
Reference in New Issue
Block a user