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). // project_changed an App + andere Tabs).
await refreshDiagProjectsCache(); await refreshDiagProjectsCache();
} catch (e) { } 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') { if (msg.type === 'voice_id_delete_response') {
const p = msg.payload || msg; const p = msg.payload || msg;
if (p.removed) { 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 { } else {
alert('Es war kein Fingerprint vorhanden.'); uiAlert('Es war kein Fingerprint vorhanden.');
} }
refreshVoiceIdStatus(); refreshVoiceIdStatus();
loadLlmCatalog(); loadLlmCatalog();
@@ -3195,7 +3195,7 @@
* Backend (Bridge) raeumt chat_backup.jsonl + Brain-Conversation * Backend (Bridge) raeumt chat_backup.jsonl + Brain-Conversation
* und broadcastet danach chat_message_deleted — wir entfernen die * und broadcastet danach chat_message_deleted — wir entfernen die
* Bubble lokal erst dann, nicht optimistisch. */ * Bubble lokal erst dann, nicht optimistisch. */
function deleteDiagBubble(ts) { async function deleteDiagBubble(ts) {
if (!ts) return; if (!ts) return;
let preview = ''; let preview = '';
for (const box of [chatBox, document.getElementById('chat-box-fs')]) { for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
@@ -3203,7 +3203,7 @@
const el = box.querySelector(`.chat-msg[data-ts="${ts}"]`); const el = box.querySelector(`.chat-msg[data-ts="${ts}"]`);
if (el) { preview = (el.textContent || '').slice(0, 80); break; } 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 }); send({ action: 'delete_chat_message', ts });
} }
@@ -3619,7 +3619,7 @@
diagModelNames = diagCtxModels.names || {}; diagModelNames = diagCtxModels.names || {};
renderContextStrip(); renderContextStrip();
renderCtxModelBar(); renderCtxModelBar();
} catch (e) { alert('Modell setzen fehlgeschlagen: ' + e.message); } } catch (e) { uiAlert('Modell setzen fehlgeschlagen: ' + e.message); }
} }
async function ctxModelBack() { async function ctxModelBack() {
@@ -3634,7 +3634,7 @@
renderContextStrip(); renderContextStrip();
renderCtxModelBar(); renderCtxModelBar();
try { loadSafeguard(); } catch {} 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. // Untertitel der Kontext-Karten: welches Modell der naechste Turn nutzt.
@@ -4105,9 +4105,9 @@
if (modal) modal.style.display = 'none'; if (modal) modal.style.display = 'none';
} }
function clearThoughtStream() { async function clearThoughtStream() {
if (thoughtStream.length === 0) return; 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 = []; thoughtStream = [];
lastThoughtKey = ''; lastThoughtKey = '';
updateThoughtsBadge(); updateThoughtsBadge();
@@ -4202,7 +4202,7 @@
const b64 = btoa(bin); const b64 = btoa(bin);
// Name aus Dateinamen ableiten — z.B. "maia.tar.gz" → "maia" // Name aus Dateinamen ableiten — z.B. "maia.tar.gz" → "maia"
const baseName = file.name.replace(/\.tar\.gz$|\.tgz$/i, ''); 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 = ''; event.target.value = '';
return; return;
} }
@@ -4278,8 +4278,8 @@
send({ action: 'voice_id_status' }); send({ action: 'voice_id_status' });
} }
function deleteVoiceId() { async function deleteVoiceId() {
if (!confirm('Voice-ID-Fingerprint loeschen?\n\nDanach muss in der App neu enrolled werden.')) return; if (!await uiConfirm('Voice-ID-Fingerprint loeschen?\n\nDanach muss in der App neu enrolled werden.')) return;
send({ action: 'voice_id_delete' }); send({ action: 'voice_id_delete' });
} }
@@ -4377,7 +4377,7 @@
// Beim Verstecken bleibt der „anzeigen"-Modus wie er ist; beim // Beim Verstecken bleibt der „anzeigen"-Modus wie er ist; beim
// Wieder-Sichtbarmachen sieht man es sofort ohne Umschalten. // Wieder-Sichtbarmachen sieht man es sofort ohne Umschalten.
loadProjects(); loadProjects();
} catch (e) { alert('Verstecken/Anzeigen fehlgeschlagen: ' + e.message); } } catch (e) { uiAlert('Verstecken/Anzeigen fehlgeschlagen: ' + e.message); }
} }
async function setProjectKind(id, kind) { async function setProjectKind(id, kind) {
@@ -4389,7 +4389,7 @@
}); });
if (!r.ok) throw new Error('HTTP ' + r.status); if (!r.ok) throw new Error('HTTP ' + r.status);
loadProjects(); loadProjects();
} catch (e) { alert('Code-Markierung fehlgeschlagen: ' + e.message); } } catch (e) { uiAlert('Code-Markierung fehlgeschlagen: ' + e.message); }
} }
async function switchProject(projectId) { async function switchProject(projectId) {
@@ -4400,23 +4400,23 @@
body: JSON.stringify({ project_id: projectId }), body: JSON.stringify({ project_id: projectId }),
}); });
loadProjects(); loadProjects();
} catch (e) { alert('Wechsel fehlgeschlagen: ' + e.message); } } catch (e) { uiAlert('Wechsel fehlgeschlagen: ' + e.message); }
} }
async function endProject(id, name) { 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 { try {
await fetch(`/api/brain/projects/${encodeURIComponent(id)}/end`, { method: 'POST' }); await fetch(`/api/brain/projects/${encodeURIComponent(id)}/end`, { method: 'POST' });
loadProjects(); loadProjects();
} catch (e) { alert('Beenden fehlgeschlagen: ' + e.message); } } catch (e) { uiAlert('Beenden fehlgeschlagen: ' + e.message); }
} }
async function archiveProject(id, name) { 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 { try {
await fetch(`/api/brain/projects/${encodeURIComponent(id)}/archive`, { method: 'POST' }); await fetch(`/api/brain/projects/${encodeURIComponent(id)}/archive`, { method: 'POST' });
loadProjects(); loadProjects();
} catch (e) { alert('Archivieren fehlgeschlagen: ' + e.message); } } catch (e) { uiAlert('Archivieren fehlgeschlagen: ' + e.message); }
} }
function openCreateProjectModal() { function openCreateProjectModal() {
@@ -4433,7 +4433,7 @@
async function submitCreateProject() { async function submitCreateProject() {
const name = document.getElementById('project-create-name').value.trim(); const name = document.getElementById('project-create-name').value.trim();
const description = document.getElementById('project-create-desc').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 { try {
const r = await fetch('/api/brain/projects/create', { const r = await fetch('/api/brain/projects/create', {
method: 'POST', method: 'POST',
@@ -4458,7 +4458,7 @@
// ein Projekt an und schreibt weiter in den Hauptchat. // ein Projekt an und schreibt weiter in den Hauptchat.
const pid = (d && d.id) ? d.id : ''; const pid = (d && d.id) ? d.id : '';
if (pid) switchDiagFocus(pid); if (pid) switchDiagFocus(pid);
} catch (e) { alert('Anlegen fehlgeschlagen: ' + e.message); } } catch (e) { uiAlert('Anlegen fehlgeschlagen: ' + e.message); }
} }
function escapeHtml(str) { function escapeHtml(str) {
@@ -4468,8 +4468,72 @@
return String(str).replace(/['"\\]/g, '\\$&'); return String(str).replace(/['"\\]/g, '\\$&');
} }
function deleteXttsVoice(name) { // ── Eigene Dialoge statt alert/confirm/prompt ─────────────────────
if (!confirm(`Stimme "${name}" endgueltig loeschen?`)) return; // 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 }); send({ action: 'xtts_delete_voice', name });
// Bei aktueller Auswahl: auf Default zuruecksetzen // Bei aktueller Auswahl: auf Default zuruecksetzen
const sel = document.getElementById('diag-xtts-voice'); const sel = document.getElementById('diag-xtts-voice');
@@ -4498,9 +4562,9 @@
async function uploadVoiceSamples() { async function uploadVoiceSamples() {
const name = document.getElementById('xtts-clone-name').value.trim(); const name = document.getElementById('xtts-clone-name').value.trim();
const files = document.getElementById('xtts-clone-files').files; const files = document.getElementById('xtts-clone-files').files;
if (!name) { alert('Bitte einen Namen eingeben'); return; } if (!name) { uiAlert('Bitte einen Namen eingeben'); return; }
if (!files || files.length === 0) { alert('Bitte Audio-Dateien auswaehlen'); return; } if (!files || files.length === 0) { uiAlert('Bitte Audio-Dateien auswaehlen'); return; }
if (files.length > 10) { alert('Maximal 10 Dateien'); return; } if (files.length > 10) { uiAlert('Maximal 10 Dateien'); return; }
const status = document.getElementById('xtts-clone-status'); const status = document.getElementById('xtts-clone-status');
status.textContent = `Lade ${files.length} Datei(en)...`; status.textContent = `Lade ${files.length} Datei(en)...`;
@@ -4592,7 +4656,7 @@
const text = await file.text(); const text = await file.text();
// Sanity-Check: muss JSON sein // Sanity-Check: muss JSON sein
JSON.parse(text); 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 = ''; event.target.value = '';
return; return;
} }
@@ -4603,13 +4667,13 @@
}); });
const d = await r.json(); const d = await r.json();
if (d.ok) { if (d.ok) {
alert('✓ ' + d.message); uiAlert('✓ ' + d.message);
send({ action: 'get_voice_config' }); send({ action: 'get_voice_config' });
} else { } else {
alert('Import fehlgeschlagen: ' + d.error); uiAlert('Import fehlgeschlagen: ' + d.error);
} }
} catch (e) { } catch (e) {
alert('Ungültige Datei: ' + e.message); uiAlert('Ungültige Datei: ' + e.message);
} finally { } finally {
event.target.value = ''; event.target.value = '';
} }
@@ -4618,8 +4682,8 @@
// ── Wipe All — Komplett-Reset ────────────────────────── // ── Wipe All — Komplett-Reset ──────────────────────────
async function wipeAll() { async function wipeAll() {
const statusEl = document.getElementById('wipe-status'); 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 (!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 (!confirm('Letzte Warnung: Das KANN NICHT rückgängig gemacht werden.\n\nFortfahren?')) return; if (!await uiConfirm('Letzte Warnung: Das KANN NICHT rückgängig gemacht werden.\n\nFortfahren?')) return;
statusEl.innerHTML = '⏳ Stoppe Container, lösche Daten...'; statusEl.innerHTML = '⏳ Stoppe Container, lösche Daten...';
try { try {
const r = await fetch('/api/wipe-all', { method: 'POST' }); const r = await fetch('/api/wipe-all', { method: 'POST' });
@@ -4638,7 +4702,7 @@
// ── Generischer Container-Restart (Bridge/Brain/Qdrant) ────── // ── Generischer Container-Restart (Bridge/Brain/Qdrant) ──────
async function restartContainer(name) { async function restartContainer(name) {
const statusEl = document.getElementById('restart-status'); 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}...`; if (statusEl) statusEl.innerHTML = `⏳ Restart ${name}...`;
try { try {
const r = await fetch('/api/container-restart', { 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>`; return `<div style="color:#AAAACC;">${time} <span>${_ariaEsc(kind)}: ${_ariaEsc(JSON.stringify(p).slice(0, 500))}</span></div>`;
} }
function ariaPanicStop() { async function ariaPanicStop() {
if (!confirm('Wirklich NOT-AUS? Alle aktiven Claude-Subprocesses werden sofort gekillt.')) return; if (!await uiConfirm('Wirklich NOT-AUS? Alle aktiven Claude-Subprocesses werden sofort gekillt.')) return;
send({ action: 'aria_panic_stop' }); send({ action: 'aria_panic_stop' });
_ariaPushLine( _ariaPushLine(
`<span style="color:#FF3B30;font-weight:bold;">━━━ ${_ariaTimePrefix()} ⛔ NOT-AUS ausgeloest ━━━</span>`, `<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 /** Entfernt einen Agenten aus der Liste (z.B. den alten Namen nach dem
* Umbenennen). Läuft er noch, meldet er sich von selbst wieder. */ * Umbenennen). Läuft er noch, meldet er sich von selbst wieder. */
function forgetHost(hostId, name) { async function forgetHost(hostId, name) {
if (!confirm('"' + name + '" aus der Liste entfernen?\n\n' + if (!await uiConfirm('"' + name + '" aus der Liste entfernen?\n\n' +
'Das stoppt den Agenten NICHT — läuft er noch, erscheint er ' + 'Das stoppt den Agenten NICHT — läuft er noch, erscheint er ' +
'beim nächsten Lebenszeichen wieder.')) return; 'beim nächsten Lebenszeichen wieder.')) return;
send({ action: 'host_forget', hostId }); send({ action: 'host_forget', hostId });
@@ -5756,7 +5820,7 @@
if (pw) body.password = pw; if (pw) body.password = pw;
if (spw) body.smtp_password = spw; if (spw) body.smtp_password = spw;
if (body.imap_encryption === 'none' || body.smtp_encryption === 'none') { 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 …'); _mailStatus('Speichere …');
try { try {
@@ -5809,9 +5873,9 @@
renderJellyfin(); renderJellyfin();
} }
function removeJellyfinServer(i) { async function removeJellyfinServer(i) {
const s = jellyfinServers[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); jellyfinServers.splice(i, 1);
if (jellyfinServers.length && !jellyfinServers.some(x => x.isDefault)) { if (jellyfinServers.length && !jellyfinServers.some(x => x.isDefault)) {
jellyfinServers[0].isDefault = true; jellyfinServers[0].isDefault = true;
@@ -6110,9 +6174,9 @@
* sofort und fuer jeden schon ausgerollten Agenten. Die Bridge loest ihn * sofort und fuer jeden schon ausgerollten Agenten. Die Bridge loest ihn
* beim Ansprechen wieder auf, damit sich der Agent weiter angesprochen * beim Ansprechen wieder auf, damit sich der Agent weiter angesprochen
* fuehlt. */ * fuehlt. */
function renameEntity(kind, id, currentAlias, ownName) { async function renameEntity(kind, id, currentAlias, ownName) {
const was = kind === 'satellite' ? 'Satellit' : 'Agent'; const was = kind === 'satellite' ? 'Satellit' : 'Agent';
const name = prompt( const name = await uiPrompt(
was + ' umbenennen\n\n' was + ' umbenennen\n\n'
+ 'Heißt sich selbst: ' + ownName + '\n' + 'Heißt sich selbst: ' + ownName + '\n'
+ 'ARIA spricht ihn danach unter dem neuen Namen an.\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. * ("mach mir Port 5000 von duffy-desktop auf") statt ueber eine IP.
* Gespeichert wird an der MAC — ein IP-Wechsel per DHCP verliert den * Gespeichert wird an der MAC — ein IP-Wechsel per DHCP verliert den
* Namen dann nicht. */ * Namen dann nicht. */
function renameSatDevice(satId, mac, ip, current) { async function renameSatDevice(satId, mac, ip, current) {
const name = prompt( const name = await uiPrompt(
'Name für ' + (ip || mac) + '\n\n' 'Name für ' + (ip || mac) + '\n\n'
+ 'ARIA spricht das Gerät danach so an. Leer lassen = Namen entfernen.', + 'ARIA spricht das Gerät danach so an. Leer lassen = Namen entfernen.',
current || ''); current || '');
@@ -6354,17 +6418,17 @@
}); });
loadTriggers(); loadTriggers();
} catch (e) { } catch (e) {
alert('Toggle fehlgeschlagen: ' + e.message); uiAlert('Toggle fehlgeschlagen: ' + e.message);
} }
} }
async function deleteTrigger(name) { async function deleteTrigger(name) {
if (!confirm(`Trigger "${name}" wirklich löschen?`)) return; if (!await uiConfirm(`Trigger "${name}" wirklich löschen?`)) return;
try { try {
await fetch('/api/brain/triggers/' + encodeURIComponent(name), { method: 'DELETE' }); await fetch('/api/brain/triggers/' + encodeURIComponent(name), { method: 'DELETE' });
loadTriggers(); loadTriggers();
} catch (e) { } 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. */ /** Edit-Modus: Modal mit existierenden Trigger-Werten fuellen. */
async function openTriggerEdit(name) { async function openTriggerEdit(name) {
const t = triggersCache.find(x => x.name === 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; editingTriggerName = name;
document.getElementById('trigger-modal-title').textContent = 'Trigger bearbeiten — ' + name; document.getElementById('trigger-modal-title').textContent = 'Trigger bearbeiten — ' + name;
document.getElementById('trigger-modal-save-btn').textContent = 'Speichern'; document.getElementById('trigger-modal-save-btn').textContent = 'Speichern';
@@ -6725,7 +6789,7 @@
if (stat) { stat.textContent = 'Fehler ' + r.status; stat.style.color = '#FF6B6B'; } if (stat) { stat.textContent = 'Fehler ' + r.status; stat.style.color = '#FF6B6B'; }
} }
} catch (e) { } catch (e) {
alert('Speichern fehlgeschlagen: ' + e.message); uiAlert('Speichern fehlgeschlagen: ' + e.message);
} }
} }
@@ -6760,7 +6824,7 @@
} }
async function rollbackSkillVersion(name, versionId) { 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 { try {
const r = await fetch('/api/brain/skills/' + encodeURIComponent(name) + '/rollback', { const r = await fetch('/api/brain/skills/' + encodeURIComponent(name) + '/rollback', {
method: 'POST', headers: {'Content-Type':'application/json'}, method: 'POST', headers: {'Content-Type':'application/json'},
@@ -6768,24 +6832,24 @@
}); });
const d = await r.json(); const d = await r.json();
if (r.ok) { if (r.ok) {
alert(`✓ Rollback OK\nSicherheits-Snapshot: ${d.safety_snapshot}`); uiAlert(`✓ Rollback OK\nSicherheits-Snapshot: ${d.safety_snapshot}`);
loadSkillVersionsSection(name); loadSkillVersionsSection(name);
loadSkills(); loadSkills();
} else { } 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) { 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 { try {
const r = await fetch('/api/brain/skills/' + encodeURIComponent(name) + '/versions/' + encodeURIComponent(versionId), { const r = await fetch('/api/brain/skills/' + encodeURIComponent(name) + '/versions/' + encodeURIComponent(versionId), {
method: 'DELETE', method: 'DELETE',
}); });
if (r.ok) loadSkillVersionsSection(name); if (r.ok) loadSkillVersionsSection(name);
else { const d = await r.json().catch(()=>({})); alert('Löschen fehlgeschlagen: ' + (d.detail || r.status)); } else { const d = await r.json().catch(()=>({})); uiAlert('Löschen fehlgeschlagen: ' + (d.detail || r.status)); }
} catch (e) { alert('Fehler: ' + e.message); } } catch (e) { uiAlert('Fehler: ' + e.message); }
} }
async function toggleSkillActive(name, newActive) { async function toggleSkillActive(name, newActive) {
@@ -6797,29 +6861,29 @@
}); });
loadSkills(); loadSkills();
} catch (e) { } catch (e) {
alert('Toggle fehlgeschlagen: ' + e.message); uiAlert('Toggle fehlgeschlagen: ' + e.message);
} }
} }
async function deleteSkill(name) { 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 { try {
await fetch('/api/brain/skills/' + encodeURIComponent(name), { method: 'DELETE' }); await fetch('/api/brain/skills/' + encodeURIComponent(name), { method: 'DELETE' });
skillExpanded.delete(name); skillExpanded.delete(name);
loadSkills(); loadSkills();
} catch (e) { } catch (e) {
alert('Löschen fehlgeschlagen: ' + e.message); uiAlert('Löschen fehlgeschlagen: ' + e.message);
} }
} }
async function runSkillPrompt(name) { 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; if (argsStr === null) return;
let args = {}; let args = {};
try { try {
args = JSON.parse(argsStr || '{}'); args = JSON.parse(argsStr || '{}');
} catch (e) { } catch (e) {
alert('Ungültiges JSON: ' + e.message); uiAlert('Ungültiges JSON: ' + e.message);
return; return;
} }
try { try {
@@ -6830,11 +6894,11 @@
}); });
const d = await r.json(); 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)}`; 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 if (skillExpanded.has(name)) toggleSkillExpand(name); // refresh logs by collapse+expand
loadSkills(); loadSkills();
} catch (e) { } catch (e) {
alert('Run fehlgeschlagen: ' + e.message); uiAlert('Run fehlgeschlagen: ' + e.message);
} }
} }
@@ -6845,7 +6909,7 @@
async function importSkillFile(event) { async function importSkillFile(event) {
const file = event.target.files[0]; const file = event.target.files[0];
if (!file) return; 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; event.target.value = ''; return;
} }
try { try {
@@ -6856,13 +6920,13 @@
}); });
const d = await r.json(); const d = await r.json();
if (r.ok) { if (r.ok) {
alert(`✓ Skill "${d.imported?.name}" importiert.`); uiAlert(`✓ Skill "${d.imported?.name}" importiert.`);
loadSkills(); loadSkills();
} else { } else {
alert('Import fehlgeschlagen: ' + (d.detail || JSON.stringify(d))); uiAlert('Import fehlgeschlagen: ' + (d.detail || JSON.stringify(d)));
} }
} catch (e) { } catch (e) {
alert('Import fehlgeschlagen: ' + e.message); uiAlert('Import fehlgeschlagen: ' + e.message);
} finally { } finally {
event.target.value = ''; event.target.value = '';
} }
@@ -7027,7 +7091,7 @@
if (f) f.projectId = projectId || ''; if (f) f.projectId = projectId || '';
renderFilesList(); renderFilesList();
} catch (e) { } catch (e) {
alert('Zuordnung fehlgeschlagen: ' + e.message); uiAlert('Zuordnung fehlgeschlagen: ' + e.message);
renderFilesList(); // Dropdown auf alten Wert zuruecksetzen renderFilesList(); // Dropdown auf alten Wert zuruecksetzen
} }
} }
@@ -7086,7 +7150,7 @@
} }
async function restoreVersion(rel, hash) { 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 { try {
const r = await fetch('/api/files-version-restore', { const r = await fetch('/api/files-version-restore', {
method: 'POST', method: 'POST',
@@ -7099,7 +7163,7 @@
showVersions(rel); showVersions(rel);
loadFiles(); loadFiles();
} catch (e) { } catch (e) {
alert('Restore fehlgeschlagen: ' + e.message); uiAlert('Restore fehlgeschlagen: ' + e.message);
} }
} }
@@ -7131,14 +7195,14 @@
document.body.appendChild(a); a.click(); document.body.appendChild(a); a.click();
setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 100); setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 100);
} catch (e) { } catch (e) {
alert('ZIP-Download fehlgeschlagen: ' + e.message); uiAlert('ZIP-Download fehlgeschlagen: ' + e.message);
} }
} }
async function deleteSelected() { async function deleteSelected() {
const paths = [...filesSelected]; const paths = [...filesSelected];
if (!paths.length) return; 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 { try {
const r = await fetch('/api/files-delete-batch', { const r = await fetch('/api/files-delete-batch', {
method: 'POST', method: 'POST',
@@ -7150,10 +7214,10 @@
filesSelected.clear(); filesSelected.clear();
loadFiles(); loadFiles();
} else { } else {
alert('Bulk-Delete fehlgeschlagen: ' + (d.error || 'unbekannt')); uiAlert('Bulk-Delete fehlgeschlagen: ' + (d.error || 'unbekannt'));
} }
} catch (e) { } catch (e) {
alert('Bulk-Delete fehlgeschlagen: ' + e.message); uiAlert('Bulk-Delete fehlgeschlagen: ' + e.message);
} }
} }
@@ -7168,7 +7232,7 @@
} }
async function deleteFile(p, name) { 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 { try {
const r = await fetch('/api/files-delete', { const r = await fetch('/api/files-delete', {
method: 'POST', method: 'POST',
@@ -7180,10 +7244,10 @@
loadFiles(); loadFiles();
// Server broadcastet file_deleted Event → markFileDeletedInChat() updated bubbles // Server broadcastet file_deleted Event → markFileDeletedInChat() updated bubbles
} else { } else {
alert('Löschen fehlgeschlagen: ' + d.error); uiAlert('Löschen fehlgeschlagen: ' + d.error);
} }
} catch (e) { } 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>`; listEl.innerHTML = `<div style="color:#FF6B6B;">Fehler beim Laden: ${_ofmt(e.message)}</div>`;
} }
} }
function openOAuthCustomDialog() { async function openOAuthCustomDialog() {
const name = (prompt('Service-Name (z.B. dropbox, discord) — a-z 0-9 _ -:') || '').trim().toLowerCase(); 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 || !/^[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; return;
} }
const authUrl = (prompt(`auth_url fuer ${name}:`, 'https://') || '').trim(); const authUrl = (await uiPrompt(`auth_url fuer ${name}:`, 'https://') || '').trim();
if (!authUrl) return; 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; 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; const scopes = scopesRaw ? scopesRaw.split(/\s+/).filter(Boolean) : undefined;
fetch('/api/brain/oauth/apps', { fetch('/api/brain/oauth/apps', {
method: 'POST', method: 'POST',
@@ -7373,19 +7437,19 @@
}) })
.then(r => r.ok ? r.json() : r.text().then(t => Promise.reject(new Error(t)))) .then(r => r.ok ? r.json() : r.text().then(t => Promise.reject(new Error(t))))
.then(() => loadOAuthServices()) .then(() => loadOAuthServices())
.catch(e => alert('Custom-Service anlegen fehlgeschlagen: ' + e.message)); .catch(e => uiAlert('Custom-Service anlegen fehlgeschlagen: ' + e.message));
} }
async function deleteOAuthApp(service) { 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 { try {
const r = await fetch('/api/brain/oauth/apps/' + encodeURIComponent(service), { method: 'DELETE' }); const r = await fetch('/api/brain/oauth/apps/' + encodeURIComponent(service), { method: 'DELETE' });
if (!r.ok) { if (!r.ok) {
alert('Loeschen fehlgeschlagen: ' + (await r.text())); uiAlert('Loeschen fehlgeschlagen: ' + (await r.text()));
return; return;
} }
loadOAuthServices(); loadOAuthServices();
} catch (e) { } catch (e) {
alert('Loeschen fehlgeschlagen: ' + e.message); uiAlert('Loeschen fehlgeschlagen: ' + e.message);
} }
} }
async function saveOAuthApp(service) { async function saveOAuthApp(service) {
@@ -7395,7 +7459,7 @@
const tokenUrl = (document.getElementById('oauth-tok-' + service)?.value || '').trim(); const tokenUrl = (document.getElementById('oauth-tok-' + service)?.value || '').trim();
const scopesRaw = (document.getElementById('oauth-scopes-' + service)?.value || '').trim(); const scopesRaw = (document.getElementById('oauth-scopes-' + service)?.value || '').trim();
if (!cid) { if (!cid) {
alert('client_id darf nicht leer sein.'); uiAlert('client_id darf nicht leer sein.');
return; return;
} }
const body = { service, client_id: cid, client_secret: sec }; const body = { service, client_id: cid, client_secret: sec };
@@ -7410,12 +7474,12 @@
}); });
if (!r.ok) { if (!r.ok) {
const t = await r.text(); const t = await r.text();
alert('Speichern fehlgeschlagen: ' + t); uiAlert('Speichern fehlgeschlagen: ' + t);
return; return;
} }
loadOAuthServices(); loadOAuthServices();
} catch (e) { } catch (e) {
alert('Speichern fehlgeschlagen: ' + e.message); uiAlert('Speichern fehlgeschlagen: ' + e.message);
} }
} }
async function authorizeOAuth(service) { async function authorizeOAuth(service) {
@@ -7427,7 +7491,7 @@
}); });
if (!r.ok) { if (!r.ok) {
const t = await r.text(); const t = await r.text();
alert('Authorize fehlgeschlagen: ' + t); uiAlert('Authorize fehlgeschlagen: ' + t);
return; return;
} }
const data = await r.json(); const data = await r.json();
@@ -7436,39 +7500,39 @@
// Status nach ein paar Sekunden refreshen — Provider redirect → RVS → Brain // Status nach ein paar Sekunden refreshen — Provider redirect → RVS → Brain
setTimeout(loadOAuthServices, 8000); setTimeout(loadOAuthServices, 8000);
} catch (e) { } catch (e) {
alert('Authorize fehlgeschlagen: ' + e.message); uiAlert('Authorize fehlgeschlagen: ' + e.message);
} }
} }
async function revokeOAuth(service) { 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 { try {
const r = await fetch('/api/brain/oauth/' + service + '/revoke', { method: 'POST' }); const r = await fetch('/api/brain/oauth/' + service + '/revoke', { method: 'POST' });
if (!r.ok) { if (!r.ok) {
const t = await r.text(); const t = await r.text();
alert('Revoke fehlgeschlagen: ' + t); uiAlert('Revoke fehlgeschlagen: ' + t);
return; return;
} }
loadOAuthServices(); loadOAuthServices();
} catch (e) { } catch (e) {
alert('Revoke fehlgeschlagen: ' + e.message); uiAlert('Revoke fehlgeschlagen: ' + e.message);
} }
} }
async function distillNow() { 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 { try {
const r = await fetch('/api/brain/conversation/distill', { method: 'POST' }); const r = await fetch('/api/brain/conversation/distill', { method: 'POST' });
const d = await r.json(); 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(); loadBrainStatus();
loadBrainMemoryList(); loadBrainMemoryList();
} catch (e) { } catch (e) {
alert('Destillat fehlgeschlagen: ' + e.message); uiAlert('Destillat fehlgeschlagen: ' + e.message);
} }
} }
async function resetConversation() { 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 { try {
// Beides parallel — Brain Window + Diagnostic chat_backup // Beides parallel — Brain Window + Diagnostic chat_backup
const [brainR, histR] = await Promise.all([ const [brainR, histR] = await Promise.all([
@@ -7484,10 +7548,10 @@
if (fsBox) fsBox.innerHTML = ''; if (fsBox) fsBox.innerHTML = '';
loadBrainStatus(); loadBrainStatus();
} else { } 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) { } catch (e) {
alert('Reset fehlgeschlagen: ' + e.message); uiAlert('Reset fehlgeschlagen: ' + e.message);
} }
} }
@@ -8142,14 +8206,14 @@
const win = window.open('', '_blank'); const win = window.open('', '_blank');
if (!win) { 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; return;
} }
win.document.open(); win.document.open();
win.document.write(html); win.document.write(html);
win.document.close(); win.document.close();
} catch (e) { } 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; const id = document.getElementById('memory-edit-id').value;
if (!id) return; if (!id) return;
const name = decodeURIComponent(filenameEncoded); const name = decodeURIComponent(filenameEncoded);
if (!confirm(`Anhang "${name}" wirklich loeschen?`)) return; if (!await uiConfirm(`Anhang "${name}" wirklich loeschen?`)) return;
try { try {
const r = await fetch(`/api/brain/memory/${encodeURIComponent(id)}/attachments/${filenameEncoded}`, { const r = await fetch(`/api/brain/memory/${encodeURIComponent(id)}/attachments/${filenameEncoded}`, {
method: 'DELETE', method: 'DELETE',
@@ -8294,7 +8358,7 @@
const status = document.getElementById('memory-attachment-status'); const status = document.getElementById('memory-attachment-status');
if (status) status.textContent = `✓ "${name}" geloescht`; if (status) status.textContent = `✓ "${name}" geloescht`;
} catch (e) { } catch (e) {
alert('Loeschen fehlgeschlagen: ' + e.message); uiAlert('Loeschen fehlgeschlagen: ' + e.message);
} }
} }
@@ -8348,7 +8412,7 @@
async function deleteMemory(id) { async function deleteMemory(id) {
const m = brainMemoryCache[id]; const m = brainMemoryCache[id];
const label = m ? `"${m.title}" (${m.type})` : 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 { try {
const r = await fetch('/api/brain/memory/delete/' + encodeURIComponent(id), { method: 'DELETE' }); const r = await fetch('/api/brain/memory/delete/' + encodeURIComponent(id), { method: 'DELETE' });
if (!r.ok) throw new Error('HTTP ' + r.status); if (!r.ok) throw new Error('HTTP ' + r.status);
@@ -8366,7 +8430,7 @@
else await loadBrainMemoryList(); else await loadBrainMemoryList();
loadBrainStatus(); loadBrainStatus();
} catch (e) { } catch (e) {
alert('Löschen fehlgeschlagen: ' + e.message); uiAlert('Löschen fehlgeschlagen: ' + e.message);
} }
} }
@@ -8667,7 +8731,7 @@
// ── Migration aus brain-import/ ──────────────────────── // ── Migration aus brain-import/ ────────────────────────
async function runMigration() { async function runMigration() {
const status = document.getElementById('brain-import-files-status'); 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...'; if (status) status.innerHTML = '⏳ Migriere...';
try { try {
const r = await fetch('/api/brain/memory/migrate', { method: 'POST' }); 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.' 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.' : bScope === 'personal' ? 'Nur die aktuell pinned PERSÖNLICHEN Memories werden ersetzt — System-Regeln bleiben.'
: 'ALLE aktuell pinned Memories werden überschrieben.'; : '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 = ''; event.target.value = '';
return; return;
} }
@@ -8770,7 +8834,7 @@
const file = event.target.files[0]; const file = event.target.files[0];
if (!file) return; if (!file) return;
const statusEl = document.getElementById('brain-import-status'); 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 = ''; event.target.value = '';
return; return;
} }
@@ -8962,7 +9026,7 @@
} }
async function unquarantineTurn(ts) { 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 ' + 'blockt Anthropics Filter beim nächsten Mal erneut — dann landet '
+ 'er automatisch wieder hier.')) return; + 'er automatisch wieder hier.')) return;
try { try {
@@ -8972,11 +9036,11 @@
}); });
if (!r.ok) throw new Error('HTTP ' + r.status); if (!r.ok) throw new Error('HTTP ' + r.status);
loadSafeguard(); loadSafeguard();
} catch (e) { alert('Zurückholen fehlgeschlagen: ' + e.message); } } catch (e) { uiAlert('Zurückholen fehlgeschlagen: ' + e.message); }
} }
async function clearSafeguard() { 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. ' + 'auch in Projekten, die gerade aufs Ausweich-Modell ausweichen. '
+ 'Zurückgeholte Turns bleiben zurückgeholt.')) return; + 'Zurückgeholte Turns bleiben zurückgeholt.')) return;
try { try {
@@ -8986,7 +9050,7 @@
}); });
if (!r.ok) throw new Error('HTTP ' + r.status); if (!r.ok) throw new Error('HTTP ' + r.status);
loadSafeguard(); loadSafeguard();
} catch (e) { alert('Leeren fehlgeschlagen: ' + e.message); } } catch (e) { uiAlert('Leeren fehlgeschlagen: ' + e.message); }
} }
// ── Datenschutz-Schalter ────────────────────────────────────────── // ── Datenschutz-Schalter ──────────────────────────────────────────
@@ -9681,11 +9745,11 @@
try { try {
const j = await (await fetch('/api/llm-catalog/add', { method: 'POST', headers: { 'Content-Type': 'application/json' }, const j = await (await fetch('/api/llm-catalog/add', { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ hfRepo: repo }) })).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; _llmCatalog = j.models || _llmCatalog;
renderLlmCatalog(); renderLlmCatalog();
btn.outerHTML = '<span style="color:#3FFF9F;" title="Im Modell-Katalog Ziel-Karte wählen und „Laden"">im Katalog ✓ — dort „Laden"</span>'; 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? ── // ── Belege: was hat ARIA fuer eine Antwort wirklich ausgefuehrt? ──
@@ -9735,15 +9799,15 @@
// laden. Programme bekommen den neuen Wert automatisch (ctx in // laden. Programme bekommen den neuen Wert automatisch (ctx in
// llm_cluster_models). Passt es nicht in den Grafikspeicher, setzt die Box // llm_cluster_models). Passt es nicht in den Grafikspeicher, setzt die Box
// die Karte auf den alten Stand zurueck. // 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; const target = sel.dataset.box, gpu = sel.dataset.gpu, model = sel.dataset.model;
sel.blur(); // Fokus weg → aufgeschobenes Neuzeichnen holt den Stand nach sel.blur(); // Fokus weg → aufgeschobenes Neuzeichnen holt den Stand nach
const cur = Number(sel.dataset.current || 0), next = Number(sel.value); const cur = Number(sel.dataset.current || 0), next = Number(sel.value);
if (next === cur) return; if (next === cur) return;
const m = _llmCatalog.find(x => x.id === model); 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; 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' : '') + (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; } + '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)) || {}; const sl = (workers.find(w => w.instanceId === target)?.slots || []).find(x => String(x.gpu) === String(gpu)) || {};
@@ -9758,13 +9822,13 @@
const node = _boxName(target); const node = _boxName(target);
if (next === current) return; if (next === current) return;
if (!next) { 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; } + `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 }); send({ action: 'llm_unload_model', targetInstance: target, gpu });
return; return;
} }
const m = _llmCatalog.find(x => x.id === next); 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 sl = (workers.find(w => w.instanceId === target)?.slots || []).find(x => String(x.gpu) === String(gpu)) || {};
const vramGB = sl.vramMB ? sl.vramMB / 1024 : 0; const vramGB = sl.vramMB ? sl.vramMB / 1024 : 0;
const cpuMoe = await askCpuMoe(m, sl.vramMB || 0); const cpuMoe = await askCpuMoe(m, sl.vramMB || 0);
@@ -9772,20 +9836,20 @@
const warn = (!cpuMoe && vramGB && m.sizeGB && m.sizeGB > vramGB * 0.92) 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.` : ''; ? `\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.` : ''; 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 }); send({ action: 'llm_provision_model', targetInstance: target, gpu, key: m.id, hfRepo: m.hfRepo, quant: m.quant, ctx: m.ctx, cpuMoe });
openLlmDownloads(); openLlmDownloads();
} }
// MoE: Experten zwischen Karte und Arbeitsspeicher — gleiches Modell neu laden // 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; const target = sel.dataset.box, gpu = sel.dataset.gpu, model = sel.dataset.model;
sel.blur(); // Fokus weg → aufgeschobenes Neuzeichnen holt den Stand nach sel.blur(); // Fokus weg → aufgeschobenes Neuzeichnen holt den Stand nach
const cur = Number(sel.dataset.current || 0), next = Number(sel.value); const cur = Number(sel.dataset.current || 0), next = Number(sel.value);
const m = _llmCatalog.find(x => x.id === model); 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' }; 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)) || {}; 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 }); 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(); openLlmDownloads();
@@ -9817,8 +9881,8 @@
}).join(''); }).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>`; 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) { async function unloadLlmCard(instanceId, gpu, model) {
if (!confirm(`Karte ${gpu} leeren?\n\n${model} wird entladen; Clients können es danach nicht mehr wählen. ` 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 Datei bleibt auf der Box — erneutes Laden geht ohne Download.\n\n`
+ `Die anderen Karten dieser Box starten dabei kurz neu (wenige Sekunden).`)) return; + `Die anderen Karten dieser Box starten dabei kurz neu (wenige Sekunden).`)) return;
send({ action: 'llm_unload_model', targetInstance: instanceId, gpu }); 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]; const m = _llmCatalog[i];
if (!m) return; if (!m) return;
const b = onlineLlmBoxes().find(w => w.instanceId === instanceId); const b = onlineLlmBoxes().find(w => w.instanceId === instanceId);
const d = ((b && b.disk && b.disk.models) || []).find(x => x.repo === m.hfRepo); 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. ` + `Die heruntergeladene Datei (${d ? _fmtBytes(d.bytes) : '?'}, Repo ${m.hfRepo}) wird von der Box entfernt. `
+ `Wer es wieder will, muss es neu herunterladen.`)) return; + `Wer es wieder will, muss es neu herunterladen.`)) return;
send({ action: 'llm_remove_model', targetInstance: instanceId, key: m.id, hfRepo: m.hfRepo, deleteFiles: true }); send({ action: 'llm_remove_model', targetInstance: instanceId, key: m.id, hfRepo: m.hfRepo, deleteFiles: true });
} }
function cancelLlmJob(instanceId, key) { async 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; 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 }); send({ action: 'llm_remove_model', targetInstance: instanceId, key, cancel: true });
} }
@@ -9896,11 +9960,11 @@
const [target, gpu] = String((sel && sel.value) || '').split('|'); const [target, gpu] = String((sel && sel.value) || '').split('|');
if (!target) return; if (!target) return;
const tgt = llmCardTargets().find(t => t.value === sel.value); 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); 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 && 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` + `${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; + `Die anderen Karten dieser Box starten dabei kurz neu.\n\nErsetzen?`)) return;
const cpuMoe = await askCpuMoe(m, (tgt && tgt.slot && tgt.slot.vramMB) || 0); 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, // den Klick, wenn der Browser Dialoge unterdrueckt hatte ("Verhindern,
// dass diese Seite weitere Dialoge erstellt") → confirm()===false → es ging // dass diese Seite weitere Dialoge erstellt") → confirm()===false → es ging
// NICHTS raus. Nur "aggressive" (Volumes!) fragt noch nach. // NICHTS raus. Nur "aggressive" (Volumes!) fragt noch nach.
function runDiskCleanup(variant) { async function runDiskCleanup(variant) {
const aggressive = variant === 'aggressive'; const aggressive = variant === 'aggressive';
if (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; if (!ok) return;
} }
const btn = document.getElementById('disk-clean-btn'); const btn = document.getElementById('disk-clean-btn');
@@ -9994,7 +10058,7 @@
btn.textContent = 'Kopiert!'; btn.textContent = 'Kopiert!';
setTimeout(() => { btn.textContent = old; }, 1500); setTimeout(() => { btn.textContent = old; }, 1500);
}).catch(() => { }).catch(() => {
alert('Kopieren fehlgeschlagen — Befehl: ' + cmd); uiAlert('Kopieren fehlgeschlagen — Befehl: ' + cmd);
}); });
} }