fix(diagnostic): Karten kommen im Browser an, lokales Modell springt nicht zurueck

1. "Die LLM-Box meldet noch keine Karten": die Box schickte die Karten-
   Belegung, aber workerList() im Diagnostic-Server gibt nur eine feste
   Feldliste an den Browser — slots stand nicht drauf. Meine UI-Tests haben
   die Flotte direkt im Browser gesetzt und sind an genau dieser Stelle
   vorbeigelaufen. Jetzt Ende-zu-Ende getestet: echter Diagnostic-Server +
   RVS + Box-Meldung → Browser (per DevTools-Protokoll gelesen).

2. Lokales Modell sprang auf "Qwen3 8B" zurueck: die Auswahl wurde gespeichert,
   aber die Seite behielt intern das alte Modell und stellte das Dropdown beim
   naechsten Lebenszeichen der Box (10 s) darauf zurueck. Dazu wie gewuenscht
   umgebaut: nur Modelle, die im Cluster geladen sind, jedes einmal, ohne
   Karte. Ist das gewaehlte gerade nirgends geladen: "(offline)" — geht
   automatisch weiter, sobald eine Box es hat. Nichts da: "kein Modell
   verfuegbar". Waehrend die Auswahl offen ist, wird nicht umgebaut.

3. "Bereit", aber Karte leer: der llm-adapter meldet pro Karte den belegten
   VRAM. Laeuft ein Modell "ready" und die Karte bleibt unter 400 MB, rechnet
   llama.cpp auf der CPU (Treiber zu alt fuer das CUDA im Image) — das sagt
   llama.cpp nicht nach aussen. Diagnostic zeigt es jetzt in Flotte,
   LLM-Karten und Modell-Auswahl ("laeuft auf der CPU").

4. bootstrap.sh: der normale Lauf meldete "✓ Treiber aktiv", auch wenn er fuer
   das LLM zu alt ist. Jetzt Warnung mit Version und dem Befehl
   (--upgrade-driver), wenn das LLM-Profil aktiv ist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 02:11:02 +02:00
