diff --git a/config.py b/config.py index 736cb7c..5cbecd6 100644 --- a/config.py +++ b/config.py @@ -29,6 +29,10 @@ UPDATE_CMD = os.environ.get("MC_UPDATE_CMD", "") DEFAULT_TTL = int(os.environ.get("MC_DEFAULT_TTL", "300")) TOKEN = os.environ.get("MC_TOKEN", "") # leer = keine Auth (nur LAN!) +# Self-Update ("Mission Control aktualisieren"): Quelle = git-Repo, Prod = laufende Installation. +SOURCE_DIR = os.path.expanduser(os.environ.get("MC_SOURCE_DIR", "~/mission-control")) +PROD_DIR = str(Path(__file__).resolve().parent) + # Gemeinsame YAML-Instanz (preserve_quotes haelt Kommentare/Quotes in config.yaml). yaml = YAML() yaml.preserve_quotes = True diff --git a/routers/maintenance.py b/routers/maintenance.py index b141df2..29fa28c 100644 --- a/routers/maintenance.py +++ b/routers/maintenance.py @@ -14,7 +14,7 @@ from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisco from pydantic import BaseModel from auth import auth -from config import UPDATE_CMD +from config import UPDATE_CMD, SOURCE_DIR, PROD_DIR from jobengine import start_job router = APIRouter(prefix="/api", dependencies=[Depends(auth)]) @@ -24,9 +24,25 @@ class PwdReq(BaseModel): @router.post("/update") def update(): + """LLM-Engine (llama.cpp) aktualisieren — Befehl aus MC_UPDATE_CMD.""" if not UPDATE_CMD: raise HTTPException(400, "Kein Update-Befehl gesetzt (MC_UPDATE_CMD).") - job_id = start_job(shlex.split(UPDATE_CMD), "update containers") + job_id = start_job(shlex.split(UPDATE_CMD), "llama.cpp-Update") + return {"job_id": job_id} + +@router.post("/self-update") +def self_update(): + """Mission Control selbst aktualisieren: Quelle pullen -> nach Prod rsyncen -> Dienst neu starten. + git pull liest aus Gitea; der Restart laeuft ueber die NOPASSWD-sudoers-Whitelist. Die einzelnen + Schritte sind mit && verkettet -> bei einem Fehler (z.B. Gitea offline) bricht es sauber ab.""" + cmd = ( + f"cd {shlex.quote(SOURCE_DIR)} && git pull --ff-only && " + f"rsync -a --exclude=.git --exclude=.venv --exclude=__pycache__ --exclude='*.pyc' " + f"{shlex.quote(SOURCE_DIR)}/ {shlex.quote(PROD_DIR)}/ && " + f"sudo systemctl restart mission-control" + ) + job_id = start_job(["bash", "-c", cmd], "Mission Control Update", + log_cmd="$ git pull && rsync -> /opt && systemctl restart mission-control") return {"job_id": job_id} @router.post("/os-update") diff --git a/static/js/panels/guides.js b/static/js/panels/guides.js index 3f2ed22..a09b350 100644 --- a/static/js/panels/guides.js +++ b/static/js/panels/guides.js @@ -1,6 +1,15 @@ // guides.js — Integrations-Anleitungen (v3): Copy-Paste-Configs für externe Tools. -import { $ } from "../core/ui.js"; +import { $, esc } from "../core/ui.js"; + +// In die Zwischenablage kopieren (kopiert das vorhergehende Eingabefeld / Codeblock). +window.mcCopy = (btn, sel) => { + const src = sel ? btn.parentElement.querySelector(sel) : btn.previousElementSibling; + const text = src.value !== undefined ? src.value : src.textContent; + navigator.clipboard?.writeText(text); + const old = btn.textContent; btn.textContent = "Kopiert!"; + setTimeout(() => (btn.textContent = old), 1200); +}; // Basis-URL aus dem Browser ableiten (zeigt die echte Bosgame-Adresse statt localhost). function apiBase() { @@ -8,46 +17,20 @@ function apiBase() { return `${location.protocol}//${host}:8080/v1`; } +// Beschriftetes Feld mit Kopier-Button. +function field(label, value) { + return ` +
+ + +
`; +} + function render() { const c = $(".view[data-view='guides']"); if (!c) return; const url = apiBase(); - c.innerHTML = ` -
-

Guides & Integrationen

-
Binde deine lokalen Modelle in andere Tools ein — fertige Configs zum Kopieren.
- -
-
- Cline / Cursor -
-

