feat(server): llama-swap transparent machen

Neue Karte oben im Server-Tab beantwortet "funktioniert es / wie steuern /
wie pruefen": Status (erreichbar/offline), aktuell geladenes Modell, Adresse
fuer die Tools, Anzahl bereiter Modelle + Klartext, wie man ansteuert. Knopf
"Verbindung pruefen" testet die Engine live (/api/integration/test).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-22 17:00:40 +02:00
parent 4077d06b2d
commit bc15e75e3e
+47 -2
View File
@@ -2,7 +2,7 @@
// Selbsterklärende Aktionszeilen + heikle Aktionen mit Klartext-Bestätigung. // Selbsterklärende Aktionszeilen + heikle Aktionen mit Klartext-Bestätigung.
import { api, getToken } from "../core/api.js"; import { api, getToken } from "../core/api.js";
import { $, toast, icon, confirmModal, promptModal } from "../core/ui.js"; import { $, esc, toast, icon, confirmModal, promptModal } from "../core/ui.js";
import { track } from "./jobs.js"; import { track } from "./jobs.js";
function refreshSoon() { document.dispatchEvent(new Event("mc:refresh")); } function refreshSoon() { document.dispatchEvent(new Event("mc:refresh")); }
@@ -21,6 +21,19 @@ function mount() {
<div class="sub">Dienste steuern, Updates einspielen und live mitlesen — ganz ohne SSH oder Terminal.</div> <div class="sub">Dienste steuern, Updates einspielen und live mitlesen — ganz ohne SSH oder Terminal.</div>
</div></div> </div></div>
<div class="card" id="w-swap-card">
<div class="card-h"><h3>LLM-Engine (llama-swap)</h3>
<span class="chip" id="w-swap-state" style="margin-left:auto">prüfe…</span></div>
<div class="card-sub">Das Herzstück: llama-swap lädt bei jeder Anfrage automatisch das passende Modell und entlädt die anderen — es läuft immer nur <b>eines</b> gleichzeitig.</div>
<div class="kv" style="margin-bottom:6px">
<div class="kv-row"><span class="kv-k">Adresse (für deine Tools)</span><span class="kv-v mono-sm" id="w-swap-url">—</span></div>
<div class="kv-row"><span class="kv-k">Aktuell geladen</span><span class="kv-v" id="w-swap-loaded">—</span></div>
<div class="kv-row"><span class="kv-k">Konfigurierte Modelle</span><span class="kv-v" id="w-swap-count">—</span></div>
</div>
<div class="hint" style="margin:8px 0 12px"><b>Wie ansteuern?</b> In deinem Tool (Zed/OpenCode) die obige Adresse als OpenAI-Basis-URL eintragen und einen <b>Modellnamen oder eine Rolle</b> angeben (siehe „Verbinden"). Eine einzige Anfrage genügt — das Modell wird automatisch geweckt.</div>
<div class="btn-row" style="display:flex;align-items:center;gap:10px"><button class="ghost" id="w-swap-check">Verbindung prüfen</button><span id="w-swap-check-res" class="mono-sm"></span></div>
</div>
<div class="card"> <div class="card">
<div class="card-h"><h3>Dienste &amp; Applikation</h3></div> <div class="card-h"><h3>Dienste &amp; Applikation</h3></div>
<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>
@@ -71,9 +84,41 @@ function mount() {
$("#w-reboot").addEventListener("click", rebootServer); $("#w-reboot").addEventListener("click", rebootServer);
$("#w-console-sel").addEventListener("change", connectConsole); $("#w-console-sel").addEventListener("change", connectConsole);
$("#w-update-cancel").addEventListener("click", cancelActive); $("#w-update-cancel").addEventListener("click", cancelActive);
$("#w-swap-check").addEventListener("click", checkSwap);
if (lastStatus) renderSwap(lastStatus);
connectConsole(); 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
? `<b>${esc(loaded.name)}</b> <span class="text-mut">(${esc(loaded.state)})</span>`
: `<span class="text-mut">nichts geladen — lädt bei der nächsten Anfrage</span>`;
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() { async function cancelActive() {
if (!activeJob) return; 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; 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;
@@ -193,4 +238,4 @@ async function rebootServer() {
catch (e) { toast(e.message, true); } catch (e) { toast(e.message, true); }
} }
export default { id: "server", mount, onJobs }; export default { id: "server", mount, onJobs, onStatus };