feat(diagnostic): eigene Dialoge statt alert/confirm/prompt

Alle 43 confirm(), 7 prompt() und 71 alert() der Diagnostic laufen jetzt
ueber uiConfirm/uiPrompt/uiAlert — gleicher Stil wie der Aufteilen-Dialog:
erster Absatz als Ueberschrift, Rest als Erklaerung, Esc/Klick daneben =
Abbrechen, Enter = OK. Rueckfragen zu Loeschen/Entfernen/Zuruecksetzen
bekommen einen roten "Ja, ausfuehren"-Knopf. Nebenbei faellt die Browser-
Sperre "weitere Dialoge verhindern" weg, an der runDiskCleanup frueher hing.

Die 16 Funktionen, die noch nicht async waren, sind es jetzt (alles
onclick/onchange-Handler, kein Aufrufer nutzt einen Rueckgabewert).
Getestet in headless Chromium: OK/Abbrechen/Esc/Enter, Eingabe, Hinweis.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 15:31:28 +01:00
co-authored by Claude Opus 5.5
parent 87f521ee61
commit c511a6b5ae
+201 -137
View File
@@ -2301,7 +2301,7 @@
// project_changed an App + andere Tabs).
await refreshDiagProjectsCache();
} catch (e) {
alert('Verstecken/Anzeigen fehlgeschlagen: ' + e.message);
uiAlert('Verstecken/Anzeigen fehlgeschlagen: ' + e.message);
}
}
@@ -2771,9 +2771,9 @@
if (msg.type === 'voice_id_delete_response') {
const p = msg.payload || msg;
if (p.removed) {
alert('Fingerprint gelöscht — Voice-ID-Gating fällt zurück auf Fail-Open.');
uiAlert('Fingerprint gelöscht — Voice-ID-Gating fällt zurück auf Fail-Open.');
} else {
alert('Es war kein Fingerprint vorhanden.');
uiAlert('Es war kein Fingerprint vorhanden.');
}
refreshVoiceIdStatus();
loadLlmCatalog();
@@ -3195,7 +3195,7 @@
* Backend (Bridge) raeumt chat_backup.jsonl + Brain-Conversation
* und broadcastet danach chat_message_deleted — wir entfernen die
* Bubble lokal erst dann, nicht optimistisch. */
function deleteDiagBubble(ts) {
async function deleteDiagBubble(ts) {
if (!ts) return;
let preview = '';
for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
@@ -3203,7 +3203,7 @@
const el = box.querySelector(`.chat-msg[data-ts="${ts}"]`);
if (el) { preview = (el.textContent || '').slice(0, 80); break; }
}
if (!confirm(`Diese Bubble wirklich loeschen?\n\n"${preview}…"\n\nWird aus chat_backup, Brain-Konversation und allen Clients entfernt.`)) return;
if (!await uiConfirm(`Diese Bubble wirklich loeschen?\n\n"${preview}…"\n\nWird aus chat_backup, Brain-Konversation und allen Clients entfernt.`)) return;
send({ action: 'delete_chat_message', ts });
}
@@ -3619,7 +3619,7 @@
diagModelNames = diagCtxModels.names || {};
renderContextStrip();
renderCtxModelBar();
} catch (e) { alert('Modell setzen fehlgeschlagen: ' + e.message); }
} catch (e) { uiAlert('Modell setzen fehlgeschlagen: ' + e.message); }
}
async function ctxModelBack() {
@@ -3634,7 +3634,7 @@
renderContextStrip();
renderCtxModelBar();
try { loadSafeguard(); } catch {}
} catch (e) { alert('Zurückschalten fehlgeschlagen: ' + e.message); }
} catch (e) { uiAlert('Zurückschalten fehlgeschlagen: ' + e.message); }
}
// Untertitel der Kontext-Karten: welches Modell der naechste Turn nutzt.
@@ -4105,9 +4105,9 @@
if (modal) modal.style.display = 'none';
}
function clearThoughtStream() {
async function clearThoughtStream() {
if (thoughtStream.length === 0) return;
if (!confirm(`Gedanken-Stream leeren? ${thoughtStream.length} Eintraege werden geloescht.`)) return;
if (!await uiConfirm(`Gedanken-Stream leeren? ${thoughtStream.length} Eintraege werden geloescht.`)) return;
thoughtStream = [];
lastThoughtKey = '';
updateThoughtsBadge();
@@ -4202,7 +4202,7 @@
const b64 = btoa(bin);
// Name aus Dateinamen ableiten — z.B. "maia.tar.gz" → "maia"
const baseName = file.name.replace(/\.tar\.gz$|\.tgz$/i, '');
if (!confirm(`Stimme "${baseName}" importieren?\n\nFalls schon vorhanden, wird sie überschrieben.`)) {
if (!await uiConfirm(`Stimme "${baseName}" importieren?\n\nFalls schon vorhanden, wird sie überschrieben.`)) {
event.target.value = '';
return;
}
@@ -4278,8 +4278,8 @@
send({ action: 'voice_id_status' });
}
function deleteVoiceId() {
if (!confirm('Voice-ID-Fingerprint loeschen?\n\nDanach muss in der App neu enrolled werden.')) return;
async function deleteVoiceId() {
if (!await uiConfirm('Voice-ID-Fingerprint loeschen?\n\nDanach muss in der App neu enrolled werden.')) return;
send({ action: 'voice_id_delete' });
}
@@ -4377,7 +4377,7 @@
// Beim Verstecken bleibt der „anzeigen"-Modus wie er ist; beim
// Wieder-Sichtbarmachen sieht man es sofort ohne Umschalten.
loadProjects();
} catch (e) { alert('Verstecken/Anzeigen fehlgeschlagen: ' + e.message); }
} catch (e) { uiAlert('Verstecken/Anzeigen fehlgeschlagen: ' + e.message); }
}
async function setProjectKind(id, kind) {
@@ -4389,7 +4389,7 @@
});
if (!r.ok) throw new Error('HTTP ' + r.status);
loadProjects();
} catch (e) { alert('Code-Markierung fehlgeschlagen: ' + e.message); }
} catch (e) { uiAlert('Code-Markierung fehlgeschlagen: ' + e.message); }
}
async function switchProject(projectId) {
@@ -4400,23 +4400,23 @@
body: JSON.stringify({ project_id: projectId }),
});
loadProjects();
} catch (e) { alert('Wechsel fehlgeschlagen: ' + e.message); }
} catch (e) { uiAlert('Wechsel fehlgeschlagen: ' + e.message); }
}
async function endProject(id, name) {
if (!confirm(`Projekt "${name}" beenden?\n\nBleibt sichtbar, aktiv ist dann der Hauptchat.`)) return;
if (!await uiConfirm(`Projekt "${name}" beenden?\n\nBleibt sichtbar, aktiv ist dann der Hauptchat.`)) return;
try {
await fetch(`/api/brain/projects/${encodeURIComponent(id)}/end`, { method: 'POST' });
loadProjects();
} catch (e) { alert('Beenden fehlgeschlagen: ' + e.message); }
} catch (e) { uiAlert('Beenden fehlgeschlagen: ' + e.message); }
}
async function archiveProject(id, name) {
if (!confirm(`Projekt "${name}" archivieren?\n\nVerschwindet aus der Liste.`)) return;
if (!await uiConfirm(`Projekt "${name}" archivieren?\n\nVerschwindet aus der Liste.`)) return;
try {
await fetch(`/api/brain/projects/${encodeURIComponent(id)}/archive`, { method: 'POST' });
loadProjects();
} catch (e) { alert('Archivieren fehlgeschlagen: ' + e.message); }
} catch (e) { uiAlert('Archivieren fehlgeschlagen: ' + e.message); }
}
function openCreateProjectModal() {
@@ -4433,7 +4433,7 @@
async function submitCreateProject() {
const name = document.getElementById('project-create-name').value.trim();
const description = document.getElementById('project-create-desc').value.trim();
if (!name) { alert('Name darf nicht leer sein.'); return; }
if (!name) { uiAlert('Name darf nicht leer sein.'); return; }
try {
const r = await fetch('/api/brain/projects/create', {
method: 'POST',
@@ -4458,7 +4458,7 @@
// ein Projekt an und schreibt weiter in den Hauptchat.
const pid = (d && d.id) ? d.id : '';
if (pid) switchDiagFocus(pid);
} catch (e) { alert('Anlegen fehlgeschlagen: ' + e.message); }
} catch (e) { uiAlert('Anlegen fehlgeschlagen: ' + e.message); }
}
function escapeHtml(str) {
@@ -4468,8 +4468,72 @@
return String(str).replace(/['"\\]/g, '\\$&');
}
function deleteXttsVoice(name) {
if (!confirm(`Stimme "${name}" endgueltig loeschen?`)) return;
// ── Eigene Dialoge statt alert/confirm/prompt ─────────────────────
// Gleicher Stil wie der Aufteilen-Dialog. Alle liefern ein Promise:
// uiConfirm → true/false, uiPrompt → Text oder null, uiAlert → undefined.
// Erster Absatz der Meldung = Ueberschrift, der Rest = Erklaerung.
// Esc / Klick daneben = Abbrechen, Enter = OK.
const _UI_DANGER = /l[öo]e?schen|entfernen|zur[üu]cksetzen|NOT-AUS|AGGRESSIV|WIRKLICH|endg[üu]ltig|archivieren|beenden|verloren|[üu]berschrieben|leeren/i;
function _uiDialog({ message, input, defaultValue, buttons }) {
return new Promise(resolve => {
const text = String(message ?? '');
const cut = text.indexOf('\n\n');
const title = cut > 0 ? text.slice(0, cut) : text;
const rest = cut > 0 ? text.slice(cut + 2) : '';
const bg = document.createElement('div');
bg.style.cssText = 'position:fixed;inset:0;z-index:10001;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:480px;width:100%;color:#E0E0F0;font-size:13px;line-height:1.5;">
<div style="font-weight:700;white-space:pre-wrap;">${escapeHtml(title)}</div>
${rest ? `<div style="color:#9a9ab5;font-size:12px;margin-top:8px;white-space:pre-wrap;max-height:50vh;overflow-y:auto;">${escapeHtml(rest)}</div>` : ''}
${input ? `<input type="text" style="width:100%;box-sizing:border-box;margin-top:12px;background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:6px 8px;color:#E0E0F0;font-size:13px;">` : ''}
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:14px;">
${buttons.map((b, i) => `<button type="button" data-i="${i}" class="btn ${b.primary ? '' : 'secondary'}"
style="padding:6px 14px;font-size:12px;${b.danger ? 'background:#C0392B;border-color:#C0392B;color:#fff;' : ''}">${escapeHtml(b.label)}</button>`).join('')}
</div></div>`;
const inp = bg.querySelector('input');
if (inp) inp.value = defaultValue ?? '';
const prevFocus = document.activeElement;
const done = v => {
document.removeEventListener('keydown', onKey, true);
bg.remove();
try { prevFocus && prevFocus.focus && prevFocus.focus(); } catch (_) {}
resolve(v);
};
const value = b => (inp && b.value === true ? inp.value : b.value);
const primary = buttons.find(b => b.primary) || buttons[buttons.length - 1];
const cancel = buttons.find(b => b.cancel) || primary;
const onKey = e => {
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); done(cancel.value); }
else if (e.key === 'Enter' && (!inp || document.activeElement === inp)) { e.preventDefault(); e.stopPropagation(); done(value(primary)); }
};
bg.addEventListener('click', e => {
const b = e.target.closest('[data-i]');
if (b) done(value(buttons[+b.dataset.i]));
else if (e.target === bg) done(cancel.value);
});
document.addEventListener('keydown', onKey, true);
document.body.appendChild(bg);
(inp || bg.querySelector(`[data-i="${buttons.indexOf(primary)}"]`)).focus();
if (inp) inp.select();
});
}
function uiConfirm(message) {
const danger = _UI_DANGER.test(String(message ?? ''));
return _uiDialog({ message, buttons: [
{ label: 'Abbrechen', value: false, cancel: true },
{ label: danger ? 'Ja, ausführen' : 'OK', value: true, primary: true, danger } ] });
}
function uiPrompt(message, defaultValue = '') {
return _uiDialog({ message, input: true, defaultValue, buttons: [
{ label: 'Abbrechen', value: null, cancel: true },
{ label: 'OK', value: true, primary: true } ] });
}
function uiAlert(message) {
return _uiDialog({ message, buttons: [{ label: 'OK', value: undefined, primary: true }] });
}
async function deleteXttsVoice(name) {
if (!await uiConfirm(`Stimme "${name}" endgueltig loeschen?`)) return;
send({ action: 'xtts_delete_voice', name });
// Bei aktueller Auswahl: auf Default zuruecksetzen
const sel = document.getElementById('diag-xtts-voice');
@@ -4498,9 +4562,9 @@
async function uploadVoiceSamples() {
const name = document.getElementById('xtts-clone-name').value.trim();
const files = document.getElementById('xtts-clone-files').files;
if (!name) { alert('Bitte einen Namen eingeben'); return; }
if (!files || files.length === 0) { alert('Bitte Audio-Dateien auswaehlen'); return; }
if (files.length > 10) { alert('Maximal 10 Dateien'); return; }
if (!name) { uiAlert('Bitte einen Namen eingeben'); return; }
if (!files || files.length === 0) { uiAlert('Bitte Audio-Dateien auswaehlen'); return; }
if (files.length > 10) { uiAlert('Maximal 10 Dateien'); return; }
const status = document.getElementById('xtts-clone-status');
status.textContent = `Lade ${files.length} Datei(en)...`;
@@ -4592,7 +4656,7 @@
const text = await file.text();
// Sanity-Check: muss JSON sein
JSON.parse(text);
if (!confirm(`Voice-Settings aus "${file.name}" importieren?\n\nAktuelle voice_config + highlight_triggers werden überschrieben.`)) {
if (!await uiConfirm(`Voice-Settings aus "${file.name}" importieren?\n\nAktuelle voice_config + highlight_triggers werden überschrieben.`)) {
event.target.value = '';
return;
}
@@ -4603,13 +4667,13 @@
});
const d = await r.json();
if (d.ok) {
alert('✓ ' + d.message);
uiAlert('✓ ' + d.message);
send({ action: 'get_voice_config' });
} else {
alert('Import fehlgeschlagen: ' + d.error);
uiAlert('Import fehlgeschlagen: ' + d.error);
}
} catch (e) {
alert('Ungültige Datei: ' + e.message);
uiAlert('Ungültige Datei: ' + e.message);
} finally {
event.target.value = '';
}
@@ -4618,8 +4682,8 @@
// ── Wipe All — Komplett-Reset ──────────────────────────
async function wipeAll() {
const statusEl = document.getElementById('wipe-status');
if (!confirm('WIRKLICH alles löschen?\n\nGedächtnis + Stimmen + Settings → komplett weg.\n.env + SSH-Keys bleiben.\n\nVorher exportieren wenn was erhalten bleiben soll.')) return;
if (!confirm('Letzte Warnung: Das KANN NICHT rückgängig gemacht werden.\n\nFortfahren?')) return;
if (!await uiConfirm('WIRKLICH alles löschen?\n\nGedächtnis + Stimmen + Settings → komplett weg.\n.env + SSH-Keys bleiben.\n\nVorher exportieren wenn was erhalten bleiben soll.')) return;
if (!await uiConfirm('Letzte Warnung: Das KANN NICHT rückgängig gemacht werden.\n\nFortfahren?')) return;
statusEl.innerHTML = '⏳ Stoppe Container, lösche Daten...';
try {
const r = await fetch('/api/wipe-all', { method: 'POST' });
@@ -4638,7 +4702,7 @@
// ── Generischer Container-Restart (Bridge/Brain/Qdrant) ──────
async function restartContainer(name) {
const statusEl = document.getElementById('restart-status');
if (!confirm(`Container "${name}" wirklich neu starten?\n\nLaufende Anfragen gehen verloren.`)) return;
if (!await uiConfirm(`Container "${name}" wirklich neu starten?\n\nLaufende Anfragen gehen verloren.`)) return;
if (statusEl) statusEl.innerHTML = `⏳ Restart ${name}...`;
try {
const r = await fetch('/api/container-restart', {
@@ -5175,8 +5239,8 @@
}
return `<div style="color:#AAAACC;">${time} <span>${_ariaEsc(kind)}: ${_ariaEsc(JSON.stringify(p).slice(0, 500))}</span></div>`;
}
function ariaPanicStop() {
if (!confirm('Wirklich NOT-AUS? Alle aktiven Claude-Subprocesses werden sofort gekillt.')) return;
async function ariaPanicStop() {
if (!await uiConfirm('Wirklich NOT-AUS? Alle aktiven Claude-Subprocesses werden sofort gekillt.')) return;
send({ action: 'aria_panic_stop' });
_ariaPushLine(
`<span style="color:#FF3B30;font-weight:bold;">━━━ ${_ariaTimePrefix()} ⛔ NOT-AUS ausgeloest ━━━</span>`,
@@ -5679,8 +5743,8 @@
}
/** Entfernt einen Agenten aus der Liste (z.B. den alten Namen nach dem
* Umbenennen). Läuft er noch, meldet er sich von selbst wieder. */
function forgetHost(hostId, name) {
if (!confirm('"' + name + '" aus der Liste entfernen?\n\n' +
async function forgetHost(hostId, name) {
if (!await uiConfirm('"' + name + '" aus der Liste entfernen?\n\n' +
'Das stoppt den Agenten NICHT — läuft er noch, erscheint er ' +
'beim nächsten Lebenszeichen wieder.')) return;
send({ action: 'host_forget', hostId });
@@ -5756,7 +5820,7 @@
if (pw) body.password = pw;
if (spw) body.smtp_password = spw;
if (body.imap_encryption === 'none' || body.smtp_encryption === 'none') {
if (!confirm('Ohne Verschlüsselung gehen Passwort und Mails im Klartext durchs Netz. Trotzdem speichern?')) return;
if (!await uiConfirm('Ohne Verschlüsselung gehen Passwort und Mails im Klartext durchs Netz. Trotzdem speichern?')) return;
}
_mailStatus('Speichere …');
try {
@@ -5809,9 +5873,9 @@
renderJellyfin();
}
function removeJellyfinServer(i) {
async function removeJellyfinServer(i) {
const s = jellyfinServers[i];
if (!confirm('Server "' + (s.name || s.url || 'neu') + '" entfernen?')) return;
if (!await uiConfirm('Server "' + (s.name || s.url || 'neu') + '" entfernen?')) return;
jellyfinServers.splice(i, 1);
if (jellyfinServers.length && !jellyfinServers.some(x => x.isDefault)) {
jellyfinServers[0].isDefault = true;
@@ -6110,9 +6174,9 @@
* sofort und fuer jeden schon ausgerollten Agenten. Die Bridge loest ihn
* beim Ansprechen wieder auf, damit sich der Agent weiter angesprochen
* fuehlt. */
function renameEntity(kind, id, currentAlias, ownName) {
async function renameEntity(kind, id, currentAlias, ownName) {
const was = kind === 'satellite' ? 'Satellit' : 'Agent';
const name = prompt(
const name = await uiPrompt(
was + ' umbenennen\n\n'
+ 'Heißt sich selbst: ' + ownName + '\n'
+ 'ARIA spricht ihn danach unter dem neuen Namen an.\n'
@@ -6126,8 +6190,8 @@
* ("mach mir Port 5000 von duffy-desktop auf") statt ueber eine IP.
* Gespeichert wird an der MAC — ein IP-Wechsel per DHCP verliert den
* Namen dann nicht. */
function renameSatDevice(satId, mac, ip, current) {
const name = prompt(
async function renameSatDevice(satId, mac, ip, current) {
const name = await uiPrompt(
'Name für ' + (ip || mac) + '\n\n'
+ 'ARIA spricht das Gerät danach so an. Leer lassen = Namen entfernen.',
current || '');
@@ -6354,17 +6418,17 @@
});
loadTriggers();
} catch (e) {
alert('Toggle fehlgeschlagen: ' + e.message);
uiAlert('Toggle fehlgeschlagen: ' + e.message);
}
}
async function deleteTrigger(name) {
if (!confirm(`Trigger "${name}" wirklich löschen?`)) return;
if (!await uiConfirm(`Trigger "${name}" wirklich löschen?`)) return;
try {
await fetch('/api/brain/triggers/' + encodeURIComponent(name), { method: 'DELETE' });
loadTriggers();
} catch (e) {
alert('Löschen fehlgeschlagen: ' + e.message);
uiAlert('Löschen fehlgeschlagen: ' + e.message);
}
}
@@ -6423,7 +6487,7 @@
/** Edit-Modus: Modal mit existierenden Trigger-Werten fuellen. */
async function openTriggerEdit(name) {
const t = triggersCache.find(x => x.name === name);
if (!t) { alert('Trigger nicht in cache, lade neu...'); loadTriggers(); return; }
if (!t) { uiAlert('Trigger nicht in cache, lade neu...'); loadTriggers(); return; }
editingTriggerName = name;
document.getElementById('trigger-modal-title').textContent = 'Trigger bearbeiten — ' + name;
document.getElementById('trigger-modal-save-btn').textContent = 'Speichern';
@@ -6725,7 +6789,7 @@
if (stat) { stat.textContent = 'Fehler ' + r.status; stat.style.color = '#FF6B6B'; }
}
} catch (e) {
alert('Speichern fehlgeschlagen: ' + e.message);
uiAlert('Speichern fehlgeschlagen: ' + e.message);
}
}
@@ -6760,7 +6824,7 @@
}
async function rollbackSkillVersion(name, versionId) {
if (!confirm(`Skill "${name}" auf Version ${versionId} zurückrollen?\n\nDer aktuelle Stand wird vorher automatisch gesichert (safety-snapshot).`)) return;
if (!await uiConfirm(`Skill "${name}" auf Version ${versionId} zurückrollen?\n\nDer aktuelle Stand wird vorher automatisch gesichert (safety-snapshot).`)) return;
try {
const r = await fetch('/api/brain/skills/' + encodeURIComponent(name) + '/rollback', {
method: 'POST', headers: {'Content-Type':'application/json'},
@@ -6768,24 +6832,24 @@
});
const d = await r.json();
if (r.ok) {
alert(`✓ Rollback OK\nSicherheits-Snapshot: ${d.safety_snapshot}`);
uiAlert(`✓ Rollback OK\nSicherheits-Snapshot: ${d.safety_snapshot}`);
loadSkillVersionsSection(name);
loadSkills();
} else {
alert('Rollback fehlgeschlagen: ' + (d.detail || JSON.stringify(d)));
uiAlert('Rollback fehlgeschlagen: ' + (d.detail || JSON.stringify(d)));
}
} catch (e) { alert('Rollback-Fehler: ' + e.message); }
} catch (e) { uiAlert('Rollback-Fehler: ' + e.message); }
}
async function deleteSkillVersion(name, versionId) {
if (!confirm(`Version ${versionId} von "${name}" wirklich löschen?\n\nNicht rückholbar.`)) return;
if (!await uiConfirm(`Version ${versionId} von "${name}" wirklich löschen?\n\nNicht rückholbar.`)) return;
try {
const r = await fetch('/api/brain/skills/' + encodeURIComponent(name) + '/versions/' + encodeURIComponent(versionId), {
method: 'DELETE',
});
if (r.ok) loadSkillVersionsSection(name);
else { const d = await r.json().catch(()=>({})); alert('Löschen fehlgeschlagen: ' + (d.detail || r.status)); }
} catch (e) { alert('Fehler: ' + e.message); }
else { const d = await r.json().catch(()=>({})); uiAlert('Löschen fehlgeschlagen: ' + (d.detail || r.status)); }
} catch (e) { uiAlert('Fehler: ' + e.message); }
}
async function toggleSkillActive(name, newActive) {
@@ -6797,29 +6861,29 @@
});
loadSkills();
} catch (e) {
alert('Toggle fehlgeschlagen: ' + e.message);
uiAlert('Toggle fehlgeschlagen: ' + e.message);
}
}
async function deleteSkill(name) {
if (!confirm(`Skill "${name}" wirklich löschen?\n\nFolder, venv, Logs — alles weg.`)) return;
if (!await uiConfirm(`Skill "${name}" wirklich löschen?\n\nFolder, venv, Logs — alles weg.`)) return;
try {
await fetch('/api/brain/skills/' + encodeURIComponent(name), { method: 'DELETE' });
skillExpanded.delete(name);
loadSkills();
} catch (e) {
alert('Löschen fehlgeschlagen: ' + e.message);
uiAlert('Löschen fehlgeschlagen: ' + e.message);
}
}
async function runSkillPrompt(name) {
const argsStr = prompt(`Skill "${name}" ausführen.\n\nArgs als JSON (oder leer):`, '{}');
const argsStr = await uiPrompt(`Skill "${name}" ausführen.\n\nArgs als JSON (oder leer):`, '{}');
if (argsStr === null) return;
let args = {};
try {
args = JSON.parse(argsStr || '{}');
} catch (e) {
alert('Ungültiges JSON: ' + e.message);
uiAlert('Ungültiges JSON: ' + e.message);
return;
}
try {
@@ -6830,11 +6894,11 @@
});
const d = await r.json();
const summary = `${d.ok ? '✓ OK' : '✗ FEHLER'} · ${d.duration_sec}s · exit=${d.exit_code}\n\nstdout:\n${(d.stdout||'').slice(0,1500)}\n\nstderr:\n${(d.stderr||'').slice(0,500)}`;
alert(summary);
uiAlert(summary);
if (skillExpanded.has(name)) toggleSkillExpand(name); // refresh logs by collapse+expand
loadSkills();
} catch (e) {
alert('Run fehlgeschlagen: ' + e.message);
uiAlert('Run fehlgeschlagen: ' + e.message);
}
}
@@ -6845,7 +6909,7 @@
async function importSkillFile(event) {
const file = event.target.files[0];
if (!file) return;
if (!confirm(`Skill aus "${file.name}" importieren?\n\nFalls schon vorhanden mit gleichem Namen, wird er überschrieben.`)) {
if (!await uiConfirm(`Skill aus "${file.name}" importieren?\n\nFalls schon vorhanden mit gleichem Namen, wird er überschrieben.`)) {
event.target.value = ''; return;
}
try {
@@ -6856,13 +6920,13 @@
});
const d = await r.json();
if (r.ok) {
alert(`✓ Skill "${d.imported?.name}" importiert.`);
uiAlert(`✓ Skill "${d.imported?.name}" importiert.`);
loadSkills();
} else {
alert('Import fehlgeschlagen: ' + (d.detail || JSON.stringify(d)));
uiAlert('Import fehlgeschlagen: ' + (d.detail || JSON.stringify(d)));
}
} catch (e) {
alert('Import fehlgeschlagen: ' + e.message);
uiAlert('Import fehlgeschlagen: ' + e.message);
} finally {
event.target.value = '';
}
@@ -7027,7 +7091,7 @@
if (f) f.projectId = projectId || '';
renderFilesList();
} catch (e) {
alert('Zuordnung fehlgeschlagen: ' + e.message);
uiAlert('Zuordnung fehlgeschlagen: ' + e.message);
renderFilesList(); // Dropdown auf alten Wert zuruecksetzen
}
}
@@ -7086,7 +7150,7 @@
}
async function restoreVersion(rel, hash) {
if (!confirm(`Diese Version (${hash.slice(0,7)}) als aktive Version setzen?\n\nDie aktuelle Version bleibt rollback-bar in der Historie.`)) return;
if (!await uiConfirm(`Diese Version (${hash.slice(0,7)}) als aktive Version setzen?\n\nDie aktuelle Version bleibt rollback-bar in der Historie.`)) return;
try {
const r = await fetch('/api/files-version-restore', {
method: 'POST',
@@ -7099,7 +7163,7 @@
showVersions(rel);
loadFiles();
} catch (e) {
alert('Restore fehlgeschlagen: ' + e.message);
uiAlert('Restore fehlgeschlagen: ' + e.message);
}
}
@@ -7131,14 +7195,14 @@
document.body.appendChild(a); a.click();
setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 100);
} catch (e) {
alert('ZIP-Download fehlgeschlagen: ' + e.message);
uiAlert('ZIP-Download fehlgeschlagen: ' + e.message);
}
}
async function deleteSelected() {
const paths = [...filesSelected];
if (!paths.length) return;
if (!confirm(`${paths.length} Datei(en) wirklich löschen?\n\nIn allen Chat-Bubbles werden sie als gelöscht markiert.`)) return;
if (!await uiConfirm(`${paths.length} Datei(en) wirklich löschen?\n\nIn allen Chat-Bubbles werden sie als gelöscht markiert.`)) return;
try {
const r = await fetch('/api/files-delete-batch', {
method: 'POST',
@@ -7150,10 +7214,10 @@
filesSelected.clear();
loadFiles();
} else {
alert('Bulk-Delete fehlgeschlagen: ' + (d.error || 'unbekannt'));
uiAlert('Bulk-Delete fehlgeschlagen: ' + (d.error || 'unbekannt'));
}
} catch (e) {
alert('Bulk-Delete fehlgeschlagen: ' + e.message);
uiAlert('Bulk-Delete fehlgeschlagen: ' + e.message);
}
}
@@ -7168,7 +7232,7 @@
}
async function deleteFile(p, name) {
if (!confirm(`Datei "${name}" wirklich löschen?\n\nIn allen Chat-Bubbles wird sie als gelöscht markiert.`)) return;
if (!await uiConfirm(`Datei "${name}" wirklich löschen?\n\nIn allen Chat-Bubbles wird sie als gelöscht markiert.`)) return;
try {
const r = await fetch('/api/files-delete', {
method: 'POST',
@@ -7180,10 +7244,10 @@
loadFiles();
// Server broadcastet file_deleted Event → markFileDeletedInChat() updated bubbles
} else {
alert('Löschen fehlgeschlagen: ' + d.error);
uiAlert('Löschen fehlgeschlagen: ' + d.error);
}
} catch (e) {
alert('Löschen fehlgeschlagen: ' + e.message);
uiAlert('Löschen fehlgeschlagen: ' + e.message);
}
}
@@ -7354,17 +7418,17 @@
listEl.innerHTML = `<div style="color:#FF6B6B;">Fehler beim Laden: ${_ofmt(e.message)}</div>`;
}
}
function openOAuthCustomDialog() {
const name = (prompt('Service-Name (z.B. dropbox, discord) — a-z 0-9 _ -:') || '').trim().toLowerCase();
async function openOAuthCustomDialog() {
const name = (await uiPrompt('Service-Name (z.B. dropbox, discord) — a-z 0-9 _ -:') || '').trim().toLowerCase();
if (!name || !/^[a-z0-9_-]+$/.test(name)) {
if (name) alert('Ungueltiger Name. Erlaubt: a-z 0-9 _ -');
if (name) uiAlert('Ungueltiger Name. Erlaubt: a-z 0-9 _ -');
return;
}
const authUrl = (prompt(`auth_url fuer ${name}:`, 'https://') || '').trim();
const authUrl = (await uiPrompt(`auth_url fuer ${name}:`, 'https://') || '').trim();
if (!authUrl) return;
const tokenUrl = (prompt(`token_url fuer ${name}:`, 'https://') || '').trim();
const tokenUrl = (await uiPrompt(`token_url fuer ${name}:`, 'https://') || '').trim();
if (!tokenUrl) return;
const scopesRaw = (prompt(`scopes (space-separated, optional):`, '') || '').trim();
const scopesRaw = (await uiPrompt(`scopes (space-separated, optional):`, '') || '').trim();
const scopes = scopesRaw ? scopesRaw.split(/\s+/).filter(Boolean) : undefined;
fetch('/api/brain/oauth/apps', {
method: 'POST',
@@ -7373,19 +7437,19 @@
})
.then(r => r.ok ? r.json() : r.text().then(t => Promise.reject(new Error(t))))
.then(() => loadOAuthServices())
.catch(e => alert('Custom-Service anlegen fehlgeschlagen: ' + e.message));
.catch(e => uiAlert('Custom-Service anlegen fehlgeschlagen: ' + e.message));
}
async function deleteOAuthApp(service) {
if (!confirm(`Service "${service}" komplett entfernen? client_id/secret + Token werden geloescht.`)) return;
if (!await uiConfirm(`Service "${service}" komplett entfernen? client_id/secret + Token werden geloescht.`)) return;
try {
const r = await fetch('/api/brain/oauth/apps/' + encodeURIComponent(service), { method: 'DELETE' });
if (!r.ok) {
alert('Loeschen fehlgeschlagen: ' + (await r.text()));
uiAlert('Loeschen fehlgeschlagen: ' + (await r.text()));
return;
}
loadOAuthServices();
} catch (e) {
alert('Loeschen fehlgeschlagen: ' + e.message);
uiAlert('Loeschen fehlgeschlagen: ' + e.message);
}
}
async function saveOAuthApp(service) {
@@ -7395,7 +7459,7 @@
const tokenUrl = (document.getElementById('oauth-tok-' + service)?.value || '').trim();
const scopesRaw = (document.getElementById('oauth-scopes-' + service)?.value || '').trim();
if (!cid) {
alert('client_id darf nicht leer sein.');
uiAlert('client_id darf nicht leer sein.');
return;
}
const body = { service, client_id: cid, client_secret: sec };
@@ -7410,12 +7474,12 @@
});
if (!r.ok) {
const t = await r.text();
alert('Speichern fehlgeschlagen: ' + t);
uiAlert('Speichern fehlgeschlagen: ' + t);
return;
}
loadOAuthServices();
} catch (e) {
alert('Speichern fehlgeschlagen: ' + e.message);
uiAlert('Speichern fehlgeschlagen: ' + e.message);
}
}
async function authorizeOAuth(service) {
@@ -7427,7 +7491,7 @@
});
if (!r.ok) {
const t = await r.text();
alert('Authorize fehlgeschlagen: ' + t);
uiAlert('Authorize fehlgeschlagen: ' + t);
return;
}
const data = await r.json();
@@ -7436,39 +7500,39 @@
// Status nach ein paar Sekunden refreshen — Provider redirect → RVS → Brain
setTimeout(loadOAuthServices, 8000);
} catch (e) {
alert('Authorize fehlgeschlagen: ' + e.message);
uiAlert('Authorize fehlgeschlagen: ' + e.message);
}
}
async function revokeOAuth(service) {
if (!confirm(`Token fuer ${service} wirklich loeschen? ARIA muss danach neu autorisiert werden.`)) return;
if (!await uiConfirm(`Token fuer ${service} wirklich loeschen? ARIA muss danach neu autorisiert werden.`)) return;
try {
const r = await fetch('/api/brain/oauth/' + service + '/revoke', { method: 'POST' });
if (!r.ok) {
const t = await r.text();
alert('Revoke fehlgeschlagen: ' + t);
uiAlert('Revoke fehlgeschlagen: ' + t);
return;
}
loadOAuthServices();
} catch (e) {
alert('Revoke fehlgeschlagen: ' + e.message);
uiAlert('Revoke fehlgeschlagen: ' + e.message);
}
}
async function distillNow() {
if (!confirm('Destillat manuell auslösen?\n\nDie ältesten Turns werden zu fact-Memories verdichtet — kostet einen Claude-Call.')) return;
if (!await uiConfirm('Destillat manuell auslösen?\n\nDie ältesten Turns werden zu fact-Memories verdichtet — kostet einen Claude-Call.')) return;
try {
const r = await fetch('/api/brain/conversation/distill', { method: 'POST' });
const d = await r.json();
alert(`Destillat: ${d.distilled || 0} Facts geschrieben, ${d.removed_turns || 0} Turns entfernt.${d.error ? '\nFehler: ' + d.error : ''}`);
uiAlert(`Destillat: ${d.distilled || 0} Facts geschrieben, ${d.removed_turns || 0} Turns entfernt.${d.error ? '\nFehler: ' + d.error : ''}`);
loadBrainStatus();
loadBrainMemoryList();
} catch (e) {
alert('Destillat fehlgeschlagen: ' + e.message);
uiAlert('Destillat fehlgeschlagen: ' + e.message);
}
}
async function resetConversation() {
if (!confirm('Konversation komplett zurücksetzen?\n\n• ARIAs Rolling-Window (Brain) wird geleert — sie "vergisst" die letzten Turns\n• Chat-Anzeige in der Diagnostic wird geleert\n\nDestillierte Facts + andere Memories bleiben in der Vector-DB.')) return;
if (!await uiConfirm('Konversation komplett zurücksetzen?\n\n• ARIAs Rolling-Window (Brain) wird geleert — sie "vergisst" die letzten Turns\n• Chat-Anzeige in der Diagnostic wird geleert\n\nDestillierte Facts + andere Memories bleiben in der Vector-DB.')) return;
try {
// Beides parallel — Brain Window + Diagnostic chat_backup
const [brainR, histR] = await Promise.all([
@@ -7484,10 +7548,10 @@
if (fsBox) fsBox.innerHTML = '';
loadBrainStatus();
} else {
alert(`Reset teilweise fehlgeschlagen — Brain: ${brainOk ? 'OK' : 'fail'}, History: ${histOk ? 'OK' : 'fail'}`);
uiAlert(`Reset teilweise fehlgeschlagen — Brain: ${brainOk ? 'OK' : 'fail'}, History: ${histOk ? 'OK' : 'fail'}`);
}
} catch (e) {
alert('Reset fehlgeschlagen: ' + e.message);
uiAlert('Reset fehlgeschlagen: ' + e.message);
}
}
@@ -8142,14 +8206,14 @@
const win = window.open('', '_blank');
if (!win) {
alert('Popup blockiert — bitte Popups für Diagnostic erlauben und nochmal klicken.');
uiAlert('Popup blockiert — bitte Popups für Diagnostic erlauben und nochmal klicken.');
return;
}
win.document.open();
win.document.write(html);
win.document.close();
} catch (e) {
alert('Druckansicht konnte nicht geladen werden: ' + e.message);
uiAlert('Druckansicht konnte nicht geladen werden: ' + e.message);
}
}
@@ -8282,7 +8346,7 @@
const id = document.getElementById('memory-edit-id').value;
if (!id) return;
const name = decodeURIComponent(filenameEncoded);
if (!confirm(`Anhang "${name}" wirklich loeschen?`)) return;
if (!await uiConfirm(`Anhang "${name}" wirklich loeschen?`)) return;
try {
const r = await fetch(`/api/brain/memory/${encodeURIComponent(id)}/attachments/${filenameEncoded}`, {
method: 'DELETE',
@@ -8294,7 +8358,7 @@
const status = document.getElementById('memory-attachment-status');
if (status) status.textContent = `✓ "${name}" geloescht`;
} catch (e) {
alert('Loeschen fehlgeschlagen: ' + e.message);
uiAlert('Loeschen fehlgeschlagen: ' + e.message);
}
}
@@ -8348,7 +8412,7 @@
async function deleteMemory(id) {
const m = brainMemoryCache[id];
const label = m ? `"${m.title}" (${m.type})` : id;
if (!confirm(`Memory ${label} wirklich löschen?`)) return;
if (!await uiConfirm(`Memory ${label} wirklich löschen?`)) return;
try {
const r = await fetch('/api/brain/memory/delete/' + encodeURIComponent(id), { method: 'DELETE' });
if (!r.ok) throw new Error('HTTP ' + r.status);
@@ -8366,7 +8430,7 @@
else await loadBrainMemoryList();
loadBrainStatus();
} catch (e) {
alert('Löschen fehlgeschlagen: ' + e.message);
uiAlert('Löschen fehlgeschlagen: ' + e.message);
}
}
@@ -8667,7 +8731,7 @@
// ── Migration aus brain-import/ ────────────────────────
async function runMigration() {
const status = document.getElementById('brain-import-files-status');
if (!confirm('Migration starten?\n\nParst die Markdown-Dateien aus brain-import/ und schreibt sie als pinned Memory-Punkte in die DB. Bei Re-Run werden nur die Migration-Punkte ersetzt — eigene Memories bleiben.')) return;
if (!await uiConfirm('Migration starten?\n\nParst die Markdown-Dateien aus brain-import/ und schreibt sie als pinned Memory-Punkte in die DB. Bei Re-Run werden nur die Migration-Punkte ersetzt — eigene Memories bleiben.')) return;
if (status) status.innerHTML = '⏳ Migriere...';
try {
const r = await fetch('/api/brain/memory/migrate', { method: 'POST' });
@@ -8741,7 +8805,7 @@
const scopeInfo = bScope === 'system' ? 'Nur die aktuell pinned SYSTEM-Regeln werden ersetzt — Persönliches bleibt.'
: bScope === 'personal' ? 'Nur die aktuell pinned PERSÖNLICHEN Memories werden ersetzt — System-Regeln bleiben.'
: 'ALLE aktuell pinned Memories werden überschrieben.';
if (!confirm(`Bootstrap importieren? (scope: ${bScope})\n\n${bundle.memories.length} pinned Memories aus "${file.name}".\n\n${scopeInfo} Cold Memory bleibt unverändert.`)) {
if (!await uiConfirm(`Bootstrap importieren? (scope: ${bScope})\n\n${bundle.memories.length} pinned Memories aus "${file.name}".\n\n${scopeInfo} Cold Memory bleibt unverändert.`)) {
event.target.value = '';
return;
}
@@ -8770,7 +8834,7 @@
const file = event.target.files[0];
if (!file) return;
const statusEl = document.getElementById('brain-import-status');
if (!confirm(`Wirklich importieren? ALLE aktuellen Memories + Skills gehen verloren.\nDatei: ${file.name} (${(file.size/1024/1024).toFixed(1)} MB)`)) {
if (!await uiConfirm(`Wirklich importieren? ALLE aktuellen Memories + Skills gehen verloren.\nDatei: ${file.name} (${(file.size/1024/1024).toFixed(1)} MB)`)) {
event.target.value = '';
return;
}
@@ -8962,7 +9026,7 @@
}
async function unquarantineTurn(ts) {
if (!confirm('Turn wieder in den Verlauf aufnehmen?\n\nWar er der Auslöser, '
if (!await uiConfirm('Turn wieder in den Verlauf aufnehmen?\n\nWar er der Auslöser, '
+ 'blockt Anthropics Filter beim nächsten Mal erneut — dann landet '
+ 'er automatisch wieder hier.')) return;
try {
@@ -8972,11 +9036,11 @@
});
if (!r.ok) throw new Error('HTTP ' + r.status);
loadSafeguard();
} catch (e) { alert('Zurückholen fehlgeschlagen: ' + e.message); }
} catch (e) { uiAlert('Zurückholen fehlgeschlagen: ' + e.message); }
}
async function clearSafeguard() {
if (!confirm('Vorfallsliste leeren?\n\nDanach läuft wieder das Hauptmodell — '
if (!await uiConfirm('Vorfallsliste leeren?\n\nDanach läuft wieder das Hauptmodell — '
+ 'auch in Projekten, die gerade aufs Ausweich-Modell ausweichen. '
+ 'Zurückgeholte Turns bleiben zurückgeholt.')) return;
try {
@@ -8986,7 +9050,7 @@
});
if (!r.ok) throw new Error('HTTP ' + r.status);
loadSafeguard();
} catch (e) { alert('Leeren fehlgeschlagen: ' + e.message); }
} catch (e) { uiAlert('Leeren fehlgeschlagen: ' + e.message); }
}
// ── Datenschutz-Schalter ──────────────────────────────────────────
@@ -9681,11 +9745,11 @@
try {
const j = await (await fetch('/api/llm-catalog/add', { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ hfRepo: repo }) })).json();
if (!j.ok) { btn.disabled = false; btn.textContent = 'In Katalog'; alert('Nicht übernommen: ' + (j.error || 'Fehler')); return; }
if (!j.ok) { btn.disabled = false; btn.textContent = 'In Katalog'; uiAlert('Nicht übernommen: ' + (j.error || 'Fehler')); return; }
_llmCatalog = j.models || _llmCatalog;
renderLlmCatalog();
btn.outerHTML = '<span style="color:#3FFF9F;" title="Im Modell-Katalog Ziel-Karte wählen und „Laden"">im Katalog ✓ — dort „Laden"</span>';
} catch (e) { btn.disabled = false; btn.textContent = 'In Katalog'; alert(e.message); }
} catch (e) { btn.disabled = false; btn.textContent = 'In Katalog'; uiAlert(e.message); }
}
// ── Belege: was hat ARIA fuer eine Antwort wirklich ausgefuehrt? ──
@@ -9735,15 +9799,15 @@
// laden. Programme bekommen den neuen Wert automatisch (ctx in
// llm_cluster_models). Passt es nicht in den Grafikspeicher, setzt die Box
// die Karte auf den alten Stand zurueck.
function onLlmCardCtx(sel) {
async function onLlmCardCtx(sel) {
const target = sel.dataset.box, gpu = sel.dataset.gpu, model = sel.dataset.model;
sel.blur(); // Fokus weg → aufgeschobenes Neuzeichnen holt den Stand nach
const cur = Number(sel.dataset.current || 0), next = Number(sel.value);
if (next === cur) return;
const m = _llmCatalog.find(x => x.id === model);
if (!m || !m.hfRepo) { alert(`${model} steht nicht im Modell-Katalog — Kontext so nicht änderbar.`); sel.value = cur; return; }
if (!m || !m.hfRepo) { uiAlert(`${model} steht nicht im Modell-Katalog — Kontext so nicht änderbar.`); sel.value = cur; return; }
const more = next > cur;
if (!confirm(`${model} auf ${_boxName(target)} · Karte ${gpu} mit ${next} statt ${cur || '?'} Tokens Kontext neu laden?\n\n`
if (!await uiConfirm(`${model} auf ${_boxName(target)} · Karte ${gpu} mit ${next} statt ${cur || '?'} Tokens Kontext neu laden?\n\n`
+ (more ? 'Mehr Kontext braucht mehr Grafikspeicher. Passt es nicht, setzt die Box die Karte von selbst auf den alten Stand zurück.\n\n' : '')
+ 'Die Box startet ihre Modelle dabei kurz neu. Programme bekommen den neuen Wert automatisch.')) { sel.value = cur; return; }
const sl = (workers.find(w => w.instanceId === target)?.slots || []).find(x => String(x.gpu) === String(gpu)) || {};
@@ -9758,13 +9822,13 @@
const node = _boxName(target);
if (next === current) return;
if (!next) {
if (!confirm(`Karte ${gpu} auf ${node} leeren?\n\n${current} wird entladen; ARIA und Programme können es dann nicht mehr wählen. `
if (!await uiConfirm(`Karte ${gpu} auf ${node} leeren?\n\n${current} wird entladen; ARIA und Programme können es dann nicht mehr wählen. `
+ `Die Datei bleibt auf der Box.\n\nDie anderen Karten dieser Box starten dabei kurz neu.`)) { sel.value = current; return; }
send({ action: 'llm_unload_model', targetInstance: target, gpu });
return;
}
const m = _llmCatalog.find(x => x.id === next);
if (!m || !m.hfRepo) { alert(`${next} steht nicht im Modell-Katalog — Herkunft unbekannt.`); sel.value = current; return; }
if (!m || !m.hfRepo) { uiAlert(`${next} steht nicht im Modell-Katalog — Herkunft unbekannt.`); sel.value = current; return; }
const sl = (workers.find(w => w.instanceId === target)?.slots || []).find(x => String(x.gpu) === String(gpu)) || {};
const vramGB = sl.vramMB ? sl.vramMB / 1024 : 0;
const cpuMoe = await askCpuMoe(m, sl.vramMB || 0);
@@ -9772,20 +9836,20 @@
const warn = (!cpuMoe && vramGB && m.sizeGB && m.sizeGB > vramGB * 0.92)
? `\n\n⚠ ${next} ist etwa ${m.sizeGB} GB groß, die Karte hat ${vramGB.toFixed(0)} GB — wahrscheinlich passt es nicht.` : '';
const repl = current ? `\n\nErsetzt ${current} — Programme, die es nutzen, verlieren es.` : '';
if (!confirm(`${next} auf ${node} · Karte ${gpu} laden?${repl}${warn}\n\nDie anderen Karten dieser Box starten dabei kurz neu.`)) { sel.value = current; return; }
if (!await uiConfirm(`${next} auf ${node} · Karte ${gpu} laden?${repl}${warn}\n\nDie anderen Karten dieser Box starten dabei kurz neu.`)) { sel.value = current; return; }
send({ action: 'llm_provision_model', targetInstance: target, gpu, key: m.id, hfRepo: m.hfRepo, quant: m.quant, ctx: m.ctx, cpuMoe });
openLlmDownloads();
}
// MoE: Experten zwischen Karte und Arbeitsspeicher — gleiches Modell neu laden
function onLlmCardMoe(sel) {
async function onLlmCardMoe(sel) {
const target = sel.dataset.box, gpu = sel.dataset.gpu, model = sel.dataset.model;
sel.blur(); // Fokus weg → aufgeschobenes Neuzeichnen holt den Stand nach
const cur = Number(sel.dataset.current || 0), next = Number(sel.value);
const m = _llmCatalog.find(x => x.id === model);
if (!m || !m.hfRepo) { alert(`${model} steht nicht im Modell-Katalog.`); sel.value = String(cur); return; }
if (!m || !m.hfRepo) { uiAlert(`${model} steht nicht im Modell-Katalog.`); sel.value = String(cur); return; }
const txt = { '0': 'alle Experten auf die Karte', '-2': 'automatisch aufteilen (so viele wie passen auf die Karte)', '-1': 'alle Experten in den Arbeitsspeicher' };
if (!confirm(`${model} neu laden: ${txt[String(next)] || next}?\n\nDie Box startet ihre Modelle dabei kurz neu.`)) { sel.value = String(cur); return; }
if (!await uiConfirm(`${model} neu laden: ${txt[String(next)] || next}?\n\nDie Box startet ihre Modelle dabei kurz neu.`)) { sel.value = String(cur); return; }
const sl = (workers.find(w => w.instanceId === target)?.slots || []).find(x => String(x.gpu) === String(gpu)) || {};
send({ action: 'llm_provision_model', targetInstance: target, gpu, key: m.id, hfRepo: m.hfRepo, quant: m.quant, ctx: sl.ctx || m.ctx, cpuMoe: next });
openLlmDownloads();
@@ -9817,8 +9881,8 @@
}).join('');
return `<div style="margin:2px 0 6px 18px;padding:6px 10px;background:#0D0D18;border-left:2px solid #2A2A3E;font-size:11px;">${rows || '<span style="color:#555570;">Keine LLM-Karte konfiguriert (LLM_GPUS in der .env der Box).</span>'}</div>`;
}
function unloadLlmCard(instanceId, gpu, model) {
if (!confirm(`Karte ${gpu} leeren?\n\n${model} wird entladen; Clients können es danach nicht mehr wählen. `
async function unloadLlmCard(instanceId, gpu, model) {
if (!await uiConfirm(`Karte ${gpu} leeren?\n\n${model} wird entladen; Clients können es danach nicht mehr wählen. `
+ `Die Datei bleibt auf der Box — erneutes Laden geht ohne Download.\n\n`
+ `Die anderen Karten dieser Box starten dabei kurz neu (wenige Sekunden).`)) return;
send({ action: 'llm_unload_model', targetInstance: instanceId, gpu });
@@ -9874,18 +9938,18 @@
});
}
function deleteLlmFiles(instanceId, i) {
async function deleteLlmFiles(instanceId, i) {
const m = _llmCatalog[i];
if (!m) return;
const b = onlineLlmBoxes().find(w => w.instanceId === instanceId);
const d = ((b && b.disk && b.disk.models) || []).find(x => x.repo === m.hfRepo);
if (!confirm(`${m.id} von ${(b && b.node) || instanceId} löschen?\n\n`
if (!await uiConfirm(`${m.id} von ${(b && b.node) || instanceId} löschen?\n\n`
+ `Die heruntergeladene Datei (${d ? _fmtBytes(d.bytes) : '?'}, Repo ${m.hfRepo}) wird von der Box entfernt. `
+ `Wer es wieder will, muss es neu herunterladen.`)) return;
send({ action: 'llm_remove_model', targetInstance: instanceId, key: m.id, hfRepo: m.hfRepo, deleteFiles: true });
}
function cancelLlmJob(instanceId, key) {
if (!confirm(`Laden von ${key} abbrechen?\n\nDer halbe Download wird gelöscht, die Karte bleibt bzw. wird wie vorher.`)) return;
async function cancelLlmJob(instanceId, key) {
if (!await uiConfirm(`Laden von ${key} abbrechen?\n\nDer halbe Download wird gelöscht, die Karte bleibt bzw. wird wie vorher.`)) return;
send({ action: 'llm_remove_model', targetInstance: instanceId, key, cancel: true });
}
@@ -9896,11 +9960,11 @@
const [target, gpu] = String((sel && sel.value) || '').split('|');
if (!target) return;
const tgt = llmCardTargets().find(t => t.value === sel.value);
if (tgt && tgt.slot && tgt.slot.model === m.id) { alert(`${m.id} liegt schon auf dieser Karte.`); return; }
if (tgt && tgt.slot && tgt.slot.model === m.id) { uiAlert(`${m.id} liegt schon auf dieser Karte.`); return; }
const busyBox = ((tgt && tgt.box.slots) || []).find(sl => sl.model === m.id);
if (busyBox) { alert(`${m.id} liegt auf dieser Box schon auf Karte ${busyBox.gpu}. Ein Modell pro Box nur einmal.`); return; }
if (busyBox) { uiAlert(`${m.id} liegt auf dieser Box schon auf Karte ${busyBox.gpu}. Ein Modell pro Box nur einmal.`); return; }
if (tgt && tgt.slot && tgt.slot.model &&
!confirm(`Karte ${gpu} auf ${tgt.box.node} hält gerade ${tgt.slot.model}.\n\n`
!await uiConfirm(`Karte ${gpu} auf ${tgt.box.node} hält gerade ${tgt.slot.model}.\n\n`
+ `${m.id} ersetzt es. Clients, die ${tgt.slot.model} nutzen, verlieren es dann.\n`
+ `Die anderen Karten dieser Box starten dabei kurz neu.\n\nErsetzen?`)) return;
const cpuMoe = await askCpuMoe(m, (tgt && tgt.slot && tgt.slot.vramMB) || 0);
@@ -9974,10 +10038,10 @@
// den Klick, wenn der Browser Dialoge unterdrueckt hatte ("Verhindern,
// dass diese Seite weitere Dialoge erstellt") → confirm()===false → es ging
// NICHTS raus. Nur "aggressive" (Volumes!) fragt noch nach.
function runDiskCleanup(variant) {
async function runDiskCleanup(variant) {
const aggressive = variant === 'aggressive';
if (aggressive) {
const ok = confirm('AGGRESSIV aufraeumen? Loescht zusaetzlich ungenutzte Volumes — nur wenn ALLE ARIA-Container laufen, sonst Datenverlust!');
const ok = await uiConfirm('AGGRESSIV aufraeumen? Loescht zusaetzlich ungenutzte Volumes — nur wenn ALLE ARIA-Container laufen, sonst Datenverlust!');
if (!ok) return;
}
const btn = document.getElementById('disk-clean-btn');
@@ -9994,7 +10058,7 @@
btn.textContent = 'Kopiert!';
setTimeout(() => { btn.textContent = old; }, 1500);
}).catch(() => {
alert('Kopieren fehlgeschlagen — Befehl: ' + cmd);
uiAlert('Kopieren fehlgeschlagen — Befehl: ' + cmd);
});
}