Nutze deine lokalen Modelle kostenlos in Cursor oder Cline (Provider: „OpenAI Compatible").

- - - - - - -
-
- -
- OpenWebUI -
-

Settings → Admin → Connections → neue OpenAI-Verbindung:

- - - - -

OpenWebUI erkennt automatisch, wenn ein Modell getauscht wird.

-
-
- -
- Python / LangChain (openai-SDK) -
-

Mit dem offiziellen openai-Paket:

-
from openai import OpenAI + const py = `from openai import OpenAI client = OpenAI( base_url="${url}", @@ -58,7 +41,42 @@ resp = client.chat.completions.create( model="coder", # Alias aus dem Cookbook messages=[{"role": "user", "content": "Hallo Modell!"}] ) -print(resp.choices[0].message.content)
+print(resp.choices[0].message.content)`; + + c.innerHTML = ` +
+

Guides & Integrationen

+
Binde deine lokalen Modelle in andere Tools ein — fertige Configs zum Kopieren.
+ +
+
+ Cline / Cursor +
+

Nutze deine lokalen Modelle kostenlos in Cursor oder Cline (Provider: „OpenAI Compatible").

+ ${field("Modell-ID", "coder")} + ${field("Basis-URL (OpenAI-kompatibel)", url)} + ${field("API-Key", "(dein Token oder leer lassen)")} +
+
+ +
+ OpenWebUI +
+

Settings → Admin → Connections → neue OpenAI-Verbindung:

+ ${field("Basis-URL", url)} + ${field("API-Key", "dummy-key")} +

OpenWebUI erkennt automatisch, wenn ein Modell getauscht wird.

+
+
+ +
+ Python / LangChain (openai-SDK) +
+

Mit dem offiziellen openai-Paket:

+
+ +
${esc(py)}
+
@@ -66,8 +84,7 @@ print(resp.choices[0].message.content)
n8n (AI-Agent-Nodes)

„OpenAI Chat Model"-Node → Credentials → Custom URL:

- - + ${field("Basis-URL", url)}

Modell-ID „coder" eintragen und loslegen.

diff --git a/static/js/panels/server.js b/static/js/panels/server.js index 2cb984c..02e10cb 100644 --- a/static/js/panels/server.js +++ b/static/js/panels/server.js @@ -26,7 +26,8 @@ function mount() {
Diese Aktionen sind harmlos und passwortlos — es geht nichts dabei verloren.
${row("w-restart-swap", "refresh", "teal", "LLM-Engine neustarten", "Startet llama-swap neu. Geladene Modelle werden kurz entladen (~5 Sek), laden danach automatisch wieder.")} ${row("w-restart-mc", "refresh", "blue", "Dashboard neustarten", "Startet diese Oberfläche neu. Sie trennt kurz und verbindet automatisch wieder.")} - ${row("w-update", "download", "teal", "LLM-Engine aktualisieren", "Lädt die neueste llama.cpp-Version (ROCm) für deine GPU und installiert sie.")} + ${row("w-self-update", "download", "teal", "Mission Control aktualisieren", "Holt die neueste Dashboard-Version aus Gitea und startet automatisch neu (~10 Sek).")} + ${row("w-update", "bolt", "blue", "LLM-Engine aktualisieren", "Lädt die neueste llama.cpp-Version (ROCm) für deine GPU und installiert sie.")} ${row("w-unload", "database", "amber", "Grafikspeicher leeren", "Wirft alle aktuell geladenen Modelle aus dem VRAM. Fragt vorher nach.")}
@@ -54,6 +55,7 @@ function mount() { "LLM-Engine neustarten?", "Die Engine startet neu. Geladene Modelle werden kurz entladen (~5 Sekunden), laden danach automatisch wieder.")); $("#w-restart-mc").addEventListener("click", () => restartService("mission-control", "Dashboard neustarten?", "Diese Oberfläche trennt sich kurz und verbindet automatisch wieder. Laufende Downloads laufen weiter.")); + $("#w-self-update").addEventListener("click", selfUpdate); $("#w-update").addEventListener("click", update); $("#w-unload").addEventListener("click", unloadAll); $("#w-os-update").addEventListener("click", osUpdate); @@ -85,6 +87,12 @@ async function unloadAll() { catch (e) { toast(e.message, true); } } +async function selfUpdate() { + if (!await confirmModal({ title: "Mission Control aktualisieren?", body: "Holt die neueste Dashboard-Version aus Gitea und startet automatisch neu. Die Seite trennt kurz und verbindet von selbst wieder (~10 Sek)." })) return; + try { const r = await api("/api/self-update", { method: "POST" }); toast("Update läuft — Dashboard startet gleich neu."); track(r.job_id); } + catch (e) { toast(e.message, true); } +} + async function update() { try { const r = await api("/api/update", { method: "POST" }); toast("llama.cpp-Update läuft — siehe Aktivität."); track(r.job_id); } catch (e) { toast(e.message, true); }