co-authored by Claude Opus 5.5
parent 901cd1c16b
commit 9be14eef5e
4 changed files with 62 additions and 18 deletions
+22 -16
View File
@@ -8886,24 +8886,24 @@
// Baut das Modell-Dropdown aus der Vereinigung von live-Flotte + kuratierter
// Liste; zeigt pro Modell "· N Box(en)" bzw. "· offline". Auswahl bleibt
// erhalten (auch wenn das gewaehlte Modell gerade keine online Box hat).
// ARIAs lokales Modell: nur was im Cluster GELADEN ist, jedes einmal, ohne
// Karte — wo es laeuft, entscheidet der Cluster. Ist das gewaehlte gerade
// nirgends geladen, bleibt es mit "(offline)" stehen; sobald eine Box es
// wieder hat, geht es von selbst weiter (bis dahin antwortet Claude).
function refreshLocalLlmModelChoices() {
const sel = document.getElementById('local-llm-model');
if (!sel) return;
if (document.activeElement === sel) return; // nicht unter der offenen Auswahl umbauen
const counts = fleetLlmModelCounts();
const nameOf = id => { const m = _localModelsCache.find(x => x.id === id); return (m && m.display_name) || id; };
const ids = new Set();
Object.keys(counts).forEach(id => ids.add(id));
_localModelsCache.forEach(m => ids.add(m.id));
if (_currentLocalModel) ids.add(_currentLocalModel);
const order = Array.from(ids).sort();
sel.innerHTML = order.map(id => {
const n = counts[id] || 0;
// Nur geladene Modelle zaehlen (Karten-Belegung) — alles andere kann
// ARIA nicht nutzen; laden geht im Modell-Katalog.
const avail = n > 0 ? ` · geladen auf ${n} Box${n > 1 ? 'en' : ''}` : ' · nicht geladen';
return `<option value="${id}">${escapeHtml(nameOf(id))}${avail}</option>`;
}).join('') || '<option value="">(keine)</option>';
if (order.includes(_currentLocalModel)) sel.value = _currentLocalModel;
const cpu = id => workers.some(w => w.online && (w.slots || []).some(sl => sl.model === id && sl.onCpu));
const ids = Object.keys(counts).sort();
const opts = ids.map(id => `<option value="${escapeHtml(id)}">${escapeHtml(nameOf(id))}${cpu(id) ? ' · läuft auf der CPU' : ''}</option>`);
if (_currentLocalModel && !ids.includes(_currentLocalModel)) {
opts.unshift(`<option value="${escapeHtml(_currentLocalModel)}">${escapeHtml(nameOf(_currentLocalModel))} (offline)</option>`);
}
sel.innerHTML = opts.join('') || '<option value="">kein Modell verfügbar</option>';
sel.value = _currentLocalModel && (ids.includes(_currentLocalModel) || opts.length) ? _currentLocalModel : (ids[0] || '');
updateLocalModelDesc();
renderLocalLlmFleet();
}
@@ -8952,6 +8952,9 @@
toolVariant: document.getElementById('local-llm-toolvariant').value,
localLlmModel: (modelSel && modelSel.value) || '',
};
// Sofort merken — sonst stellt das naechste Flotten-Update (alle 10 s) das
// Dropdown auf das alte Modell zurueck, obwohl das neue gespeichert ist.
if (body.localLlmModel) _currentLocalModel = body.localLlmModel;
updateLocalModelDesc();
try {
const r = await fetch('/api/local-llm-config', {
@@ -9167,13 +9170,15 @@
+ (tooBig ? ' · passt nicht' : '');
opts.push(`<option value="${escapeHtml(m.id)}"${m.id === sl.model ? ' selected' : ''}${tooBig ? ' style="color:#FF9F0A;"' : ''}>${escapeHtml(label)}</option>`);
}
const [c, st] = sl.model ? (SLOT_STATE[sl.state] || ['#8888AA', escapeHtml(sl.state || '?')]) : ['#555570', 'leer'];
let [c, st] = sl.model ? (SLOT_STATE[sl.state] || ['#8888AA', escapeHtml(sl.state || '?')]) : ['#555570', 'leer'];
if (sl.onCpu) { c = '#FF9F0A'; st = '⚠ auf der CPU'; }
const job = Object.values(_llmJobs || {}).find(j => j.instanceId === b.instanceId && String(j.gpu) === String(sl.gpu)
&& !['ready', 'error'].includes(j.phase));
const jobTxt = job ? (job.phase === 'download' && job.percent != null ? `lädt ${job.percent.toFixed(0)} %` : 'lädt …') : '';
rows.push(`<div style="display:grid;grid-template-columns:minmax(220px,1fr) minmax(260px,2fr) 160px;gap:10px;align-items:center;">
<div><b style="color:#E0E0F0;">${escapeHtml(b.node)}</b> · Karte ${escapeHtml(sl.gpu)}
<div style="color:#555570;font-size:11px;">${escapeHtml(sl.gpuName || '')}${vramGB ? ' · ' + vramGB.toFixed(0) + ' GB' : ''}${sl.ctx ? ' · Kontext ' + sl.ctx : ''}</div></div>
<div style="color:#555570;font-size:11px;">${escapeHtml(sl.gpuName || '')}${vramGB ? ' · ' + vramGB.toFixed(0) + ' GB' : ''}${sl.vramUsedMB >= 0 ? ' · belegt ' + (sl.vramUsedMB / 1024).toFixed(1) + ' GB' : ''}${sl.ctx ? ' · Kontext ' + sl.ctx : ''}</div>
${sl.onCpu ? '<div style="color:#FF9F0A;font-size:11px;" title="llama.cpp findet die Karte nicht und rechnet langsam auf der CPU. Ursache meist: Treiber zu alt fuer das CUDA im llama-swap-Image. Abhilfe: auf der Box sudo ./bootstrap.sh --upgrade-driver">⚠ Modell läuft auf der CPU, Karte leer — Treiber zu alt? (bootstrap.sh --upgrade-driver)</div>' : ''}</div>
<select data-box="${escapeHtml(b.instanceId)}" data-gpu="${escapeHtml(sl.gpu)}" data-current="${escapeHtml(sl.model || '')}"
onchange="onLlmCardSelect(this)" ${job ? 'disabled' : ''}
style="background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:5px 8px;color:#E0E0F0;font-size:12px;">${opts.join('')}</select>
@@ -9220,7 +9225,8 @@
<span style="min-width:200px;color:#8888AA;">${card}</span>
<span style="color:#555570;">— leer · im Modell-Katalog „Laden" → diese Karte</span></div>`;
}
const [c, label] = SLOT_STATE[sl.state] || ['#8888AA', escapeHtml(sl.state || '?')];
let [c, label] = SLOT_STATE[sl.state] || ['#8888AA', escapeHtml(sl.state || '?')];
if (sl.onCpu) { c = '#FF9F0A'; label = '⚠ läuft auf der CPU — Karte leer (Treiber zu alt?)'; }
return `<div style="display:flex;gap:10px;align-items:center;padding:2px 0;">
<span style="min-width:200px;color:#8888AA;">${card}</span>
<b style="color:#E0E0F0;">${escapeHtml(sl.model)}</b>
+4
View File
@@ -762,6 +762,10 @@ function workerList() {
gpus: w.gpus, model: w.model, models: w.models || [], busy: !!w.busy,
openaiPort: w.openaiPort || "", openaiBind: w.openaiBind || "",
openaiKeySet: !!w.openaiKeySet, openaiEnvLocked: !!w.openaiEnvLocked,
// LLM-Karten (Belegung, Zustand, VRAM). Fehlte hier — die Box schickte sie,
// aber diese Feldliste liess sie nie zum Browser durch ("meldet keine Karten").
slots: Array.isArray(w.slots) ? w.slots : null,
knownModels: w.knownModels || [],
online: (now - (w.last_seen || 0)) < WORKER_OFFLINE_MS,
}));
}