// server.js — Server & Wartung (v3): Dienste, OS-Updates, Reboot, Live-Konsole. // Selbsterklärende Aktionszeilen + heikle Aktionen mit Klartext-Bestätigung. import { api, getToken } from "../core/api.js"; import { $, esc, toast, icon, confirmModal, promptModal } from "../core/ui.js"; import { track } from "./jobs.js"; function refreshSoon() { document.dispatchEvent(new Event("mc:refresh")); } function row(id, ic, tone, title, desc) { return ``; } function mount() { $("#wartung").outerHTML = `

Server & Wartung

Dienste steuern, Updates einspielen und live mitlesen — ganz ohne SSH oder Terminal.

LLM-Engine (llama-swap)

prüfe…
Das Herzstück: llama-swap lädt bei jeder Anfrage automatisch das passende Modell und entlädt die anderen — es läuft immer nur eines gleichzeitig.
Adresse (für deine Tools)
Aktuell geladen
Konfigurierte Modelle
Wie ansteuern? In deinem Tool (Zed/OpenCode) die obige Adresse als OpenAI-Basis-URL eintragen und einen Modellnamen oder eine Rolle angeben (siehe „Verbinden"). Eine einzige Anfrage genügt — das Modell wird automatisch geweckt.

Dienste & Applikation

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-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.")}

Betriebssystem (Bosgame)

Tiefe Eingriffe — das Dashboard fragt einmalig nach deinem sudo-Passwort.
${row("w-os-update", "refresh", "amber", "OS-Updates installieren", "Führt apt update & upgrade aus. Kann ein paar Minuten dauern — Fortschritt in der Aktivität.")} ${row("w-reboot", "bolt", "red", "Server neustarten (Reboot)", "Bootet den ganzen Bosgame neu. Alles ist für ~1 Minute offline, auch dieses Dashboard.")}

Live-Konsole

Live mitlesen, was der Dienst gerade tut.
Verbinde…
`; $("#w-restart-swap").addEventListener("click", () => restartService("llama-swap", "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); $("#w-reboot").addEventListener("click", rebootServer); $("#w-console-sel").addEventListener("change", connectConsole); $("#w-update-cancel").addEventListener("click", cancelActive); $("#w-swap-check").addEventListener("click", checkSwap); if (lastStatus) renderSwap(lastStatus); connectConsole(); } // ---- llama-swap-Transparenz ---- let lastStatus = null; function renderSwap(s) { const chip = $("#w-swap-state"); if (!chip) return; const ok = !!s?.swap_ok; chip.textContent = ok ? "● erreichbar" : "● offline"; chip.style.cssText = "margin-left:auto;" + (ok ? "background:rgba(63,185,80,.12);border-color:rgba(63,185,80,.25);color:#7ee29a" : "background:rgba(248,81,73,.12);border-color:rgba(248,81,73,.3);color:#ff9b95"); $("#w-swap-url").textContent = (s?.swap_url || "—") + "/v1"; const models = (s?.models || []); const loaded = models.find(m => m.state === "running" || m.state === "loading"); $("#w-swap-loaded").innerHTML = loaded ? `${esc(loaded.name)} (${esc(loaded.state)})` : `nichts geladen — lädt bei der nächsten Anfrage`; const real = models.filter(m => !m.incomplete).length; $("#w-swap-count").textContent = real ? `${real} bereit` : "keine"; } async function checkSwap() { const res = $("#w-swap-check-res"); res.textContent = "prüfe…"; res.style.color = "var(--mut)"; try { const r = await api("/api/integration/test"); if (r.ok) { res.style.color = "#7ee29a"; res.textContent = `✓ Engine antwortet — ${r.models.length} Modell(e): ${r.models.join(", ")}`; } else { res.style.color = "#ff9b95"; res.textContent = "✗ keine Verbindung: " + r.error; } } catch (e) { res.style.color = "#ff9b95"; res.textContent = "✗ " + e.message; } } function onStatus(s) { lastStatus = s; renderSwap(s); } async function cancelActive() { if (!activeJob) return; if (!await confirmModal({ title: activeJob.label + " abbrechen?", body: "Der laufende Vorgang wird gestoppt. Bei einem Download bleibt eine unvollständige Datei zurück (kannst du später neu starten).", confirmLabel: "Abbrechen", danger: true })) return; try { await api(`/api/jobs/${activeJob.id}/cancel`, { method: "POST" }); toast("Abbruch angefordert…"); } catch (e) { toast(e.message, true); } } let ws = null; function connectConsole() { if (ws) { ws.close(); ws = null; } const svc = $("#w-console-sel").value, out = $("#w-console"); out.textContent = "Verbinde mit " + svc + "…\n"; const proto = location.protocol === "https:" ? "wss:" : "ws:"; ws = new WebSocket(`${proto}//${location.host}/api/logs/${svc}?token=${encodeURIComponent(getToken())}`); ws.onmessage = e => { out.textContent += e.data; out.scrollTop = out.scrollHeight; }; ws.onclose = () => { out.textContent += "\n— Verbindung getrennt —"; }; } async function restartService(name, title, body) { if (!await confirmModal({ title, body, confirmLabel: "Neustarten" })) return; try { await api(`/api/service/${name}/restart`, { method: "POST" }); toast("Neustart ausgelöst: " + name); setTimeout(refreshSoon, 2000); } catch (e) { toast(e.message, true); } } async function unloadAll() { if (!await confirmModal({ title: "Grafikspeicher leeren?", body: "Alle geladenen Modelle werden entladen. Sie laden beim nächsten Aufruf automatisch neu — es geht nichts verloren.", confirmLabel: "Leeren" })) return; try { await api("/api/unload", { method: "POST" }); toast("Grafikspeicher geleert."); setTimeout(refreshSoon, 600); } 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); } } // ---- Live-Verlauf laufender Vorgaenge (Updates) direkt auf der Server-Seite ---- let activeJob = null; // { id, label } const notified = new Set(); // Job-IDs, fuer die schon eine Abschluss-Meldung kam function watchJob(id, label) { activeJob = { id, label }; const card = $("#w-update-card"); if (!card) return; card.style.display = ""; $("#w-update-label").textContent = label + " läuft…"; $("#w-update-badge").innerHTML = 'läuft…'; $("#w-update-log").textContent = "Starte…"; card.scrollIntoView({ behavior: "smooth", block: "nearest" }); } function onJobs(jobs) { if (!activeJob) return; const j = (jobs || []).find(x => x.id === activeJob.id); const card = $("#w-update-card"); if (!j || !card) return; card.style.display = ""; const logEl = $("#w-update-log"); if (logEl) { logEl.textContent = (j.log || []).join("\n"); logEl.scrollTop = logEl.scrollHeight; } const done = j.state === "done", failed = j.state === "failed", canceled = j.state === "canceled"; const terminal = done || failed || canceled; const badge = $("#w-update-badge"); if (badge) badge.innerHTML = done ? '✓ fertig' : failed ? '✗ fehlgeschlagen' : canceled ? 'abgebrochen' : 'läuft…'; // Fortschrittsbalken: exakter Backend-Wert (Download) bevorzugt, sonst %-Angabe im Log. let pval = null; if (!terminal) { if (typeof j.progress === "number") { pval = j.progress; } else { const last = (j.log || [])[j.log.length - 1] || ""; const pm = last.match(/(\d+(?:\.\d+)?)\s*%/); if (pm) pval = parseFloat(pm[1]); } } const bar = $("#w-update-bar"); if (bar) { if (pval != null) { bar.style.display = ""; bar.querySelector("i").style.width = Math.min(100, pval) + "%"; } else bar.style.display = "none"; } $("#w-update-cancel").style.display = terminal ? "none" : ""; if (terminal) { $("#w-update-label").textContent = activeJob.label + (done ? " — erfolgreich abgeschlossen." : canceled ? " — abgebrochen." : " — fehlgeschlagen (siehe Protokoll)."); if (!notified.has(j.id)) { notified.add(j.id); toast(done ? "✓ " + activeJob.label + " abgeschlossen." : canceled ? activeJob.label + " abgebrochen." : "✗ " + activeJob.label + " fehlgeschlagen.", failed); if (done) setTimeout(refreshSoon, 800); } } } async function update() { if (!await confirmModal({ title: "LLM-Engine aktualisieren?", body: "Lädt die neueste llama.cpp-ROCm-Version und installiert sie. Die Engine ist während des Updates kurz nicht erreichbar." })) return; try { const r = await api("/api/update", { method: "POST" }); toast("LLM-Engine-Update läuft."); track(r.job_id); watchJob(r.job_id, "LLM-Engine-Update"); } catch (e) { toast(e.message, true); } } async function osUpdate() { if (!await confirmModal({ title: "OS-Updates installieren?", body: "Führt apt update & upgrade aus. Das kann ein paar Minuten dauern; der Fortschritt erscheint in der Aktivität." })) return; const pwd = await promptModal({ title: "sudo-Passwort", body: "Für die System-Updates wird einmalig dein sudo-Passwort gebraucht.", placeholder: "sudo-Passwort", password: true, confirmLabel: "Installieren" }); if (!pwd) return; try { const r = await api("/api/os-update", { method: "POST", body: JSON.stringify({ password: pwd }) }); toast("OS-Update gestartet."); track(r.job_id); watchJob(r.job_id, "OS-Update"); } catch (e) { toast(e.message, true); } } async function rebootServer() { if (!await confirmModal({ title: "Server wirklich neu starten?", body: "Der ganze Bosgame startet physisch neu. Alles ist für ~1 Minute offline — auch dieses Dashboard.", confirmLabel: "Reboot", danger: true })) return; const pwd = await promptModal({ title: "sudo-Passwort", body: "Für den Reboot wird einmalig dein sudo-Passwort gebraucht.", placeholder: "sudo-Passwort", password: true, confirmLabel: "Jetzt neustarten", danger: true }); if (!pwd) return; try { await api("/api/reboot", { method: "POST", body: JSON.stringify({ password: pwd }) }); toast("Reboot ausgelöst — bis gleich."); } catch (e) { toast(e.message, true); } } export default { id: "server", mount, onJobs, onStatus };