fix(diagnostic): Modell-Katalog als Tabelle (Beschreibung + VRAM-Spalten), Button-Layout, Flotte nachziehen

- Katalog jetzt echte Tabelle: Spalten Modell | Kann gut/Beschreibung | VRAM |
  Status | Ziel-Box | Laden. VRAM aus sizeGB (>=12 GB gelb markiert).
- "Von HuggingFace aktualisieren" sitzt jetzt direkt neben der Ueberschrift
  (statt space-between ans rechte Ende); Card breiter (720→1100px), Tabelle
  horizontal scrollbar.
- loadLlmCatalog() zieht die Worker-Flotte aktiv nach (requestWorkers) — falls
  ein worker_update-Push waehrend RVS-Flappens verpasst wurde, erscheint die
  Box beim Oeffnen trotzdem.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-19 03:48:38 +02:00
co-authored by Claude Opus 4.8
parent 45167b7bb5
commit 8633840f94
+24 -14
View File
@@ -650,18 +650,18 @@
<!-- Modell-Katalog: GGUF-Modelle herunterladen/aktivieren (Stage D) -->
<div class="settings-section">
<div style="display:flex;justify-content:space-between;align-items:center;">
<div style="display:flex;align-items:center;gap:14px;margin-bottom:8px;">
<h2 style="margin:0;">Modell-Katalog</h2>
<button class="btn secondary" onclick="refreshLlmCatalog()" style="padding:4px 10px;font-size:11px;">Von HuggingFace aktualisieren</button>
</div>
<div class="card" style="max-width:720px;">
<div class="card" style="max-width:1100px;">
<p style="color:#8888AA;font-size:12px;margin:0 0 8px;">
Gaengige lokale GGUF-Modelle. „Laden" schickt das Modell an die gewaehlte
LLM-Box — llama-swap zieht das GGUF beim ersten Mal (mehrere GB) und meldet
den Fortschritt. Danach ist es im Modell-Dropdown oben waehlbar.
<span style="color:#FFD60A;">Achte auf den VRAM der Box (Groesse je Modell).</span>
<span style="color:#FFD60A;">Achte auf den VRAM der Box (Spalte VRAM).</span>
</p>
<div id="llm-catalog-list" style="font-size:12px;color:#8888AA;">(lade Katalog…)</div>
<div id="llm-catalog-list" style="font-size:12px;color:#8888AA;overflow-x:auto;">(lade Katalog…)</div>
<div id="llm-catalog-status" style="font-size:11px;color:#6a6a88;margin-top:6px;min-height:14px;"></div>
</div>
</div>
@@ -7085,6 +7085,7 @@
// ── Modell-Katalog (Stage D) ────────────────────────────
let _llmCatalog = [];
async function loadLlmCatalog() {
if (typeof requestWorkers === 'function') requestWorkers(); // Flotte aktiv nachziehen
try {
const r = await fetch('/api/llm-catalog');
const j = await r.json();
@@ -7111,21 +7112,30 @@
if (!box) return;
if (!_llmCatalog.length) { box.innerHTML = '<span style="color:#6a6a88;">Katalog leer.</span>'; return; }
const boxes = onlineLlmBoxes();
box.innerHTML = _llmCatalog.map((m, i) => {
const th = 'style="padding:6px 8px;text-align:left;position:sticky;top:0;background:#12122A;"';
const td = 'style="padding:6px 8px;vertical-align:top;"';
const rows = _llmCatalog.map((m, i) => {
const have = boxesServing(m.id);
const haveTxt = have.length ? `<span style="color:#3FFF3F;"> auf ${have.map(b => escapeHtml(b.node)).join(', ')}</span>` : '<span style="color:#6a6a88;">nicht geladen</span>';
const sizeTxt = m.sizeGB ? ` · ~${m.sizeGB} GB` : '';
const haveTxt = have.length ? `<span style="color:#3FFF3F;">✓ ${have.map(b => escapeHtml(b.node)).join(', ')}</span>` : '<span style="color:#6a6a88;">nicht geladen</span>';
const vram = m.sizeGB ? '~' + m.sizeGB + ' GB' : '';
const vramStyle = m.sizeGB >= 12 ? 'color:#FFB020;' : 'color:#AAB;';
const opts = boxes.length
? boxes.map(b => `<option value="${escapeHtml(b.instanceId)}">${escapeHtml(b.node)}</option>`).join('')
: '<option value="">(keine Box online)</option>';
return '<div style="display:flex;align-items:center;gap:8px;padding:5px 0;border-bottom:1px solid #1E1E2E;flex-wrap:wrap;">' +
'<span style="min-width:150px;"><b>' + escapeHtml(m.id) + '</b>' + sizeTxt + '</span>' +
'<span style="color:#8888AA;flex:1;min-width:160px;">' + escapeHtml(m.description || m.hfRepo || '') + '</span>' +
haveTxt +
'<select id="llm-cat-box-' + i + '" style="background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:3px 6px;color:#E0E0F0;font-size:11px;">' + opts + '</select>' +
'<button class="btn secondary" ' + (boxes.length ? '' : 'disabled') + ' onclick="provisionModel(' + i + ')" style="padding:3px 10px;font-size:11px;">Laden</button>' +
'</div>';
return '<tr style="border-bottom:1px solid #1E1E2E;">' +
'<td ' + td + 'white-space:nowrap;"><b>' + escapeHtml(m.id) + '</b></td>' +
'<td ' + td + 'color:#9a9ab5;min-width:220px;">' + escapeHtml(m.description || m.hfRepo || '') + '</td>' +
'<td ' + td + vramStyle + 'white-space:nowrap;">' + vram + '</td>' +
'<td ' + td + 'white-space:nowrap;">' + haveTxt + '</td>' +
'<td ' + td + '"><select id="llm-cat-box-' + i + '" style="background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:3px 6px;color:#E0E0F0;font-size:11px;">' + opts + '</select></td>' +
'<td ' + td + '"><button class="btn secondary" ' + (boxes.length ? '' : 'disabled') + ' onclick="provisionModel(' + i + ')" style="padding:3px 10px;font-size:11px;">Laden</button></td>' +
'</tr>';
}).join('');
box.innerHTML = '<table style="width:100%;border-collapse:collapse;font-size:12px;min-width:760px;">' +
'<thead><tr style="color:#8888AA;border-bottom:1px solid #2A2A3E;">' +
'<th ' + th + '>Modell</th><th ' + th + '>Kann gut / Beschreibung</th><th ' + th + '>VRAM</th>' +
'<th ' + th + '>Status</th><th ' + th + '>Ziel-Box</th><th ' + th + '></th>' +
'</tr></thead><tbody>' + rows + '</tbody></table>';
}
function provisionModel(i) {
const m = _llmCatalog[i];