feat(llm): Download-Fortschritt beim Modell-Laden — und "Laden" kommt endlich an
Eigentlicher Fehler zuerst: llm_provision_model/_result und llm_remove_model
standen nie in der RVS-Allow-List. Der RVS verwirft unbekannte Typen mit einem
stillen return — jeder Klick auf "Laden" im Modell-Katalog ist seit Stage D
(19.09.) an dieser Liste gestorben, die Box hat nie davon erfahren. Laden
liessen sich nur die Modelle aus der Basis-Config der Box.
Gefunden, weil grep rvs/server.js fuer eine Binaerdatei hielt und still
uebersprang: in Zeile 164 stand seit a7998bcd ein ECHTES Nullbyte im
Template-String statt eines Escapes. Node stoert das nicht, aber git zeigte
fuer jede Aenderung am RVS nur noch "Binary files differ". Jetzt \u0000 —
zur Laufzeit derselbe String.
Fortschritt: llama.cpp zieht das GGUF per -hf selbst und meldet nichts. Der
llm-adapter fragt vorab bei Hugging Face, welche Dateien (Groesse, sha256)
kommen, und misst genau diese im HF-Cache-Layout, das llama.cpp verwendet:
/models/models--org--repo/blobs/<sha256>.downloadInProgress (waechst)
/models/models--org--repo/blobs/<sha256> (fertig)
Layout gegen den echten llama.cpp-Server ausprobiert, nicht angenommen. Geht
auch bei gesplitteten Modellen und zwei Downloads gleichzeitig; Fallback fuer
aeltere Layouts ueber alle *.downloadInProgress. Phasen: queued → start →
download (%, MB/s, Restzeit) → load ("laedt in die Karte") → ready ("in Karte
N geladen", Name + VRAM aus nvidia-smi) bzw. error.
Mitgefixt: der Warmup wartete LLM_TIMEOUT_SEC (Default 60 s). Ein Modell mit
mehreren GB meldete deshalb "Fehler", waehrend llama-swap weiterlud. Jetzt
LLM_WARMUP_TIMEOUT_S (1900 s, ueber llama-swaps healthCheckTimeout).
Diagnostic: Knopf "⬇ Downloads" neben "Von HuggingFace aktualisieren" mit
Zaehler fuer laufende; "Laden" oeffnet das Fenster direkt. Der Server merkt
sich den Stand pro (Box, Modell), ein spaeter geoeffneter Tab sieht laufende
Downloads. Antwortet die Box nicht, steht nach 30 s eine Warnung da statt
nichts.
Compose (KI-Box): llm-adapter bekommt LLM_GPU (Karte fuer die Meldung) und
HF_TOKEN (Dateiliste gesperrter Repos).
Test: Fortschritt gegen einen echten llama.cpp-Download (Qwen2.5-0.5B Q8,
676 MB): Prozent steigend 0 → 91.6, dann load; zweiter Lauf aus dem Cache
direkt load mit cached=true.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -544,6 +544,7 @@ Erreichbar unter `http://<VM-IP>:3001`. Teilt das Netzwerk mit der Bridge.
|
||||
- `GET /api/chat-backup?lines=N` — letzte N Zeilen aus `chat_backup.jsonl` (Default 200, max 5000) als geparstes JSON. Hilfreich um nachzuvollziehen was ARIA tatsaechlich gemacht hat.
|
||||
- `GET /api/agent-stream?lines=N` — gleiche Mechanik fuer den persistierten Live-Stream (Tool-Calls + Inputs + Outputs).
|
||||
- **OAuth-Callback-Pipeline**: Caddy davor terminiert TLS via Let's Encrypt, RVS hat einen HTTP-Listener auf demselben Port wie der WebSocket. Provider (Spotify/Dropbox/Discord/...) redirecten den User an `https://{RVS_HOST}/oauth/callback/{service}` → RVS broadcastet als `oauth_callback`-WS-Message → aria-bridge forwarded an Brain → Brain matched `state`, tauscht `code` gegen Token, persistiert in `/shared/config/oauth_tokens.json`. Token-Refresh laeuft automatisch. ARIA hat fuenf Brain-Tools: **`oauth_register_provider`** (legt URLs eines neuen Providers wie Dropbox/Discord/Notion/... on-demand in `oauth_apps.json` an — Credentials bleiben Stefans Job), `oauth_authorize`, `oauth_get_token`, **`oauth_call`**, `oauth_revoke`
|
||||
- **Lokale Modelle laden mit Fortschritt** (Modell-Katalog → „Laden", Knopf **⬇ Downloads**): echte Prozentanzeige mit Tempo und Restzeit, danach „laedt in die Karte" und am Ende „✓ in Karte N (Name) geladen" samt belegtem VRAM. llama.cpp zieht das GGUF per `-hf` selbst und meldet nichts; der llm-adapter fragt deshalb vorab bei Hugging Face, welche Dateien mit welcher Groesse und sha256 kommen, und misst genau diese im HF-Cache-Layout (`/models/models--org--repo/blobs/<sha256>.downloadInProgress`, nach Abschluss ohne Endung) — klappt auch bei gesplitteten Modellen und parallelen Downloads; liegt es schon im Cache, geht es direkt ans Laden. Der Warmup wartet jetzt bis zu `LLM_WARMUP_TIMEOUT_S` (1900 s) statt 60 s — vorher meldete ein mehrere GB grosses Modell „Fehler", waehrend llama-swap weiterlud. Ein Auftrag, auf den die Box nicht antwortet, erscheint nach 30 s als Warnung statt still zu verschwinden. Pro LLM-Box laeuft **ein** Modell auf ihrer LLM-Karte (`LLM_GPU`), ein anderes zu laden wechselt dorthin. *Wichtig:* `llm_provision_*` fehlte bis hierhin in der RVS-Allow-List — „Laden" hat seit Stage D nie eine Box erreicht
|
||||
- **Sprachmodell**: die Auswahlliste kommt aus **Anthropics eigenem Modell-Katalog** (`downloads.claude.ai/model-catalog/v1/catalog.json`, Surface `cc`) — dieselbe Quelle, aus der sich Claude Code bedient. Oeffentlich, ohne API-Key. Oben die **Tier-Aliase** (opus/sonnet/haiku/fable), die automatisch der neuesten Version folgen; darunter die **exakten Versionen**, aeltere als solche markiert. „↻ Aktualisieren" holt frisch, sonst 6h-Cache mit Datei-Fallback fuer offline. **🔬 Testen** schickt eine echte Mini-Anfrage durch den ganzen Weg und zeigt, welches Modell tatsaechlich geantwortet hat (aus `modelUsage.canonicalModel` der CLI — das Modell selbst zu fragen waere unzuverlaessig). Vier Ergebnisse: Tier aufgeloest (gruen), exakte Version (gruen), **nicht erkannt** (gelb — der Proxy haette still `opus` eingesetzt), ungueltig (rot, mit CLI-Begruendung)
|
||||
- **ARIA E-Mail** (Einstellungen → Integrationen): ARIA liest und sendet Mails ueber IMAP/SMTP (je SSL/TLS, STARTTLS oder unverschluesselt, Zertifikatspruefung abschaltbar fuer eigene Server) — Text, HTML, **Anhaenge** und **eingebettete Bilder** (`<img src="cid:…">`), Antworten mit `In-Reply-To`, Ordner anlegen (auch Unterordner) und Mails verschieben. Tools: `email_status`, `email_send`, `email_list`, `email_read`, `email_folders`, `email_folder_create`, `email_move`. Code in [`aria-brain/aria_mail.py`](aria-brain/aria_mail.py). **Whitelist** ist eine harte Grenze im Sendepfad, kein Prompt-Hinweis: alles, was nicht drinsteht, ist gesperrt, leer = ARIA sendet nirgendwohin; geprueft wird jeder Empfaenger (An/Cc/Bcc), ein fremder blockiert die ganze Mail. Eine Schreibweise, damit kein Eintrag mehrdeutig ist: `stefan@hacker-net.de` (genau die), `*@hacker-net.de` (die Domain, NICHT `evilhacker-net.de`, keine Subdomains), `*@*.hacker-net.de` (Subdomains), `*@*` (alles); Kurzformen wie `*hacker-net.de` werden beim Speichern mit Korrekturvorschlag abgelehnt. **Zugangsdaten sieht ARIA nie**: Konfiguration und Passwort liegen im Brain unter `/data` (in ARIAs Container nicht eingehaengt — `/shared` dagegen schon, darum NICHT dort), keine Schnittstelle gibt das Passwort zurueck (nur „gesetzt"), der Brain-Export laesst `email_secret.json` und `email_admin.key` aus. Konfiguration lesen/aendern nur mit Admin-Schluessel (`X-Aria-Admin`, liegt in `/data`), den die Diagnostic anhaengt — Anfragen aus ARIAs Container weist sie ab, sonst koennte eine eingehende Mail ARIA dazu bringen, per curl die Whitelist zu oeffnen. *Grenze:* ARIAs Container hat einen SSH-Schluessel auf die VM; legt sie es gezielt darauf an, kommt sie als root an jede Datei — das hier verhindert, dass Zugangsdaten von selbst irgendwo auftauchen, keinen bewussten Einbruch. Eingehende Mails sind fuer ARIA als **Daten, nicht Anweisungen** markiert. Kopie jeder gesendeten Mail landet in „Gesendet" (fehlt der Ordner, wird er angelegt). **📬 Postfach oeffnen** zeigt ARIAs ganzes Postfach in eigenem Fenster, Unterordner eingerueckt — nur fuer dich zum Reinschauen, markiert nichts als gelesen (sonst wuesste ARIA nicht mehr, was neu ist); Mail-HTML laeuft in einem Rahmen ohne Skripte und ohne Nachladen von aussen (kein Tracking-Pixel), eingebettete Bilder werden mitgeliefert
|
||||
- **Modell pro Chat**: jeder Chat (Hauptchat und jedes Projekt) kann sein eigenes Modell haben, ohne die Einstellung fuer alle umzustellen. Reihenfolge: **Einstellung** (`brainModel`) < **pro Chat** (`/shared/config/context_models.json`, gesetzt per 🧠-Auswahl neben „+ Projekt") < **Ausweich** (Sicherheitsfilter, s.u.). Angezeigt wird immer, was der **naechste Turn wirklich nutzt**: in den Kontext-Karten, in der Auswahl (orange „↪ läuft auf …" plus **„↩ zurück zu …"**, wenn gerade ausgewichen wird) und an jeder ARIA-Bubble — dort das Modell, das tatsaechlich geantwortet hat (aus `modelUsage` der CLI, also die aufgeloeste Version statt des Alias). Als Grundeinstellung zaehlt das Modell, mit dem das Brain **laeuft**, nicht der Dateiwert — ein ausstehender Neustart nach Aenderung von `brainModel` faellt so auf. In der **App** dasselbe hinter der Quell-Anzeige (Einstellungen, Default aus): Modell-Chip in der Kopfzeile, Tippen oeffnet die Auswahl fuer diesen Chat, die Bubbles zeigen das Modell statt nur „Claude"
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
.modal-overlay { display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.85);
|
||||
z-index:1000; justify-content:center; align-items:center; }
|
||||
.modal-overlay.open { display:flex; }
|
||||
@keyframes llmdlpulse { 0%,100% { opacity:.15; } 50% { opacity:.45; } }
|
||||
.modal-box { background:#0D0D1A; border:1px solid #FFD60A44; border-radius:10px; width:90vw; max-width:860px;
|
||||
max-height:90vh; display:flex; flex-direction:column; overflow:hidden; }
|
||||
.modal-header { display:flex; justify-content:space-between; align-items:center; padding:10px 14px;
|
||||
@@ -843,6 +844,7 @@
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<div class="card" style="max-width:1100px;">
|
||||
<p style="color:#8888AA;font-size:12px;margin:0 0 8px;">
|
||||
@@ -1859,6 +1861,25 @@
|
||||
</div>
|
||||
|
||||
<!-- Generisches Info-Modal — wird via openInfoModal(title, html) gefuellt -->
|
||||
<!-- Modell-Downloads auf LLM-Boxen: Fortschritt, dann „in die Karte geladen" -->
|
||||
<div class="modal-overlay" id="llm-dl-modal" onclick="if(event.target===this)closeLlmDownloads()">
|
||||
<div class="modal-box" style="max-width:720px;">
|
||||
<div class="modal-header">
|
||||
<h3>⬇ Modell-Downloads</h3>
|
||||
<div style="display:flex;gap:8px;align-items:center;">
|
||||
<button class="btn secondary" onclick="send({action:'clear_llm_jobs'})" title="Fertige und fehlgeschlagene aus der Liste nehmen" style="padding:3px 10px;font-size:11px;">Erledigte entfernen</button>
|
||||
<button class="modal-close" onclick="closeLlmDownloads()">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="llm-dl-list" style="padding:14px;overflow-y:auto;display:flex;flex-direction:column;gap:12px;"></div>
|
||||
<div style="padding:0 14px 12px;color:#555570;font-size:11px;line-height:1.5;">
|
||||
Pro LLM-Box läuft immer <strong style="color:#8888AA;">ein</strong> Modell auf ihrer LLM-Karte
|
||||
(<code>LLM_GPU</code> in der .env der Box). Laden eines anderen wechselt dorthin — das vorige
|
||||
wird entladen, bleibt aber heruntergeladen.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="info-modal">
|
||||
<div class="modal-box" style="max-width:640px;">
|
||||
<div class="modal-header">
|
||||
@@ -2313,6 +2334,8 @@
|
||||
try { loadModelList(); } catch {}
|
||||
// Sicherheitsfilter-Vorfaelle (Blockaden + Quarantaene)
|
||||
try { loadSafeguard(); } catch {}
|
||||
// Laufende Modell-Downloads (Badge am ⬇-Knopf)
|
||||
send({ action: 'get_llm_jobs' });
|
||||
// Lokales-LLM: erst Modell-Liste (Dropdown), dann Config (Auswahl setzen)
|
||||
try { loadLocalModelList().then(() => loadLocalLlmConfig()); } catch {}
|
||||
};
|
||||
@@ -2574,6 +2597,13 @@
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (msg.type === 'llm_provision_progress') { onLlmJob(msg.payload); return; }
|
||||
if (msg.type === 'llm_jobs') {
|
||||
_llmJobs = {};
|
||||
for (const j of ((msg.payload || {}).jobs || [])) _llmJobs[_llmJobId(j)] = j;
|
||||
renderLlmDownloads();
|
||||
return;
|
||||
}
|
||||
if (msg.type === 'llm_provision_result') {
|
||||
const p = msg.payload || {};
|
||||
const st = document.getElementById('llm-catalog-status');
|
||||
@@ -8942,6 +8972,105 @@
|
||||
'<th ' + th + '>Status</th><th ' + th + '>Ziel-Box</th><th ' + th + '></th>' +
|
||||
'</tr></thead><tbody>' + rows + '</tbody></table>';
|
||||
}
|
||||
// ── Download-Fenster ──────────────────────────────────────────────
|
||||
// Zustand kommt vom Diagnostic-Server (llm_provision_progress / llm_jobs);
|
||||
// der kennt auch Downloads, die vor dem Oeffnen dieses Tabs gestartet sind.
|
||||
let _llmJobs = {};
|
||||
const _llmJobId = j => j.instanceId + '|' + j.key;
|
||||
|
||||
function _fmtBytes(b) {
|
||||
if (!b && b !== 0) return '?';
|
||||
if (b >= 1e9) return (b / 1e9).toFixed(2) + ' GB';
|
||||
if (b >= 1e6) return (b / 1e6).toFixed(0) + ' MB';
|
||||
return Math.round(b / 1e3) + ' KB';
|
||||
}
|
||||
function _fmtEta(s) {
|
||||
if (s == null) return '';
|
||||
if (s < 60) return s + ' s';
|
||||
return Math.floor(s / 60) + ' min ' + String(s % 60).padStart(2, '0') + ' s';
|
||||
}
|
||||
function _boxName(instanceId) {
|
||||
const w = (typeof workers !== 'undefined' ? workers : []).find(x => x.instanceId === instanceId);
|
||||
return (w && w.node) || (instanceId || '').replace(/^llm@/, '');
|
||||
}
|
||||
|
||||
function onLlmJob(job) {
|
||||
if (!job || !job.key) return;
|
||||
_llmJobs[_llmJobId(job)] = job;
|
||||
renderLlmDownloads();
|
||||
// Fertig geladen → Katalog-Status ("✓ ki-box") nachziehen.
|
||||
if (job.phase === 'ready' && typeof requestWorkers === 'function') requestWorkers();
|
||||
}
|
||||
|
||||
function renderLlmDownloads() {
|
||||
const jobs = Object.values(_llmJobs).sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0));
|
||||
const running = jobs.filter(j => !['ready', 'error'].includes(j.phase)).length;
|
||||
const badge = document.getElementById('llm-dl-badge');
|
||||
if (badge) { badge.textContent = running; badge.style.display = running ? 'inline-block' : 'none'; }
|
||||
const box = document.getElementById('llm-dl-list');
|
||||
if (!box) return;
|
||||
if (!jobs.length) {
|
||||
box.innerHTML = '<div style="color:#6a6a88;text-align:center;padding:20px;">Keine Downloads. „Laden" im Modell-Katalog startet einen.</div>';
|
||||
return;
|
||||
}
|
||||
box.innerHTML = jobs.map(j => {
|
||||
const box_ = escapeHtml(_boxName(j.instanceId));
|
||||
let pct = 0, color = '#0096FF', line = '', sub = '';
|
||||
if (j.phase === 'queued') {
|
||||
// Kommt hier nie mehr was, erreicht der Auftrag die Box nicht.
|
||||
const age = (Date.now() - (j.requestedAt || j.startedAt || Date.now())) / 1000;
|
||||
line = 'Angefragt — warte auf die Box …';
|
||||
if (age > 30) { line = '⚠ Die Box antwortet nicht.'; color = '#FF9F0A';
|
||||
sub = 'Ist sie online? Nach einem Update von RVS und Box erneut versuchen.'; }
|
||||
} else if (j.phase === 'start') {
|
||||
line = 'Box hat den Auftrag — prüfe, ob das Modell schon da ist …';
|
||||
} else if (j.phase === 'download') {
|
||||
pct = j.percent != null ? j.percent : 0;
|
||||
line = j.percent != null
|
||||
? `${pct.toFixed(1)} % · ${_fmtBytes(j.bytes)} / ${_fmtBytes(j.total)}`
|
||||
: `${_fmtBytes(j.bytes)} heruntergeladen (Größe unbekannt)`;
|
||||
sub = (j.speedBps ? (j.speedBps / 1e6).toFixed(1) + ' MB/s' : '') + (j.etaSeconds != null ? ' · noch ' + _fmtEta(j.etaSeconds) : '');
|
||||
} else if (j.phase === 'load') {
|
||||
pct = 100; color = '#FFD60A';
|
||||
line = j.cached ? 'Liegt schon auf der Box — lädt in die Karte …' : '100 % heruntergeladen — lädt jetzt in die Karte …';
|
||||
sub = 'Das Modell wandert in den Grafikspeicher; das dauert je nach Größe ein paar Sekunden bis Minuten.';
|
||||
} else if (j.phase === 'ready') {
|
||||
pct = 100; color = '#34C759';
|
||||
const card = j.gpu != null && j.gpu !== '' ? `Karte ${escapeHtml(String(j.gpu))}${j.gpuName ? ' (' + escapeHtml(j.gpuName) + ')' : ''}` : 'die Karte';
|
||||
line = `✓ In ${card} geladen — bereit`;
|
||||
const vram = j.vramTotalMB ? ` · VRAM ${(j.vramUsedMB / 1024).toFixed(1)} / ${(j.vramTotalMB / 1024).toFixed(1)} GB belegt` : '';
|
||||
sub = (j.loadSeconds != null ? `Dauer ${_fmtEta(Math.round(j.loadSeconds))}` : '') + vram;
|
||||
} else if (j.phase === 'error') {
|
||||
color = '#FF6B6B'; pct = j.percent || 0;
|
||||
line = '✗ ' + escapeHtml(j.error || 'Fehler');
|
||||
}
|
||||
const bar = `<div style="height:8px;background:#1E1E2E;border-radius:4px;overflow:hidden;margin:6px 0 4px;">
|
||||
<div style="height:100%;width:${Math.max(0, Math.min(100, pct))}%;background:${color};transition:width .6s;
|
||||
${j.phase === 'queued' || j.phase === 'start' ? 'opacity:.35;width:100%;animation:llmdlpulse 1.2s ease-in-out infinite;' : ''}"></div></div>`;
|
||||
return `<div style="background:#12121E;border:1px solid #2A2A3E;border-radius:8px;padding:10px 12px;">
|
||||
<div style="display:flex;justify-content:space-between;gap:10px;">
|
||||
<strong style="color:#E0E0F0;">${escapeHtml(j.key)}</strong>
|
||||
<span style="color:#8888AA;font-size:11px;">→ ${box_}</span>
|
||||
</div>${bar}
|
||||
<div style="color:${color === '#0096FF' ? '#E0E0F0' : color};font-size:12px;">${line}</div>
|
||||
${sub ? `<div style="color:#6a6a88;font-size:11px;margin-top:2px;">${sub}</div>` : ''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function openLlmDownloads() {
|
||||
send({ action: 'get_llm_jobs' });
|
||||
renderLlmDownloads();
|
||||
document.getElementById('llm-dl-modal').classList.add('open');
|
||||
}
|
||||
function closeLlmDownloads() {
|
||||
document.getElementById('llm-dl-modal').classList.remove('open');
|
||||
}
|
||||
// "Box antwortet nicht" braucht ein Ticken, auch wenn keine Meldung kommt.
|
||||
setInterval(() => {
|
||||
if (Object.values(_llmJobs).some(j => j.phase === 'queued')) renderLlmDownloads();
|
||||
}, 5000);
|
||||
|
||||
function provisionModel(i) {
|
||||
const m = _llmCatalog[i];
|
||||
if (!m) return;
|
||||
@@ -8951,6 +9080,7 @@
|
||||
const st = document.getElementById('llm-catalog-status');
|
||||
if (st) { st.textContent = `Lade '${m.id}' auf ${target}… (GGUF-Download kann dauern)`; st.style.color = '#FFD60A'; }
|
||||
send({ action: 'llm_provision_model', targetInstance: target, key: m.id, hfRepo: m.hfRepo, quant: m.quant, ctx: m.ctx });
|
||||
openLlmDownloads();
|
||||
}
|
||||
function runLlmTest() {
|
||||
const inp = document.getElementById('llm-test-input');
|
||||
|
||||
+43
-1
@@ -1372,7 +1372,17 @@ function connectRVS(forcePlain) {
|
||||
broadcast({ type: "service_status", payload: msg.payload });
|
||||
} else if (msg.type === "llm_provision_result") {
|
||||
// Ergebnis eines Modell-Downloads/Aktivierens → an Browser (Katalog-Status).
|
||||
broadcast({ type: "llm_provision_result", payload: msg.payload || {} });
|
||||
const p = msg.payload || {};
|
||||
if (p.key && p.instanceId && !p.removed) {
|
||||
const job = llmJobUpdate(p.instanceId, p.key, p.ok ? {} : { phase: "error", error: p.error || "Fehler" });
|
||||
if (job) broadcast({ type: "llm_provision_progress", payload: job });
|
||||
}
|
||||
broadcast({ type: "llm_provision_result", payload: p });
|
||||
} else if (msg.type === "llm_provision_progress") {
|
||||
// Download-Fortschritt einer Box (Prozent, Tempo, Restzeit, dann
|
||||
// "laedt in die Karte" und "geladen"). Merken fuer spaeter geoeffnete Tabs.
|
||||
const job = llmJobUpdate((msg.payload || {}).instanceId, (msg.payload || {}).key, msg.payload || {});
|
||||
if (job) broadcast({ type: "llm_provision_progress", payload: job });
|
||||
} else if (msg.type === "node_stats" || msg.type === "node_stats_history" || msg.type === "node_stats_reset_done") {
|
||||
// Auslastungs-Monitor (Stage E): Box-Antworten an die Browser durchreichen.
|
||||
broadcast({ type: msg.type, payload: msg.payload || {} });
|
||||
@@ -2074,6 +2084,27 @@ setInterval(checkDiskSpace, 30000);
|
||||
|
||||
// ── HTTP Server + WebSocket fuer Browser ────────────────
|
||||
|
||||
// ── Modell-Downloads auf LLM-Boxen ──────────────────────────────────
|
||||
// Letzter bekannter Stand pro (Box, Modell). Im Speicher reicht: das Fenster
|
||||
// soll zeigen, was gerade laeuft und was eben fertig wurde, keine Historie.
|
||||
const llmJobs = new Map();
|
||||
function llmJobUpdate(instanceId, key, patch) {
|
||||
if (!instanceId || !key) return null;
|
||||
const id = `${instanceId}\u0000${key}`;
|
||||
const job = { ...(llmJobs.get(id) || { instanceId, key, startedAt: Date.now() }), ...patch,
|
||||
instanceId, key, updatedAt: Date.now() };
|
||||
// Ein neuer Auftrag fuer dasselbe Modell faengt frisch an.
|
||||
if (patch.phase === "queued") { delete job.error; delete job.percent; delete job.bytes; job.startedAt = Date.now(); }
|
||||
llmJobs.set(id, job);
|
||||
// Alte Abgeschlossene nicht ewig mitschleppen.
|
||||
if (llmJobs.size > 30) {
|
||||
const done = [...llmJobs.entries()].filter(([, j]) => j.phase === "ready" || j.phase === "error")
|
||||
.sort((a, b) => a[1].updatedAt - b[1].updatedAt);
|
||||
while (llmJobs.size > 30 && done.length) llmJobs.delete(done.shift()[0]);
|
||||
}
|
||||
return job;
|
||||
}
|
||||
|
||||
const htmlPath = path.join(__dirname, "index.html");
|
||||
const mailHtmlPath = path.join(__dirname, "email.html");
|
||||
|
||||
@@ -3320,8 +3351,19 @@ wss.on("connection", (ws) => {
|
||||
// Sessions- und Brain-File-Viewer entfernt — Sessions sind raus, Memory
|
||||
// laeuft jetzt komplett ueber die Vector-DB im aria-brain (siehe Gehirn-Tab).
|
||||
// restart_session kommt weiter rein, weil der Watchdog ihn manchmal triggert.
|
||||
} else if (msg.action === "get_llm_jobs") {
|
||||
ws.send(JSON.stringify({ type: "llm_jobs", payload: { jobs: [...llmJobs.values()] } }));
|
||||
} else if (msg.action === "clear_llm_jobs") {
|
||||
// Nur Abgeschlossene wegraeumen — laufende Downloads bleiben sichtbar.
|
||||
for (const [k, j] of llmJobs) if (j.phase === "ready" || j.phase === "error") llmJobs.delete(k);
|
||||
broadcast({ type: "llm_jobs", payload: { jobs: [...llmJobs.values()] } });
|
||||
} else if (msg.action === "llm_provision_model") {
|
||||
// Modell auf eine bestimmte LLM-Box laden/aktivieren (Stage D).
|
||||
// Sofort als "angefragt" fuehren: kommt danach nie etwas von der Box,
|
||||
// sieht man das im Download-Fenster, statt dass einfach nichts passiert.
|
||||
const queued = llmJobUpdate(msg.targetInstance || "", msg.key || "",
|
||||
{ phase: "queued", requestedAt: Date.now() });
|
||||
if (queued) broadcast({ type: "llm_provision_progress", payload: queued });
|
||||
sendToRVS_raw({ type: "llm_provision_model", payload: {
|
||||
targetInstance: msg.targetInstance || "",
|
||||
key: msg.key, hfRepo: msg.hfRepo, quant: msg.quant, ctx: msg.ctx, ngl: msg.ngl,
|
||||
|
||||
+6
-1
@@ -124,6 +124,11 @@ const ALLOWED_TYPES = new Set([
|
||||
// OpenAI-Endpunkt einer LLM-Box: Diagnostic setzt den API-Key (der PORT kommt
|
||||
// immer aus der .env der Box — er ist ein Docker-Port-Mapping).
|
||||
"llm_openai_config", "llm_openai_config_result",
|
||||
// Modell auf eine LLM-Box laden (Diagnostic → Modell-Katalog → „Laden").
|
||||
// Fehlten hier seit Stage D: jeder Klick auf „Laden" starb still an dieser
|
||||
// Liste, die Box hat nie davon erfahren. _progress meldet den Download in %.
|
||||
"llm_provision_model", "llm_provision_result", "llm_provision_progress",
|
||||
"llm_remove_model",
|
||||
"node_stats", "node_stats_stream_start", "node_stats_stream_stop",
|
||||
"node_stats_history_request", "node_stats_history",
|
||||
"node_stats_reset", "node_stats_reset_done",
|
||||
@@ -161,7 +166,7 @@ const pendingReplies = new Map();
|
||||
const PENDING_TTL_MS = 180_000;
|
||||
|
||||
function pendingKey(replyType, requestId) {
|
||||
return `${replyType} | ||||