feat(llm): llama.cpp/llama-swap aus der Diagnostic aktualisieren

Modell-Katalog → ⚙ llama.cpp zeigt je LLM-Box die installierte Version,
sucht nach Updates (Pruefsumme des Image-Tags in der Registry + letztes
llama-swap-Release) und aktualisiert auf Knopfdruck: altes Image sichern,
neues ziehen, Container per Compose neu anlegen, Selbsttest (llama.cpp
sieht die Karten, vorgeladene Modelle wieder bereit) — sonst automatisch
zurueck. Zuruecksetzen auf die Vorversion per Knopf.

Docker-Zugriff hat nur der neue llm-updater (feste Aktionen fuer
aria-llama-swap, kein Port nach aussen, Token in /models). Der llm-adapter
reicht llm_runtime weiter und meldet den Fortschritt. RVS-Allow-List um
llm_runtime/_result/_progress ergaenzt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-01 12:39:52 +01:00
co-authored by Claude Opus 5.5
parent 3773178726
commit 68686de9dc
7 changed files with 671 additions and 1 deletions
+149
View File
@@ -952,6 +952,7 @@
<button class="btn secondary" onclick="refreshLlmCatalog()" style="padding:4px 10px;font-size:11px;">Von HuggingFace aktualisieren</button>
<button class="btn secondary" id="llm-dl-btn" onclick="openLlmDownloads()" title="Fortschritt der Modell-Downloads auf die LLM-Boxen" style="padding:4px 10px;font-size:11px;">⬇ Downloads <span id="llm-dl-badge" style="display:none;margin-left:4px;padding:0 6px;border-radius:8px;background:#FFD60A;color:#000;font-weight:700;"></span></button>
<button class="btn secondary" onclick="openModelBoards()" title="Ranglisten der besten Modelle + was gerade auf HuggingFace im Trend ist (direkt ladbar)" style="padding:4px 10px;font-size:11px;">🏆 Bestenlisten</button>
<button class="btn secondary" onclick="openLlmRuntime()" title="Installierte llama.cpp-/llama-swap-Version der LLM-Boxen, nach Updates suchen, aktualisieren" style="padding:4px 10px;font-size:11px;">⚙ llama.cpp</button>
</div>
<div class="card" style="max-width:1100px;">
<p style="color:#8888AA;font-size:12px;margin:0 0 8px;">
@@ -2691,6 +2692,8 @@
if (msg.type === 'rooms_info') { rooms = (msg.payload && msg.payload.rooms) || []; renderRooms(); return; }
if (msg.type === 'sat_result') { onSatResult(msg.payload || {}); return; }
if (msg.type === 'llm_openai_config_result') { onOpenAiConfigResult(msg.payload || {}); return; }
if (msg.type === 'llm_runtime_result') { onLlmRuntimeResult(msg.payload || {}); return; }
if (msg.type === 'llm_runtime_progress') { onLlmRuntimeProgress(msg.payload || {}); return; }
if (msg.type === 'agent_history_list') {
if (agentHistoryHost && msg.payload && msg.payload.host === agentHistoryHost) {
renderAgentHistory(msg.payload.events || []);
@@ -9972,6 +9975,152 @@
function closeLlmDownloads() {
document.getElementById('llm-dl-modal').classList.remove('open');
}
// ── llama.cpp/llama-swap der LLM-Boxen (Modell-Katalog → ⚙ llama.cpp) ──
// Pruefen/Aktualisieren/Zuruecksetzen macht auf der Box der llm-updater
// (einziger Dienst mit Docker-Zugriff). Ob es etwas Neues gibt, entscheidet
// die Pruefsumme des Image-Tags in der Registry, nicht die Versionsnummer.
const _llmRt = {}; // instanceId -> {status, checked, busy, log, result, error}
function openLlmRuntime() {
let bg = document.getElementById('llmrt-modal');
if (!bg) {
bg = document.createElement('div');
bg.id = 'llmrt-modal';
bg.style.cssText = 'position:fixed;inset:0;z-index:10000;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;padding:16px;';
bg.innerHTML = '<div style="background:#12121E;border:1px solid #444466;border-radius:8px;padding:16px 18px;max-width:640px;width:100%;max-height:85vh;overflow-y:auto;color:#E0E0F0;font-size:13px;line-height:1.5;">' +
'<div style="display:flex;align-items:center;margin-bottom:10px;"><b style="font-size:15px;">⚙ llama.cpp auf den LLM-Boxen</b>' +
'<button class="btn secondary" onclick="closeLlmRuntime()" style="margin-left:auto;padding:2px 10px;font-size:11px;">Schließen</button></div>' +
'<div id="llmrt-body"></div></div>';
bg.addEventListener('click', e => { if (e.target === bg) closeLlmRuntime(); });
document.body.appendChild(bg);
}
bg.style.display = 'flex';
for (const b of onlineLlmBoxes()) {
const st = _llmRt[b.instanceId] || (_llmRt[b.instanceId] = { log: [] });
if (!st.busy) llmRtSend(b.instanceId, 'status');
}
renderLlmRuntime();
}
function closeLlmRuntime() {
const bg = document.getElementById('llmrt-modal');
if (bg) bg.style.display = 'none';
}
function llmRtSend(instanceId, op) {
send({ action: 'llm_runtime', instanceId, op, requestId: 'rt_' + Date.now() });
}
function llmRtCheck(id) {
const st = _llmRt[id] || (_llmRt[id] = { log: [] });
st.checking = true; st.error = '';
llmRtSend(id, 'check');
renderLlmRuntime();
}
async function llmRtUpdate(id) {
const st = _llmRt[id] || {};
const rel = (st.status && st.status.release) || {};
if (!await uiConfirm('llama.cpp jetzt aktualisieren?\n\n' +
'Die Box zieht das neue llama-swap-Image' + (rel.tag ? ' (' + rel.tag + ')' : '') + ' (mehrere GB) und legt den Container neu an. ' +
'Die geladenen Modelle fliegen dabei kurz von den Karten und laden danach neu — laufende Anfragen (z.B. die Schmiede) brechen ab.\n\n' +
'Vorher wird die alte Version gesichert. Sieht llama.cpp danach keine Grafikkarte oder lädt das Modell nicht, wird automatisch zurückgesetzt.')) return;
Object.assign(st, { busy: 'update', log: [], result: null, error: '' });
_llmRt[id] = st;
llmRtSend(id, 'update');
renderLlmRuntime();
}
async function llmRtRollback(id) {
const st = _llmRt[id] || {};
const bk = ((st.status && st.status.backups) || [])[0];
if (!bk) return;
if (!await uiConfirm('Auf die gesicherte Version zurücksetzen?\n\n' +
'Setzt ' + bk.tag + ' wieder ein und legt den Container neu an. Die Modelle laden danach neu, laufende Anfragen brechen ab.')) return;
Object.assign(st, { busy: 'rollback', log: [], result: null, error: '' });
llmRtSend(id, 'rollback');
renderLlmRuntime();
}
function onLlmRuntimeProgress(p) {
const st = _llmRt[p.instanceId] || (_llmRt[p.instanceId] = { log: [] });
st.busy = st.busy || p.op;
st.log = (st.log || []).concat(p.lines || []).slice(-40);
renderLlmRuntime();
}
function onLlmRuntimeResult(p) {
const st = _llmRt[p.instanceId] || (_llmRt[p.instanceId] = { log: [] });
if (p.op === 'status' || p.op === 'check') {
st.checking = false;
if (p.ok === false) { st.error = p.error || 'fehlgeschlagen'; }
else {
st.status = Object.assign({}, st.status || {}, p);
if (p.op === 'check') st.checked = true;
st.error = '';
if (p.job && p.job.state === 'running' && !st.busy) st.busy = p.job.op;
}
} else {
st.busy = '';
st.result = p;
llmRtSend(p.instanceId, 'check'); // neuen Stand anzeigen
}
renderLlmRuntime();
}
function _rtDate(s) {
if (!s) return '';
const d = new Date(s);
return isNaN(d) ? String(s) : d.toLocaleDateString('de-DE') + ' ' + d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
}
function renderLlmRuntime() {
const body = document.getElementById('llmrt-body');
if (!body) return;
const boxes = onlineLlmBoxes();
if (!boxes.length) { body.innerHTML = '<span style="color:#6a6a88;">Keine LLM-Box online.</span>'; return; }
body.innerHTML = boxes.map(b => {
const id = b.instanceId, st = _llmRt[id] || {}, s = st.status || {}, inst = s.installed || {};
const q = "'" + escapeHtml(id) + "'";
let h = '<div style="background:#0D0D1A;border-radius:6px;padding:10px 12px;margin-bottom:10px;">' +
'<div style="font-weight:600;color:#AAB;margin-bottom:6px;">🖥️ ' + escapeHtml(b.node || id) + '</div>';
if (st.error) h += '<div style="color:#FF6E6E;margin-bottom:6px;">✗ ' + escapeHtml(st.error) + '</div>';
if (!s.image && !st.error) h += '<div style="color:#8888AA;">frage Version ab …</div>';
if (s.image) {
h += '<div style="font-size:12px;"><b>Installiert:</b> llama-swap ' + escapeHtml(inst.llamaSwap || '?') +
' · llama.cpp ' + escapeHtml(inst.llamaCpp || '?') + '</div>' +
'<div style="font-size:11px;color:#8888AA;">Image gebaut ' + escapeHtml(_rtDate(inst.created)) +
' · <code>' + escapeHtml(s.image) + '</code></div>';
if (st.checked) {
const rel = s.release || {};
const relTxt = rel.tag ? 'llama-swap <b>' + escapeHtml(rel.tag) + '</b> vom ' + escapeHtml(_rtDate(rel.published)) +
(rel.url ? ' · <a href="' + escapeHtml(rel.url) + '" target="_blank" style="color:#0A84FF;">Änderungen</a>' : '') : '';
if (s.latestError) h += '<div style="color:#FF9500;margin-top:6px;">⚠ ' + escapeHtml(s.latestError) + '</div>';
else if (s.updateAvailable) h += '<div style="color:#FFD60A;margin-top:6px;">⬆ Update verfügbar' + (relTxt ? ': ' + relTxt : '') + '</div>';
else h += '<div style="color:#34C759;margin-top:6px;">✓ Aktuell' + (relTxt ? ' (neuestes Release: ' + relTxt + ')' : '') + '</div>';
}
const bk = (s.backups || [])[0];
if (bk) h += '<div style="font-size:11px;color:#8888AA;margin-top:4px;">Gesicherte Vorversion: <code>' + escapeHtml(bk.tag) + '</code></div>';
if (!st.busy) {
h += '<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px;">' +
'<button class="btn secondary" onclick="llmRtCheck(' + q + ')" style="padding:3px 10px;font-size:11px;"' + (st.checking ? ' disabled' : '') + '>' +
(st.checking ? 'suche …' : 'Nach Updates suchen') + '</button>' +
(st.checked && s.updateAvailable ? '<button class="btn" onclick="llmRtUpdate(' + q + ')" style="padding:3px 10px;font-size:11px;">Jetzt aktualisieren</button>' : '') +
(bk ? '<button class="btn secondary" onclick="llmRtRollback(' + q + ')" style="padding:3px 10px;font-size:11px;">Zurück auf Vorversion</button>' : '') +
'</div>';
}
}
if (st.busy) h += '<div style="color:#FFD60A;margin-top:8px;">⏳ ' + (st.busy === 'update' ? 'Aktualisiere' : 'Setze zurück') +
' … das dauert ein paar Minuten (Download + Modell neu laden).</div>';
if ((st.log || []).length && (st.busy || st.result)) {
h += '<pre style="background:#08080F;color:#9a9ab5;font-size:10.5px;padding:6px 8px;border-radius:4px;max-height:160px;overflow-y:auto;white-space:pre-wrap;margin:6px 0 0;">' +
escapeHtml(st.log.join('\n')) + '</pre>';
}
const r = st.result;
if (r && !st.busy) {
if (r.ok && r.changed === false) h += '<div style="color:#34C759;margin-top:6px;">✓ ' + escapeHtml(r.message || 'nichts geändert') + '</div>';
else if (r.ok) h += '<div style="color:#34C759;margin-top:6px;">✓ Fertig. ' + escapeHtml(r.selftest || '') +
(r.before && r.after ? '<br>llama-swap ' + escapeHtml(r.before.llamaSwap || '?') + ' → ' + escapeHtml(r.after.llamaSwap || '?') +
' · llama.cpp ' + escapeHtml(r.before.llamaCpp || '?') + ' → ' + escapeHtml(r.after.llamaCpp || '?') : '') + '</div>';
else h += '<div style="color:#FF6E6E;margin-top:6px;">✗ ' + escapeHtml(r.error || 'fehlgeschlagen') +
(r.rolledBack ? '<br><span style="color:#FF9500;">Automatisch zurückgesetzt: ' + escapeHtml(r.rollbackSelftest || '') + '</span>' : '') + '</div>';
}
return h + '</div>';
}).join('') +
'<div style="font-size:11px;color:#6a6a88;">Ob es etwas Neues gibt, zeigt die Prüfsumme des Images in der Registry — der Tag <code>unified-cuda</code> wandert mit. ' +
'Das Image enthält llama-swap und llama.cpp zusammen.</div>';
}
// "Box antwortet nicht" braucht ein Ticken, auch wenn keine Meldung kommt.
setInterval(() => {
if (Object.values(_llmJobs).some(j => j.phase === 'queued')) renderLlmDownloads();