v3: Mission Control Self-Update + Guides-Copy + Button-Audit
- Neuer Self-Update-Button ("Mission Control aktualisieren"): POST /api/self-update
macht git pull (Quelle) -> rsync nach Prod -> systemctl restart (NOPASSWD).
Pfade aus config.py (SOURCE_DIR/PROD_DIR), nicht user-hartkodiert.
- "LLM-Engine aktualisieren" ehrlich (war MC, ist llama.cpp via MC_UPDATE_CMD).
- Guides: Kopier-Buttons fuer alle Configs + Codeblock (Copy-Paste-tauglich).
- Button-Audit: Funktionen/Labels in allen Panels geprueft, stimmig.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,10 @@ UPDATE_CMD = os.environ.get("MC_UPDATE_CMD", "")
|
|||||||
DEFAULT_TTL = int(os.environ.get("MC_DEFAULT_TTL", "300"))
|
DEFAULT_TTL = int(os.environ.get("MC_DEFAULT_TTL", "300"))
|
||||||
TOKEN = os.environ.get("MC_TOKEN", "") # leer = keine Auth (nur LAN!)
|
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).
|
# Gemeinsame YAML-Instanz (preserve_quotes haelt Kommentare/Quotes in config.yaml).
|
||||||
yaml = YAML()
|
yaml = YAML()
|
||||||
yaml.preserve_quotes = True
|
yaml.preserve_quotes = True
|
||||||
|
|||||||
+18
-2
@@ -14,7 +14,7 @@ from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisco
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from auth import auth
|
from auth import auth
|
||||||
from config import UPDATE_CMD
|
from config import UPDATE_CMD, SOURCE_DIR, PROD_DIR
|
||||||
from jobengine import start_job
|
from jobengine import start_job
|
||||||
|
|
||||||
router = APIRouter(prefix="/api", dependencies=[Depends(auth)])
|
router = APIRouter(prefix="/api", dependencies=[Depends(auth)])
|
||||||
@@ -24,9 +24,25 @@ class PwdReq(BaseModel):
|
|||||||
|
|
||||||
@router.post("/update")
|
@router.post("/update")
|
||||||
def update():
|
def update():
|
||||||
|
"""LLM-Engine (llama.cpp) aktualisieren — Befehl aus MC_UPDATE_CMD."""
|
||||||
if not UPDATE_CMD:
|
if not UPDATE_CMD:
|
||||||
raise HTTPException(400, "Kein Update-Befehl gesetzt (MC_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}
|
return {"job_id": job_id}
|
||||||
|
|
||||||
@router.post("/os-update")
|
@router.post("/os-update")
|
||||||
|
|||||||
+57
-40
@@ -1,6 +1,15 @@
|
|||||||
// guides.js — Integrations-Anleitungen (v3): Copy-Paste-Configs für externe Tools.
|
// 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).
|
// Basis-URL aus dem Browser ableiten (zeigt die echte Bosgame-Adresse statt localhost).
|
||||||
function apiBase() {
|
function apiBase() {
|
||||||
@@ -8,46 +17,20 @@ function apiBase() {
|
|||||||
return `${location.protocol}//${host}:8080/v1`;
|
return `${location.protocol}//${host}:8080/v1`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Beschriftetes Feld mit Kopier-Button.
|
||||||
|
function field(label, value) {
|
||||||
|
return `<label>${label}</label>
|
||||||
|
<div class="flex gap-2" style="align-items:stretch;margin-bottom:12px">
|
||||||
|
<input class="mono-sm" readonly value="${esc(value)}" style="flex:1;margin:0">
|
||||||
|
<button class="ghost" onclick="window.mcCopy(this)">Kopieren</button>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
const c = $(".view[data-view='guides']");
|
const c = $(".view[data-view='guides']");
|
||||||
if (!c) return;
|
if (!c) return;
|
||||||
const url = apiBase();
|
const url = apiBase();
|
||||||
c.innerHTML = `
|
const py = `from openai import OpenAI
|
||||||
<div class="pagehead"><div>
|
|
||||||
<h1>Guides & Integrationen</h1>
|
|
||||||
<div class="sub">Binde deine lokalen Modelle in andere Tools ein — fertige Configs zum Kopieren.</div></div></div>
|
|
||||||
|
|
||||||
<div class="card" style="padding:0;overflow:hidden">
|
|
||||||
<details class="guide-acc">
|
|
||||||
<summary>Cline / Cursor</summary>
|
|
||||||
<div class="acc-body">
|
|
||||||
<p>Nutze deine lokalen Modelle kostenlos in Cursor oder Cline (Provider: „OpenAI Compatible").</p>
|
|
||||||
<label>Modell-ID</label>
|
|
||||||
<input class="mono-sm" readonly value="coder">
|
|
||||||
<label>Basis-URL (OpenAI-kompatibel)</label>
|
|
||||||
<input class="mono-sm" readonly value="${url}">
|
|
||||||
<label>API-Key</label>
|
|
||||||
<input class="mono-sm" readonly value="(dein Token oder leer lassen)">
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="guide-acc">
|
|
||||||
<summary>OpenWebUI</summary>
|
|
||||||
<div class="acc-body">
|
|
||||||
<p>Settings → Admin → Connections → neue OpenAI-Verbindung:</p>
|
|
||||||
<label>Basis-URL</label>
|
|
||||||
<input class="mono-sm" readonly value="${url}">
|
|
||||||
<label>API-Key</label>
|
|
||||||
<input class="mono-sm" readonly value="dummy-key">
|
|
||||||
<p class="hint" style="margin-top:10px">OpenWebUI erkennt automatisch, wenn ein Modell getauscht wird.</p>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="guide-acc">
|
|
||||||
<summary>Python / LangChain (openai-SDK)</summary>
|
|
||||||
<div class="acc-body">
|
|
||||||
<p>Mit dem offiziellen <code>openai</code>-Paket:</p>
|
|
||||||
<div class="log" style="max-height:none"><code>from openai import OpenAI
|
|
||||||
|
|
||||||
client = OpenAI(
|
client = OpenAI(
|
||||||
base_url="${url}",
|
base_url="${url}",
|
||||||
@@ -58,7 +41,42 @@ resp = client.chat.completions.create(
|
|||||||
model="coder", # Alias aus dem Cookbook
|
model="coder", # Alias aus dem Cookbook
|
||||||
messages=[{"role": "user", "content": "Hallo Modell!"}]
|
messages=[{"role": "user", "content": "Hallo Modell!"}]
|
||||||
)
|
)
|
||||||
print(resp.choices[0].message.content)</code></div>
|
print(resp.choices[0].message.content)`;
|
||||||
|
|
||||||
|
c.innerHTML = `
|
||||||
|
<div class="pagehead"><div>
|
||||||
|
<h1>Guides & Integrationen</h1>
|
||||||
|
<div class="sub">Binde deine lokalen Modelle in andere Tools ein — fertige Configs zum Kopieren.</div></div></div>
|
||||||
|
|
||||||
|
<div class="card" style="padding:0;overflow:hidden">
|
||||||
|
<details class="guide-acc">
|
||||||
|
<summary>Cline / Cursor</summary>
|
||||||
|
<div class="acc-body">
|
||||||
|
<p>Nutze deine lokalen Modelle kostenlos in Cursor oder Cline (Provider: „OpenAI Compatible").</p>
|
||||||
|
${field("Modell-ID", "coder")}
|
||||||
|
${field("Basis-URL (OpenAI-kompatibel)", url)}
|
||||||
|
${field("API-Key", "(dein Token oder leer lassen)")}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="guide-acc">
|
||||||
|
<summary>OpenWebUI</summary>
|
||||||
|
<div class="acc-body">
|
||||||
|
<p>Settings → Admin → Connections → neue OpenAI-Verbindung:</p>
|
||||||
|
${field("Basis-URL", url)}
|
||||||
|
${field("API-Key", "dummy-key")}
|
||||||
|
<p class="hint" style="margin-top:10px">OpenWebUI erkennt automatisch, wenn ein Modell getauscht wird.</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="guide-acc">
|
||||||
|
<summary>Python / LangChain (openai-SDK)</summary>
|
||||||
|
<div class="acc-body">
|
||||||
|
<p>Mit dem offiziellen <code>openai</code>-Paket:</p>
|
||||||
|
<div style="position:relative">
|
||||||
|
<button class="ghost" style="position:absolute;top:8px;right:8px;z-index:1" onclick="window.mcCopy(this,'code')">Kopieren</button>
|
||||||
|
<div class="log" style="max-height:none"><code>${esc(py)}</code></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -66,8 +84,7 @@ print(resp.choices[0].message.content)</code></div>
|
|||||||
<summary>n8n (AI-Agent-Nodes)</summary>
|
<summary>n8n (AI-Agent-Nodes)</summary>
|
||||||
<div class="acc-body">
|
<div class="acc-body">
|
||||||
<p>„OpenAI Chat Model"-Node → Credentials → Custom URL:</p>
|
<p>„OpenAI Chat Model"-Node → Credentials → Custom URL:</p>
|
||||||
<label>Basis-URL</label>
|
${field("Basis-URL", url)}
|
||||||
<input class="mono-sm" readonly value="${url}">
|
|
||||||
<p class="hint" style="margin-top:10px">Modell-ID „coder" eintragen und loslegen.</p>
|
<p class="hint" style="margin-top:10px">Modell-ID „coder" eintragen und loslegen.</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ function mount() {
|
|||||||
<div class="card-sub">Diese Aktionen sind harmlos und passwortlos — es geht nichts dabei verloren.</div>
|
<div class="card-sub">Diese Aktionen sind harmlos und passwortlos — es geht nichts dabei verloren.</div>
|
||||||
${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-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-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.")}
|
${row("w-unload", "database", "amber", "Grafikspeicher leeren", "Wirft alle aktuell geladenen Modelle aus dem VRAM. Fragt vorher nach.")}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -54,6 +55,7 @@ function mount() {
|
|||||||
"LLM-Engine neustarten?", "Die Engine startet neu. Geladene Modelle werden kurz entladen (~5 Sekunden), laden danach automatisch wieder."));
|
"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",
|
$("#w-restart-mc").addEventListener("click", () => restartService("mission-control",
|
||||||
"Dashboard neustarten?", "Diese Oberfläche trennt sich kurz und verbindet automatisch wieder. Laufende Downloads laufen weiter."));
|
"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-update").addEventListener("click", update);
|
||||||
$("#w-unload").addEventListener("click", unloadAll);
|
$("#w-unload").addEventListener("click", unloadAll);
|
||||||
$("#w-os-update").addEventListener("click", osUpdate);
|
$("#w-os-update").addEventListener("click", osUpdate);
|
||||||
@@ -85,6 +87,12 @@ async function unloadAll() {
|
|||||||
catch (e) { toast(e.message, true); }
|
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() {
|
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); }
|
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); }
|
catch (e) { toast(e.message, true); }
|
||||||
|
|||||||
Reference in New Issue
Block a user