Files
ARIA-AGENT/diagnostic/index.html
T
duffyduck b6b4b1b4d9 fix(diagnostic): loadBrainStatus updated jetzt beide Cards (Main + Gehirn)
Symptom: Main-Tab "ARIA BRAIN"-Card blieb auf "Lade..." haengen. Klick auf
"Status pruefen" tat scheinbar nichts.

Ursache: loadBrainStatus() suchte nur brain-status (Gehirn-Tab). Die Card
im Main-Tab hat aber andere IDs (brain-dot + brain-status-short + brain-error),
die wurden nirgends mehr befuellt seit updateState() das nicht mehr macht.

Fix: loadBrainStatus update jetzt BEIDE Anzeigen synchron — kompakte Main-Card
mit Dot/Status/Error UND die ausfuehrliche Gehirn-Tab-Zeile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:07:42 +02:00

3467 lines
173 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ARIA Diagnostic</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0D0D1A; color: #E0E0F0; font-family: 'Courier New', monospace; padding: 16px; }
h1 { font-size: 20px; margin-bottom: 16px; color: #0096FF; }
h2 { font-size: 14px; margin-bottom: 8px; color: #8888AA; text-transform: uppercase; letter-spacing: 1px; }
.grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; margin-bottom: 16px; }
.card { background: #12122A; border: 1px solid #1E1E2E; border-radius: 8px; padding: 12px; }
.card.full { grid-column: 1 / -1; }
.status-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.dot.connected { background: #34C759; box-shadow: 0 0 6px #34C759; }
.dot.connecting, .dot.testing { background: #FFD60A; animation: pulse 1s infinite; }
.dot.disconnected { background: #FF3B30; }
.dot.error { background: #FF3B30; box-shadow: 0 0 6px #FF3B30; }
.dot.not_configured { background: #555570; }
.dot.unknown { background: #555570; }
@keyframes pulse { 50% { opacity: 0.5; } }
.status-label { font-size: 13px; }
.error-text { color: #FF6B6B; font-size: 11px; margin-top: 4px; word-break: break-all; }
.btn { background: #0096FF; color: #fff; border: none; border-radius: 6px; padding: 8px 16px;
font-family: inherit; font-size: 13px; cursor: pointer; margin: 4px 4px 4px 0; }
.btn:hover { background: #007ADB; }
.btn:disabled { background: #333; color: #666; cursor: not-allowed; }
.btn.secondary { background: #1E1E2E; border: 1px solid #333; }
.btn.secondary:hover { background: #2A2A3E; }
/* Tabs */
.tab-bar { display: flex; gap: 2px; margin-bottom: 0; }
.tab-btn { background: #1E1E2E; color: #8888AA; border: 1px solid #1E1E2E; border-bottom: none;
border-radius: 6px 6px 0 0; padding: 6px 14px; font-family: inherit; font-size: 12px;
cursor: pointer; position: relative; top: 1px; }
.tab-btn.active { background: #080810; color: #E0E0F0; border-color: #1E1E2E; }
.tab-btn .tab-count { background: #333; color: #888; border-radius: 8px; padding: 1px 6px;
font-size: 10px; margin-left: 4px; }
.log-header { display: flex; align-items: center; justify-content: space-between; padding: 6px 8px 4px; }
.log-header h2 { margin-bottom: 0; font-size: 12px; }
.pause-hint { font-size: 11px; color: #FFD60A; display: none; }
.pause-hint.visible { display: inline; }
.log-panel { background: #080810; border: 1px solid #1E1E2E; border-radius: 0 0 6px 6px; }
.log-box { height: 280px; overflow-y: auto; padding: 8px; font-size: 11px; line-height: 1.6; }
.log-box.hidden { display: none; }
.log-entry { white-space: pre-wrap; word-break: break-all; }
.log-entry.error { color: #FF6B6B; }
.log-entry.warn { color: #FFD60A; }
.log-entry.info { color: #AAB; }
.log-entry.debug { color: #555570; }
.log-entry.trace-step { color: #0096FF; border-left: 2px solid #0096FF; padding-left: 6px; margin: 2px 0; }
.log-entry.trace-ok { color: #34C759; border-left: 2px solid #34C759; padding-left: 6px; margin: 2px 0; }
.log-entry.trace-err { color: #FF3B30; border-left: 2px solid #FF3B30; padding-left: 6px; margin: 2px 0; }
.log-entry.trace-sep { color: #333; margin: 6px 0 2px; }
.chat-box { background: #080810; border: 1px solid #1E1E2E; border-radius: 6px;
min-height: 120px; max-height: 250px; overflow-y: auto;
padding: 12px; margin-bottom: 8px; display: flex; flex-direction: column; gap: 8px; }
.chat-msg { padding: 10px 14px; border-radius: 14px; font-size: 14px; line-height: 1.5;
word-wrap: break-word; max-width: 80%; white-space: pre-wrap;
box-shadow: 0 1px 2px rgba(0,0,0,0.4); }
.chat-msg.sent { background: #0096FF; color: #fff; align-self: flex-end;
border-bottom-right-radius: 4px; }
.chat-msg.received { background: #1E1E2E; color: #E8E8F0; align-self: flex-start;
border-bottom-left-radius: 4px; }
.chat-msg.error { background: #3B1010; color: #FF6B6B; align-self: flex-start; }
.chat-msg .meta { font-size: 10px; color: rgba(255,255,255,0.4); margin-top: 4px;
display: block; }
.chat-msg a { color: #66BBFF; text-decoration: underline; }
.chat-msg.sent a { color: #CCEEFF; }
.chat-msg .chat-media { max-width: 100%; max-height: 200px; border-radius: 8px;
margin-top: 6px; cursor: pointer; display: block; }
.chat-msg .chat-media:hover { opacity: 0.85; }
.lightbox-overlay { display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.92);
z-index:2000; justify-content:center; align-items:center; cursor:pointer; }
.lightbox-overlay.open { display:flex; }
.lightbox-overlay img, .lightbox-overlay video { max-width:95vw; max-height:95vh; border-radius:8px; }
.input-row { display: flex; gap: 6px; align-items: flex-end; }
.input-row input, .input-row textarea {
flex: 1; background: #1E1E2E; border: 1px solid #333; border-radius: 6px;
padding: 8px 12px; color: #E0E0F0; font-family: inherit; font-size: 13px;
}
.input-row textarea { resize: none; min-height: 38px; max-height: 200px; line-height: 1.4;
overflow-y: auto; }
/* Terminal Modal */
.modal-overlay { display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.85);
z-index:1000; justify-content:center; align-items:center; }
.modal-overlay.open { display:flex; }
.modal-box { background:#0D0D1A; border:1px solid #FFD60A44; border-radius:10px; width:90vw; max-width:860px;
max-height:90vh; display:flex; flex-direction:column; overflow:hidden; }
.modal-header { display:flex; justify-content:space-between; align-items:center; padding:10px 14px;
border-bottom:1px solid #1E1E2E; }
.modal-header h3 { font-size:14px; color:#FFD60A; margin:0; }
.modal-close { background:none; border:none; color:#888; font-size:20px; cursor:pointer; padding:0 6px; }
.modal-close:hover { color:#FF3B30; }
.modal-body { flex:1; padding:4px; min-height:400px; }
.modal-footer { padding:6px 14px; border-top:1px solid #1E1E2E; font-size:11px; color:#8888AA; }
/* Haupt-Navigation */
.main-nav { display:flex; gap:0; margin-bottom:16px; border-bottom:2px solid #1E1E2E; }
.main-nav-btn { background:none; border:none; color:#8888AA; font-family:inherit; font-size:14px;
padding:10px 20px; cursor:pointer; border-bottom:2px solid transparent; margin-bottom:-2px;
transition: color 0.2s, border-color 0.2s; }
.main-nav-btn:hover { color:#E0E0F0; }
.main-nav-btn.active { color:#0096FF; border-bottom-color:#0096FF; }
.main-tab { display:none; }
.main-tab.visible { display:block; }
/* Settings */
.settings-section { margin-bottom:20px; }
.settings-section h2 { margin-bottom:12px; }
/* Info-Button: kleines (i) neben Ueberschriften */
.info-btn { background:transparent; border:1px solid #0096FF; color:#0096FF; width:20px; height:20px;
border-radius:50%; padding:0; font-size:11px; font-weight:bold; cursor:pointer; margin-left:6px;
line-height:18px; text-align:center; vertical-align:middle; font-family:serif; }
.info-btn:hover { background:#0096FF; color:#fff; }
.info-btn-small { background:transparent; border:1px solid #0096FF44; color:#0096FF; width:14px; height:14px;
border-radius:50%; padding:0; font-size:9px; font-weight:bold; cursor:pointer; margin-left:4px;
line-height:11px; text-align:center; vertical-align:middle; font-family:serif; }
.info-btn-small:hover { background:#0096FF; color:#fff; }
.toggle { position:relative; width:40px; height:22px; flex-shrink:0; margin-left:8px; }
.toggle input { opacity:0; width:0; height:0; }
.toggle .slider { position:absolute; cursor:pointer; top:0; left:0; right:0; bottom:0;
background:#333; border-radius:22px; transition:0.3s; }
.toggle .slider:before { content:""; position:absolute; height:16px; width:16px; left:3px; bottom:3px;
background:#888; border-radius:50%; transition:0.3s; }
.toggle input:checked + .slider { background:#0096FF; }
.toggle input:checked + .slider:before { transform:translateX(18px); background:#fff; }
.toggle input:disabled + .slider { opacity:0.4; cursor:not-allowed; }
</style>
</head>
<body>
<!-- Service-Status Banner unten rechts (Gamebox: F5-TTS / Whisper Lade-Status) -->
<div id="service-status-banner" style="display:none;position:fixed;bottom:16px;right:16px;z-index:999;background:#1E1E2E;border:1px solid #2A2A3E;border-radius:8px;padding:10px 14px;font-size:12px;color:#fff;min-width:240px;max-width:360px;box-shadow:0 4px 14px rgba(0,0,0,0.5);">
<div style="display:flex;align-items:flex-start;gap:8px;">
<span id="service-status-icon" style="font-size:18px;line-height:1;">&#x23F3;</span>
<div id="service-status-list" style="flex:1;display:flex;flex-direction:column;gap:6px;"></div>
<button id="service-status-close" onclick="document.getElementById('service-status-banner').style.display='none'" style="background:none;border:none;color:#666680;font-size:16px;cursor:pointer;padding:0;line-height:1;display:none;">&times;</button>
</div>
</div>
<!-- Voice-Preview Modal -->
<div id="voice-preview-modal" style="display:none;position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,0.7);align-items:center;justify-content:center;">
<div style="background:#1A1A2E;border:1px solid #2A2A3E;border-radius:10px;padding:20px;max-width:560px;width:90%;display:flex;flex-direction:column;gap:12px;">
<div style="display:flex;align-items:center;justify-content:space-between;">
<h3 style="margin:0;color:#fff;">Stimmen-Preview: <span id="voice-preview-name"></span></h3>
<button onclick="closeVoicePreview()" style="background:none;border:none;color:#8888AA;font-size:22px;cursor:pointer;">&times;</button>
</div>
<textarea id="voice-preview-text" rows="4"
style="background:#0D0D1A;border:1px solid #2A2A3E;border-radius:6px;padding:10px;color:#fff;font-size:13px;resize:vertical;"></textarea>
<div style="display:flex;align-items:center;gap:10px;font-size:12px;color:#8888AA;">
<span style="min-width:120px;">Geschwindigkeit:</span>
<button onclick="adjustPreviewSpeed(-0.1)" class="btn secondary" style="padding:4px 10px;font-size:12px;">0.1</button>
<span id="voice-preview-speed-value" style="min-width:52px;text-align:center;color:#fff;font-weight:600;">1.0 x</span>
<button onclick="adjustPreviewSpeed(0.1)" class="btn secondary" style="padding:4px 10px;font-size:12px;">+0.1</button>
<span style="color:#555570;font-size:11px;">(nur fuer dieses Modal, wird nicht gespeichert)</span>
</div>
<div style="display:flex;gap:8px;align-items:center;">
<button id="voice-preview-play" onclick="playVoicePreview()" class="btn primary" style="padding:8px 16px;">
▶ Abspielen
</button>
<span id="voice-preview-status" style="color:#8888AA;font-size:11px;flex:1;"></span>
</div>
<audio id="voice-preview-audio" controls style="width:100%;display:none;"></audio>
</div>
</div>
<!-- Disk-Space Warnung (dynamisch gesetzt) -->
<div id="disk-banner" style="display:none;position:sticky;top:0;z-index:500;padding:10px 14px;border-radius:0;margin:-16px -16px 12px -16px;font-size:13px;">
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<span id="disk-banner-icon" style="font-size:18px;">&#x26A0;&#xFE0F;</span>
<span id="disk-banner-text" style="flex:1;min-width:200px;font-weight:600;"></span>
<button onclick="copyDiskCmd('safe')" class="btn secondary" style="padding:4px 10px;font-size:11px;" title="docker builder prune -a -f && docker image prune -a -f">
Sicher aufraeumen
</button>
<button onclick="document.getElementById('disk-banner-aggressive').style.display=(document.getElementById('disk-banner-aggressive').style.display==='none'?'flex':'none')"
class="btn secondary" style="padding:4px 10px;font-size:11px;">
Mehr &#x25BE;
</button>
<button onclick="document.getElementById('disk-banner').style.display='none'" class="btn secondary" style="padding:4px 10px;font-size:11px;">Schliessen</button>
</div>
<!-- Aggressive Variante (erst nach Klick sichtbar) -->
<div id="disk-banner-aggressive" style="display:none;margin-top:10px;padding:8px;background:rgba(0,0,0,0.25);border-radius:4px;flex-direction:column;gap:6px;font-size:12px;">
<div>
<b>Sicher</b> (empfohlen) — Build-Cache + ungenutzte Images, keine Volumes:<br>
<code style="font-family:monospace;">docker builder prune -a -f && docker image prune -a -f</code>
</div>
<div style="color:#FFAA55;">
<b>Aggressiv</b> — zusaetzlich ungenutzte Volumes. <b>Nur wenn alle ARIA-Container laufen</b>, sonst riskierst du Daten-Verlust (Sessions, SSH-Keys, Shared):<br>
<code style="font-family:monospace;">docker system prune -a --volumes -f</code>
<button onclick="copyDiskCmd('aggressive')" class="btn secondary" style="padding:2px 8px;font-size:10px;margin-left:6px;">Kopieren</button>
</div>
</div>
</div>
<h1>ARIA Diagnostic</h1>
<!-- Haupt-Navigation -->
<div class="main-nav">
<button class="main-nav-btn active" onclick="switchMainTab('main')">Main</button>
<button class="main-nav-btn" onclick="switchMainTab('brain')">Gehirn</button>
<button class="main-nav-btn" onclick="switchMainTab('skills')">Skills</button>
<button class="main-nav-btn" onclick="switchMainTab('files')">Dateien</button>
<button class="main-nav-btn" onclick="switchMainTab('settings')">Einstellungen</button>
</div>
<!-- ══════ TAB: Main ══════ -->
<div id="tab-main" class="main-tab visible">
<!-- Verbindungsstatus -->
<div class="grid">
<div class="card">
<h2>ARIA Brain</h2>
<div class="status-row">
<div class="dot" id="brain-dot"></div>
<span class="status-label" id="brain-status-short">Lade...</span>
</div>
<div class="error-text" id="brain-error"></div>
<button class="btn secondary" onclick="loadBrainStatus()">Status pruefen</button>
</div>
<div class="card">
<h2>RVS (Rendezvous)</h2>
<div class="status-row">
<div class="dot" id="rvs-dot"></div>
<span class="status-label" id="rvs-status">-</span>
</div>
<div class="error-text" id="rvs-error"></div>
<button class="btn secondary" onclick="send({action:'reconnect_rvs'})">Reconnect</button>
</div>
<div class="card">
<h2>Claude Proxy</h2>
<div class="status-row">
<div class="dot" id="proxy-dot"></div>
<span class="status-label" id="proxy-status">Nicht getestet</span>
</div>
<div class="error-text" id="proxy-error"></div>
<div id="proxy-models" style="margin-top:6px;display:none">
<div style="font-size:11px;color:#8888AA;margin-bottom:3px">Verfuegbare Modelle:</div>
<div id="proxy-models-list" style="font-size:12px;line-height:1.6"></div>
<div style="font-size:10px;color:#555570;margin-top:4px" id="proxy-models-hint"></div>
</div>
<div id="proxy-auth" style="margin-top:6px;display:none;background:#080810;border:1px solid #1E1E2E;border-radius:4px;padding:6px 8px;font-size:10px;line-height:1.5;max-height:220px;overflow-y:auto;white-space:pre-wrap;color:#8888AA"></div>
<!-- Terminal Modal wird weiter unten definiert -->
<div id="proxy-creds-box" style="margin-top:6px;display:none">
<div style="background:#0A1A1A;border:1px solid #0096FF33;border-radius:6px;padding:8px 10px">
<div style="font-size:11px;color:#0096FF;margin-bottom:4px;font-weight:bold">Credentials einfuegen</div>
<div style="font-size:10px;color:#8888AA;margin-bottom:6px">Auf einem Rechner mit Claude Login: <code style="background:#1E1E2E;padding:1px 4px;border-radius:3px">cat ~/.config/claude/.credentials.json</code><br>Inhalt hier einfuegen:</div>
<textarea id="creds-input" rows="4" style="width:100%;background:#080810;border:1px solid #333;border-radius:4px;padding:6px;color:#E0E0F0;font-family:monospace;font-size:11px;resize:vertical" placeholder='{"claudeAiOauth":{"accessToken":"...","refreshToken":"...","expiresAt":"..."}}'></textarea>
<button class="btn" onclick="submitCredentials()" style="margin-top:4px;padding:4px 12px;font-size:11px">Speichern</button>
<div id="creds-status" style="font-size:11px;margin-top:4px"></div>
</div>
</div>
<button class="btn secondary" id="btn-proxy-test" onclick="testProxyBtn()" style="margin-top:6px">Proxy testen</button>
<button class="btn secondary" onclick="checkProxyAuth()" style="margin-top:6px">Auth pruefen</button>
<button class="btn secondary" id="btn-proxy-login" onclick="startProxyLogin()" style="margin-top:6px">Login starten</button>
<button class="btn secondary" onclick="toggleCredsBox()" style="margin-top:6px">Credentials einfuegen</button>
</div>
</div>
<!-- Chat Test -->
<div class="grid">
<div class="card full">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
<h2 style="margin:0;">Chat Test</h2>
<div style="display:flex;align-items:center;gap:12px;">
<label style="color:#8888AA;font-size:11px;cursor:pointer;">
<input type="checkbox" id="tts-debug-toggle" onchange="toggleTtsDebug()" style="margin-right:4px;vertical-align:middle;">
TTS-Text einblenden
</label>
<label style="color:#8888AA;font-size:11px;cursor:pointer;">
<input type="checkbox" id="gps-debug-toggle" onchange="toggleGpsDebug()" style="margin-right:4px;vertical-align:middle;">
GPS-Position einblenden
</label>
<button class="btn secondary" onclick="toggleChatFullscreen()" id="btn-chat-fs" style="padding:4px 10px;font-size:11px;">Vollbild</button>
</div>
</div>
<div class="chat-box" id="chat-box"></div>
<div id="thinking-indicator" style="display:none;padding:6px 10px;font-size:12px;color:#FFD60A;background:#1E1E2E;border-radius:0 0 6px 6px;margin-top:-8px;margin-bottom:8px;align-items:center;justify-content:space-between;">
<span><span style="animation:pulse 1s infinite;">&#x1F4AD;</span> <span id="thinking-text">ARIA denkt...</span></span>
<div style="display:flex;gap:6px;">
<button class="btn secondary" onclick="cancelRequest()" style="padding:2px 10px;font-size:11px;color:#FF3B30;border-color:#FF3B30;">Abbrechen</button>
</div>
</div>
<div id="diag-pending-attachments" style="display:none;padding:6px 10px;background:#1E1E2E;border-radius:6px 6px 0 0;margin-bottom:-4px;display:flex;gap:6px;flex-wrap:wrap;align-items:center;">
</div>
<div class="input-row">
<label class="btn secondary" style="padding:6px 10px;cursor:pointer;font-size:14px;" title="Datei anhaengen">
&#x1F4CE;
<input type="file" id="diag-file-input" multiple accept="image/*,application/pdf,.doc,.docx,.txt" style="display:none;" onchange="handleDiagFileSelect(this.files)">
</label>
<textarea id="chat-input" placeholder="Nachricht an ARIA... (Enter sendet, Shift+Enter neue Zeile)" rows="2" onpaste="handleDiagPaste(event)" oninput="autoResizeTextarea(this)"></textarea>
<button class="btn" id="btn-gw" onclick="testGateway()">Gateway senden</button>
<button class="btn" id="btn-rvs" onclick="testRVS()">Via RVS senden</button>
</div>
</div>
</div>
<!-- Chat Vollbild Modal -->
<div id="chat-fullscreen" style="display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;background:#0D0D1A;z-index:1000;padding:16px;flex-direction:column;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
<h2 style="margin:0;color:#0096FF;">ARIA Chat</h2>
<button class="btn secondary" onclick="toggleChatFullscreen()" style="padding:6px 14px;">Schliessen</button>
</div>
<div id="chat-box-fs" class="chat-box" style="flex:1;max-height:none;min-height:0;overflow-y:auto;"></div>
<div id="thinking-indicator-fs" style="display:none;padding:6px 10px;font-size:12px;color:#FFD60A;background:#1E1E2E;border-radius:6px;margin-top:4px;">
<span style="animation:pulse 1s infinite;">&#x1F4AD;</span> <span id="thinking-text-fs">ARIA denkt...</span>
</div>
<div class="input-row" style="margin-top:8px;">
<textarea id="chat-input-fs" placeholder="Nachricht an ARIA... (Enter sendet, Shift+Enter neue Zeile)" rows="2" oninput="autoResizeTextarea(this)"></textarea>
<button class="btn" onclick="testGatewayFS()">Gateway senden</button>
<button class="btn" onclick="testRVSFS()">Via RVS senden</button>
</div>
</div>
<!-- Sessions + alter Brain-Viewer entfernt — Memories laufen jetzt
komplett ueber den Gehirn-Tab gegen die Vector-DB im aria-brain. -->
<!-- Logs mit Tabs -->
<div class="card" style="margin-top:12px; padding: 8px 0 0 0;">
<div style="padding: 0 12px;">
<div class="tab-bar">
<button class="tab-btn active" data-tab="all" onclick="switchTab('all')">Alle <span class="tab-count" id="count-all">0</span></button>
<button class="tab-btn" data-tab="gateway" onclick="switchTab('gateway')">Gateway <span class="tab-count" id="count-gateway">0</span></button>
<button class="tab-btn" data-tab="rvs" onclick="switchTab('rvs')">RVS <span class="tab-count" id="count-rvs">0</span></button>
<button class="tab-btn" data-tab="proxy" onclick="switchTab('proxy')">Proxy <span class="tab-count" id="count-proxy">0</span></button>
<button class="tab-btn" data-tab="bridge" onclick="switchTab('bridge')">Bridge <span class="tab-count" id="count-bridge">0</span></button>
<button class="tab-btn" data-tab="server" onclick="switchTab('server')">Server <span class="tab-count" id="count-server">0</span></button>
<button class="tab-btn" data-tab="trace" onclick="switchTab('trace')" style="margin-left:auto;border-color:#0096FF44;color:#0096FF" title="End-to-End-Mitschnitt einer einzelnen Anfrage mit Zeitstempeln">Trace <span class="tab-count" id="count-trace">0</span></button>
</div>
</div>
<div class="log-panel">
<div class="log-header">
<span>
<button class="btn secondary" id="btn-docker-logs" onclick="loadDockerLogs()" style="padding:4px 10px;font-size:11px;display:none">Docker Logs laden</button>
</span>
<span>
<span class="pause-hint" id="pause-hint">Autoscroll pausiert</span>
<button class="btn secondary" id="btn-scroll" onclick="resumeScroll()" style="display:none;padding:4px 10px;font-size:11px">Nach unten</button>
</span>
</div>
<div class="log-box" id="log-all"></div>
<div class="log-box hidden" id="log-gateway"></div>
<div class="log-box hidden" id="log-rvs"></div>
<div class="log-box hidden" id="log-proxy"></div>
<div class="log-box hidden" id="log-bridge"></div>
<div class="log-box hidden" id="log-server"></div>
<div class="log-box hidden" id="log-trace"></div>
</div>
</div>
<!-- ARIA Live-Ansicht -->
<div class="card" style="margin-top:12px; padding: 8px 0 0 0;">
<div style="padding: 0 12px;">
<div class="tab-bar">
<button class="tab-btn active" id="live-tab-ssh" onclick="switchLiveTab('ssh')">SSH Terminal</button>
<button class="tab-btn" id="live-tab-desktop" onclick="switchLiveTab('desktop')">Desktop</button>
</div>
</div>
<div style="background:#080810; border:1px solid #1E1E2E; border-radius:0 0 6px 6px; position:relative;">
<!-- SSH Terminal -->
<div id="live-ssh" style="height:350px; padding:4px;">
<div id="live-ssh-bar" style="display:flex;gap:6px;align-items:center;padding:4px 4px 6px;">
<button class="btn" onclick="startLiveSSH()" id="btn-live-ssh" style="padding:4px 12px;font-size:11px;">Verbinden</button>
<span id="live-ssh-status" style="font-size:11px;color:#8888AA;">Nicht verbunden</span>
</div>
<div id="live-ssh-term" style="height:calc(100% - 32px);"></div>
</div>
<!-- Desktop Viewer -->
<div id="live-desktop" style="height:350px; display:none; position:relative;">
<div id="desktop-placeholder" style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:#555570;">
<div style="font-size:36px;margin-bottom:12px;">🖥</div>
<div style="font-size:13px;">Kein Desktop verfuegbar</div>
<div style="font-size:11px;margin-top:4px;">ARIA muss zuerst eine Desktop-Umgebung installieren</div>
<button class="btn secondary" onclick="checkDesktop()" style="margin-top:12px;padding:4px 12px;font-size:11px;">Desktop pruefen</button>
</div>
<iframe id="desktop-vnc" style="width:100%;height:100%;border:none;display:none;"></iframe>
</div>
</div>
</div>
</div><!-- /tab-main -->
<!-- ══════ TAB: Einstellungen ══════ -->
<div id="tab-settings" class="main-tab">
<!-- Was ist ARIA? -->
<div class="settings-section">
<div class="card" style="max-width:700px;font-size:13px;color:#AAA;border-left:3px solid #0096FF;">
<strong style="color:#0096FF;">ARIA</strong> — Autonomous Reasoning &amp; Intelligence Assistant.
Selbst gehosteter JARVIS-artiger KI-Assistent, gebaut von Stefan / HackerSoft Oldenburg.
</div>
</div>
<!-- Reparatur & Restart -->
<div class="settings-section">
<h2>Reparatur & Restart</h2>
<div class="card" style="line-height:1.6;">
<p style="color:#8888AA;font-size:12px;margin:0 0 10px;">
Wenn ein Container haengt oder ARIA nicht mehr antwortet — hier kann man gezielt eingreifen.
</p>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
<button class="btn secondary" onclick="restartContainer('aria-bridge')" style="color:#FF3B30;border-color:#FF3B30;">🚨 aria-bridge neu</button>
<button class="btn secondary" onclick="restartContainer('aria-brain')" style="color:#FF3B30;border-color:#FF3B30;">🚨 aria-brain neu</button>
<button class="btn secondary" onclick="restartContainer('aria-qdrant')" style="color:#FF3B30;border-color:#FF3B30;">🚨 aria-qdrant neu</button>
</div>
<div id="restart-status" style="margin-top:10px;font-size:11px;color:#8888AA;"></div>
</div>
</div>
<!-- Komplett-Reset -->
<div class="settings-section">
<h2>Komplett-Reset</h2>
<div class="card" style="line-height:1.6;border-left:3px solid #FF3B30;">
<p style="color:#FF6B6B;font-size:12px;margin:0 0 10px;">
<strong>⚠ WIPE ALL</strong> — alle Einstellungen, Stimmen und das komplette
Gedächtnis (Vector-DB) werden gelöscht. ARIA wird leer wie nach
Erstinstallation. Bleiben tun nur:
</p>
<ul style="color:#8888AA;font-size:12px;margin:0 0 12px 16px;padding:0;">
<li><code>.env</code> (Tokens + RVS-Config)</li>
<li><code>aria-data/ssh/</code> (SSH-Keys für aria-wohnung)</li>
</ul>
<p style="color:#8888AA;font-size:12px;margin:0 0 10px;">
Vorher exportieren wenn etwas erhalten bleiben soll (Gehirn-Tab + Voice-Liste).
</p>
<button class="btn" onclick="wipeAll()" style="background:#3A1010;border:1px solid #FF3B30;color:#FF6B6B;">🗑 ALLES löschen — neue ARIA</button>
<div id="wipe-status" style="margin-top:10px;font-size:11px;color:#8888AA;"></div>
</div>
</div>
<!-- Betriebsmodus -->
<div class="settings-section">
<h2>Betriebsmodus</h2>
<div class="card" style="max-width:500px;">
<div id="mode-selector" style="display:grid;grid-template-columns:1fr 1fr;gap:8px;">
<button class="btn mode-btn" data-mode="normal" onclick="setMode('normal')" style="background:#1E1E2E;border:2px solid transparent;">
<span style="font-size:18px;">&#x1F7E2;</span> Normal<br><span style="font-size:10px;color:#8888AA;">Hoert zu, antwortet, spricht</span>
</button>
<button class="btn mode-btn" data-mode="nicht_stoeren" onclick="setMode('nicht_stoeren')" style="background:#1E1E2E;border:2px solid transparent;">
<span style="font-size:18px;">&#x1F534;</span> Nicht stoeren<br><span style="font-size:10px;color:#8888AA;">Nur Kritikalarme</span>
</button>
<button class="btn mode-btn" data-mode="fluester" onclick="setMode('fluester')" style="background:#1E1E2E;border:2px solid transparent;">
<span style="font-size:18px;">&#x1F7E1;</span> Fluestern<br><span style="font-size:10px;color:#8888AA;">Nur Text, keine Sprache</span>
</button>
<button class="btn mode-btn" data-mode="hangar" onclick="setMode('hangar')" style="background:#1E1E2E;border:2px solid transparent;">
<span style="font-size:18px;">&#x2708;&#xFE0F;</span> Hangar<br><span style="font-size:10px;color:#8888AA;">Nur wichtige Meldungen</span>
</button>
<button class="btn mode-btn" data-mode="gaming" onclick="setMode('gaming')" style="background:#1E1E2E;border:2px solid transparent;grid-column:1/-1;">
<span style="font-size:18px;">&#x1F3AE;</span> Gaming<br><span style="font-size:10px;color:#8888AA;">Nur direkte Fragen</span>
</button>
</div>
<div style="margin-top:8px;font-size:11px;color:#555570;" id="mode-status">Aktueller Modus: Normal</div>
</div>
</div>
<!-- Stimmen -->
<div class="settings-section">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<h2 style="margin:0;">Sprachausgabe</h2>
<div style="display:flex;gap:6px;">
<button class="btn secondary" onclick="exportVoiceSettings()" style="padding:4px 10px;font-size:11px;" title="voice_config.json + highlight_triggers herunterladen">⬇ Export</button>
<input type="file" id="voice-settings-import-file" accept=".json,application/json" style="display:none" onchange="importVoiceSettings(event)">
<button class="btn secondary" onclick="document.getElementById('voice-settings-import-file').click()" style="padding:4px 10px;font-size:11px;">⬆ Import</button>
</div>
</div>
<div class="card" style="max-width:500px;">
<!-- TTS aktiv (global) -->
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
<label style="color:#8888AA;font-size:12px;">TTS aktiv:</label>
<label class="toggle"><input type="checkbox" id="diag-tts-enabled" checked onchange="sendVoiceConfig()"><span class="slider"></span></label>
</div>
<!-- F5-TTS Stimme (zwingend eine Voice waehlen — F5-TTS braucht eine Referenz) -->
<div style="display:flex;align-items:center;gap:12px;margin-bottom:6px;">
<label style="color:#8888AA;font-size:12px;">F5-TTS Stimme:</label>
<select id="diag-xtts-voice" onchange="sendVoiceConfig()" style="background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:6px;padding:6px 10px;font-size:13px;">
<option value="" disabled>(keine Stimme gewaehlt)</option>
</select>
<button class="btn secondary" onclick="loadXTTSVoices()" style="padding:4px 10px;font-size:11px;">Laden</button>
</div>
<div id="voice-status" style="font-size:11px;min-height:14px;margin-bottom:12px;color:#8888AA;"></div>
<!-- Gecloned Stimmen — Liste mit Loeschen -->
<div id="xtts-voice-list" style="margin-bottom:12px;"></div>
<!-- F5-TTS Modell-Tuning -->
<details style="background:#0D0D1A;border:1px solid #2A2A3E;border-radius:6px;padding:10px 12px;margin-bottom:12px;">
<summary style="color:#8888AA;font-size:12px;cursor:pointer;">F5-TTS Modell-Tuning (advanced)</summary>
<div style="margin-top:10px;display:flex;flex-direction:column;gap:8px;">
<div style="color:#8888AA;font-size:11px;">
Werden via RVS an die f5tts-bridge auf der Gamebox geschickt.
Modell-/Checkpoint-Wechsel triggert einen Reload (~30s).
Hardcoded Defaults: F5TTS_v1_Base, cfg_strength=2.5, nfe_step=32.
</div>
<label style="color:#8888AA;font-size:12px;">
Modell-Architektur (F5TTS_v1_Base = Default multilingual, F5TTS_Base = fuer die meisten Fine-Tunes):
</label>
<input type="text" id="diag-f5tts-model"
placeholder="F5TTS_v1_Base"
style="background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:6px;padding:6px 10px;font-size:13px;">
<label style="color:#8888AA;font-size:12px;">
Custom Checkpoint — HF-Pfad (hf://user/repo/file) oder lokaler Container-Pfad. Leer = Default.
</label>
<input type="text" id="diag-f5tts-ckpt"
placeholder="z.B. hf://aihpi/F5-TTS-German/F5TTS_Base/model_365000.safetensors"
style="background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:6px;padding:6px 10px;font-size:13px;">
<label style="color:#8888AA;font-size:12px;">
Custom Vocab — muss zum Checkpoint passen. Leer = Default.
</label>
<input type="text" id="diag-f5tts-vocab"
placeholder="z.B. hf://aihpi/F5-TTS-German/vocab.txt"
style="background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:6px;padding:6px 10px;font-size:13px;">
<div style="display:flex;gap:12px;">
<div style="flex:1;">
<label style="color:#8888AA;font-size:12px;">cfg_strength (1.0 - 5.0):</label>
<input type="number" id="diag-f5tts-cfg" step="0.1" min="1" max="5"
placeholder="2.5"
style="background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:6px;padding:6px 10px;font-size:13px;width:100%;box-sizing:border-box;">
<div style="color:#666680;font-size:10px;">Hoeher = klebt staerker an Referenz</div>
</div>
<div style="flex:1;">
<label style="color:#8888AA;font-size:12px;">nfe_step (8 - 64):</label>
<input type="number" id="diag-f5tts-nfe" step="1" min="8" max="64"
placeholder="32"
style="background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:6px;padding:6px 10px;font-size:13px;width:100%;box-sizing:border-box;">
<div style="color:#666680;font-size:10px;">Hoeher = bessere Qualitaet, langsamer</div>
</div>
</div>
<button class="btn primary" onclick="sendVoiceConfig()" style="padding:6px 14px;font-size:12px;align-self:flex-start;margin-top:6px;">
Anwenden
</button>
</div>
</details>
<!-- Voice Cloning -->
<div style="background:#1E1E2E;border-radius:8px;padding:12px;margin-top:8px;">
<div style="color:#0096FF;font-size:13px;font-weight:600;margin-bottom:8px;">Stimme klonen</div>
<div style="color:#8888AA;font-size:11px;margin-bottom:8px;">
Lade ein oder mehrere Audio-Samples hoch (WAV/MP3, min. 6-10 Sekunden).
Mehrere Dateien werden automatisch zusammengefuegt.
</div>
<div style="margin-bottom:8px;">
<input type="text" id="xtts-clone-name" placeholder="Name fuer die Stimme..." style="background:#0D0D1A;border:1px solid #2A2A3E;border-radius:6px;padding:6px 10px;color:#fff;font-size:13px;width:100%;box-sizing:border-box;">
</div>
<div style="margin-bottom:8px;">
<input type="file" id="xtts-clone-files" accept="audio/*" multiple style="color:#8888AA;font-size:12px;">
</div>
<div style="display:flex;gap:8px;">
<button class="btn" onclick="uploadVoiceSamples()" style="flex:1;">Stimme erstellen</button>
</div>
<div id="xtts-clone-status" style="font-size:11px;color:#555570;margin-top:6px;"></div>
</div>
<!-- XTTS Status -->
<div style="margin-top:8px;font-size:11px;color:#555570;" id="xtts-status">
XTTS-Server: Nicht verbunden (starte xtts/ auf dem Gaming-PC)
</div>
</div>
</div>
<!-- Whisper (STT) -->
<div class="settings-section">
<h2>Whisper (Spracherkennung)</h2>
<div style="font-size:11px;color:#8888AA;margin-bottom:8px;">
Aenderungen werden sofort an die Bridge gesendet und das Modell neu geladen
(kann bei medium/large 10-30s dauern — waehrend dieser Zeit ist STT kurz pausiert).
</div>
<div class="card" style="max-width:500px;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px;">
<label style="color:#8888AA;font-size:12px;min-width:80px;">Modell:</label>
<select id="diag-whisper-model" onchange="sendVoiceConfig()" style="flex:1;background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:6px;padding:6px 10px;font-size:13px;">
<option value="tiny">tiny (39MB, schnell, niedrige Qualitaet)</option>
<option value="base">base (74MB, schnell, ok)</option>
<option value="small">small (244MB, mittel)</option>
<option value="medium" selected>medium (769MB, gut — Empfehlung)</option>
<option value="large-v3">large-v3 (1.5GB, beste Qualitaet, langsam auf CPU)</option>
</select>
</div>
<div style="font-size:10px;color:#555570;">
Tipp: <code>medium</code> ist der beste Kompromiss fuer CPU. <code>large-v3</code> nur bei GPU sinnvoll.
</div>
</div>
</div>
<!-- Runtime-Konfiguration -->
<div class="settings-section">
<h2>Runtime-Konfiguration</h2>
<div style="font-size:11px;color:#8888AA;margin-bottom:8px;">
Werte werden in <code>/shared/config/runtime.json</code> persistiert und
ueberschreiben die ENV-Variablen aus der <code>.env</code>. Bridge und Brain
lesen sie beim Start — nach Aenderung den jeweiligen Container neu starten
(Reparatur-Section oben).
</div>
<div class="card" style="max-width:600px;">
<div style="display:grid;grid-template-columns:140px 1fr;gap:8px 10px;align-items:center;font-size:13px;">
<label style="color:#8888AA;">RVS Host:</label>
<input type="text" id="rc-rvs-host" style="width:100%;box-sizing:border-box;background:#1E1E2E;border:1px solid #2A2A3E;border-radius:4px;padding:6px;color:#fff;">
<label style="color:#8888AA;">RVS Port:</label>
<input type="text" id="rc-rvs-port" style="width:100%;box-sizing:border-box;background:#1E1E2E;border:1px solid #2A2A3E;border-radius:4px;padding:6px;color:#fff;">
<label style="color:#8888AA;">RVS TLS:</label>
<select id="rc-rvs-tls" style="width:100%;box-sizing:border-box;background:#1E1E2E;border:1px solid #2A2A3E;border-radius:4px;padding:6px;color:#fff;">
<option value="true">true (wss://)</option>
<option value="false">false (ws://)</option>
</select>
<label style="color:#8888AA;">RVS Token:</label>
<div style="display:flex;gap:4px;min-width:0;">
<input type="password" id="rc-rvs-token" style="flex:1;min-width:0;box-sizing:border-box;background:#1E1E2E;border:1px solid #2A2A3E;border-radius:4px;padding:6px;color:#fff;font-family:monospace;">
<button type="button" class="btn secondary" onclick="toggleSecret('rc-rvs-token', this)" style="padding:4px 10px;flex-shrink:0;" title="Anzeigen/Verbergen">&#128065;</button>
</div>
<label style="color:#8888AA;">Aria Auth Token:</label>
<div style="display:flex;gap:4px;min-width:0;">
<input type="password" id="rc-auth-token" style="flex:1;min-width:0;box-sizing:border-box;background:#1E1E2E;border:1px solid #2A2A3E;border-radius:4px;padding:6px;color:#fff;font-family:monospace;">
<button type="button" class="btn secondary" onclick="toggleSecret('rc-auth-token', this)" style="padding:4px 10px;flex-shrink:0;" title="Anzeigen/Verbergen">&#128065;</button>
</div>
</div>
<div style="display:flex;gap:8px;margin-top:12px;">
<button class="btn" onclick="saveRuntimeConfig()" style="flex:1;">Speichern</button>
<button class="btn secondary" onclick="loadRuntimeConfig()" style="flex:1;">Neu laden</button>
</div>
<div id="rc-status" style="font-size:11px;color:#555570;margin-top:6px;"></div>
</div>
</div>
<!-- App-Onboarding via QR-Code -->
<div class="settings-section">
<h2>App-Onboarding (QR-Code)</h2>
<div style="font-size:11px;color:#8888AA;margin-bottom:8px;">
RVS-Credentials als QR-Code — App scannt, keine manuelle Eingabe.
Enthaelt Host, Port, TLS-Flag und Token.
</div>
<div class="card" style="max-width:500px;">
<div style="display:flex;gap:12px;align-items:flex-start;">
<div id="onboarding-qr" style="width:220px;height:220px;flex-shrink:0;background:#1E1E2E;border-radius:6px;overflow:hidden;display:flex;align-items:center;justify-content:center;color:#555570;font-size:11px;text-align:center;">
QR-Code wird geladen...
</div>
<div style="flex:1;font-size:11px;color:#8888AA;line-height:1.5;">
<div style="color:#FF9500;font-weight:bold;margin-bottom:4px;">Achtung</div>
Dieser QR enthaelt den RVS-Token im Klartext — zeige ihn niemandem,
speichere keine Screenshots davon in unsicheren Cloud-Diensten.
<button class="btn" onclick="loadOnboardingQR()" style="margin-top:10px;width:100%;">
QR neu generieren
</button>
</div>
</div>
</div>
</div>
<!-- Tool-Berechtigungen-Section entfernt — Brain steuert Tool-Use
selbst (Skills + skill_create Meta-Tool). Es gibt keine granulare
Permission-Maske, Brain weiss zur Laufzeit welche Tools es hat. -->
<!-- Brain-Model-Einstellung -->
<div class="settings-section">
<h2>Sprachmodell (Brain)</h2>
<div class="card" style="max-width:500px;">
<div style="font-size:11px;color:#8888AA;margin-bottom:10px;line-height:1.5;">
Welches Claude-Model nutzt das Brain pro Anfrage. Wert wird in
<code>/shared/config/runtime.json</code> als <code>brainModel</code> persistiert.
Bei Aenderung: <strong>aria-brain restarten</strong> (Reparatur-Section oben), damit's greift.
<br><br>
Verfuegbar via Proxy: <code>claude-sonnet-4</code> (Default — schnell, gut),
<code>claude-opus-4</code> (langsam, smarter), <code>claude-haiku-4-5</code> (sehr schnell, kleiner Kontext).
</div>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
<span style="font-size:12px;color:#8888AA;white-space:nowrap;">Aktives Model:</span>
<input type="text" id="setting-model" placeholder="claude-sonnet-4" style="flex:1;background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:6px 8px;color:#E0E0F0;font-family:inherit;font-size:12px;">
<button class="btn secondary" onclick="loadModel()" style="padding:4px 8px;font-size:10px;">Laden</button>
<button class="btn" onclick="saveModel()" style="padding:4px 8px;font-size:10px;">Setzen</button>
</div>
<div id="model-status" style="font-size:10px;color:#8888AA;"></div>
</div>
</div>
<!-- OpenClaw-Config-Sektion entfernt — aria-core ist raus. -->
</div><!-- /tab-settings -->
<!-- ══════ TAB: Gehirn ══════ -->
<div id="tab-brain" class="main-tab">
<div class="settings-section">
<h2>Gehirn — Status <button class="info-btn" onclick="showInfo('brain-status')" title="Was bedeutet was?"></button></h2>
<div class="card">
<div id="brain-status" style="font-size:12px;color:#8888AA;margin-bottom:8px;">(Lade...)</div>
<div id="conversation-status" style="font-size:12px;color:#8888AA;margin-bottom:8px;">
<button class="info-btn-small" onclick="showInfo('conversation')" title="Konversation — wie funktioniert das?"></button>
</div>
<div style="display:flex;gap:6px;flex-wrap:wrap;">
<button class="btn secondary" onclick="loadBrainStatus()" style="padding:4px 12px;font-size:11px;">Aktualisieren</button>
<button class="btn secondary" onclick="distillNow()" style="padding:4px 12px;font-size:11px;color:#FFD60A;border-color:#FFD60A;" title="Destilliert die aeltesten Turns sofort zu fact-Memories">⚗ Jetzt destillieren</button>
<button class="btn secondary" onclick="resetConversation()" style="padding:4px 12px;font-size:11px;color:#FF6B6B;border-color:#FF6B6B;" title="Leert ARIAs Rolling-Window (Brain) + die Chat-Anzeige (chat_backup). Memories bleiben in der Vector-DB.">🧹 Konversation komplett zurücksetzen</button>
</div>
</div>
</div>
<div class="settings-section">
<h2>Bootstrap & Migration <button class="info-btn" onclick="showInfo('bootstrap')" title="Was sind die drei Wege?"></button></h2>
<div class="card" style="line-height:1.6;">
<p style="color:#8888AA;font-size:12px;margin:0 0 12px;">
Drei Wege ARIA mit "Grundregeln" zu füttern — von leichtgewichtig bis Voll-Wiederherstellung.
</p>
<!-- 1. Migration aus brain-import/ -->
<div style="background:#0D0D1A;border-radius:6px;padding:10px 12px;margin-bottom:8px;">
<div style="color:#0096FF;font-weight:bold;font-size:12px;margin-bottom:4px;">1. Aus brain-import/ migrieren</div>
<div style="color:#8888AA;font-size:11px;margin-bottom:8px;">
Parst <code>AGENT.md</code>/<code>USER.md</code>/<code>TOOLING.md</code> aus dem Repo und schreibt sie
als atomare pinned Memory-Punkte (identity / rule / preference / tool / skill). Idempotent — Re-Run
ersetzt nur die Migration-Punkte, eigene Memories bleiben.
</div>
<div id="brain-import-files-status" style="color:#555570;font-size:11px;margin-bottom:6px;">(Brain-Status laden für Datei-Liste)</div>
<button class="btn" onclick="runMigration()" style="background:#0096FF22;border:1px solid #0096FF;color:#0096FF;">▶ Migration starten</button>
</div>
<!-- 2. Bootstrap Snapshot -->
<div style="background:#0D0D1A;border-radius:6px;padding:10px 12px;margin-bottom:8px;">
<div style="color:#FFD60A;font-weight:bold;font-size:12px;margin-bottom:4px;">2. Bootstrap-Snapshot (nur pinned)</div>
<div style="color:#8888AA;font-size:11px;margin-bottom:8px;">
Klein und schnell: <strong>nur</strong> die pinned Memories (Identität, Regeln, Präferenzen, Tools, Skills) als JSON.
Use-Case: Wipe → Bootstrap-Import → ARIA hat Persönlichkeit zurück, sonst leer.
Cold Memory (Konversations-Fakten) bleibt beim Import unangetastet.
</div>
<button class="btn secondary" onclick="exportBootstrap()" style="color:#FFD60A;border-color:#FFD60A;">⬇ Bootstrap exportieren (JSON)</button>
<input type="file" id="bootstrap-import-file" accept=".json,application/json" style="display:none" onchange="importBootstrap(event)">
<button class="btn secondary" onclick="document.getElementById('bootstrap-import-file').click()" style="color:#FFD60A;border-color:#FFD60A;">⬆ Bootstrap importieren</button>
<div id="bootstrap-status" style="margin-top:8px;font-size:11px;color:#8888AA;"></div>
</div>
<!-- 3. Komplettes Gehirn -->
<div style="background:#0D0D1A;border-radius:6px;padding:10px 12px;">
<div style="color:#FF6B6B;font-weight:bold;font-size:12px;margin-bottom:4px;">3. Komplettes Gehirn (alles)</div>
<div style="color:#8888AA;font-size:11px;margin-bottom:8px;">
Tar.gz mit Memories + Skills + Qdrant-DB. Brain + Qdrant werden kurz angehalten.
Import überschreibt ALLES — vorher exportieren wenn etwas erhalten bleiben soll.
</div>
<button class="btn" onclick="brainExport()">⬇ Gehirn exportieren (tar.gz)</button>
<input type="file" id="brain-import-file" accept=".tar.gz,.tgz,application/gzip" style="display:none" onchange="brainImport(event)">
<button class="btn secondary" onclick="document.getElementById('brain-import-file').click()" style="background:#3A1010;border-color:#FF6B6B;color:#FF6B6B;">⬆ Gehirn importieren</button>
<div id="brain-import-status" style="margin-top:8px;font-size:11px;color:#8888AA;"></div>
</div>
</div>
</div>
<!-- Alte Sessions-Sicherung entfernt — aria-core ist raus. -->
<div class="settings-section">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<h2 style="margin:0;">Memories <button class="info-btn" onclick="showInfo('memories')" title="Hot vs. Cold — wie funktioniert das Gedaechtnis?"></button></h2>
<div>
<button class="btn secondary" onclick="resetBrainFilters();loadBrainMemoryList()" style="padding:4px 10px;font-size:11px;">Aktualisieren</button>
<button class="btn" onclick="openMemoryModal()" style="padding:4px 10px;font-size:11px;">+ Neu</button>
</div>
</div>
<div class="card" style="margin-bottom:8px;">
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
<input type="text" id="brain-search" placeholder="Semantische Suche (z.B. 'Stefan Persönlichkeit')..."
style="flex:1;min-width:200px;background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px 8px;border-radius:4px;font-family:inherit;font-size:12px;"
onkeydown="if(event.key==='Enter') runBrainSearch()">
<button class="btn secondary" onclick="runBrainSearch()" style="padding:4px 12px;font-size:11px;">Suchen</button>
<select id="brain-filter-type" onchange="loadBrainMemoryList()"
style="background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;font-size:11px;">
<option value="">Alle Typen</option>
<option value="identity">Identität</option>
<option value="rule">Regeln / Werte</option>
<option value="preference">Praeferenzen</option>
<option value="tool">Tools</option>
<option value="skill">Skills</option>
<option value="fact">Fakten</option>
<option value="conversation">Konversation</option>
<option value="reminder">Reminder</option>
</select>
<select id="brain-filter-pinned" onchange="loadBrainMemoryList()"
style="background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;font-size:11px;">
<option value="all">Pinned + Cold</option>
<option value="pinned">📌 Nur Pinned</option>
<option value="cold">Nur Cold</option>
</select>
<button class="btn secondary" onclick="resetBrainFilters();loadBrainMemoryList()" style="padding:4px 8px;font-size:11px;color:#8888AA;" title="Suche + Filter zurücksetzen"></button>
</div>
<div id="brain-search-info" style="margin-top:6px;font-size:10px;color:#8888AA;display:none;"></div>
</div>
<div class="card">
<div id="brain-memory-list" style="font-size:12px;color:#8888AA;">(Brain-Container nicht erreichbar oder leer)</div>
</div>
</div>
</div><!-- /tab-brain -->
<!-- ══════ TAB: Dateien ══════ -->
<div id="tab-files" class="main-tab">
<div class="settings-section">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<h2 style="margin:0;">Dateien</h2>
<button class="btn secondary" onclick="loadFiles()" style="padding:4px 10px;font-size:11px;">Aktualisieren</button>
</div>
<div class="card" style="margin-bottom:8px;">
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
<input type="text" id="files-search" placeholder="Suche (Dateiname)..." oninput="renderFilesList()"
style="flex:1;min-width:200px;background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px 8px;border-radius:4px;font-family:inherit;font-size:12px;">
<select id="files-filter" onchange="renderFilesList()" style="background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;font-size:11px;">
<option value="all">Alle</option>
<option value="aria">Von ARIA (aria_*)</option>
<option value="user">Vom Benutzer</option>
</select>
</div>
<div id="files-info" style="margin-top:6px;font-size:10px;color:#8888AA;"></div>
</div>
<div class="card">
<div id="files-list" style="font-size:12px;color:#8888AA;">(Lade...)</div>
</div>
</div>
</div><!-- /tab-files -->
<!-- ══════ TAB: Skills ══════ -->
<div id="tab-skills" class="main-tab">
<div class="settings-section">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<h2 style="margin:0;">Skills</h2>
<div style="display:flex;gap:6px;">
<button class="btn secondary" onclick="loadSkills()" style="padding:4px 10px;font-size:11px;">Aktualisieren</button>
<input type="file" id="skill-import-file" accept=".tar.gz,.tgz,application/gzip" style="display:none" onchange="importSkillFile(event)">
<button class="btn secondary" onclick="document.getElementById('skill-import-file').click()" style="padding:4px 10px;font-size:11px;">⬆ Import</button>
</div>
</div>
<div class="card" style="margin-bottom:8px;">
<p style="color:#8888AA;font-size:12px;margin:0;">
Skills sind ARIAs Faehigkeiten. ARIA legt sie selbst an wenn sie ein wiederkehrendes
Problem geloest hat. Du kannst sie hier verwalten — ansehen, ausfuehren, deaktivieren,
exportieren, importieren. Ein deaktivierter Skill bleibt ARIA bekannt (sie baut ihn
nicht doppelt), kann aber nicht aufgerufen werden.
</p>
</div>
<div class="card">
<div id="skills-list" style="font-size:12px;color:#8888AA;">(Lade...)</div>
</div>
</div>
</div><!-- /tab-skills -->
<!-- Generisches Info-Modal — wird via openInfoModal(title, html) gefuellt -->
<div class="modal-overlay" id="info-modal">
<div class="modal-box" style="max-width:640px;">
<div class="modal-header">
<h3 id="info-modal-title">Info</h3>
<button class="modal-close" onclick="closeInfoModal()">&times;</button>
</div>
<div class="modal-body" id="info-modal-body" style="padding:16px;font-size:13px;color:#E0E0F0;line-height:1.6;"></div>
</div>
</div>
<!-- Memory-Modal (Neu + Editieren) -->
<div class="modal-overlay" id="memory-modal">
<div class="modal-box" style="max-width:640px;">
<div class="modal-header">
<h3 id="memory-modal-title">Neue Memory</h3>
<button class="modal-close" onclick="closeMemoryModal()">&times;</button>
</div>
<div class="modal-body" style="padding:16px;">
<input type="hidden" id="memory-edit-id" value="">
<label style="display:block;font-size:11px;color:#8888AA;margin-bottom:4px;">Typ</label>
<select id="memory-type" style="width:100%;background:#0D0D1A;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;margin-bottom:10px;">
<option value="identity">identity — Wer ARIA ist</option>
<option value="rule">rule — Sicherheit / Werte / Normen</option>
<option value="preference">preference — Benutzer-Praeferenzen</option>
<option value="tool">tool — Tool-Freigaben</option>
<option value="skill">skill — Faehigkeit / Workflow</option>
<option value="fact" selected>fact — Wissens-Fakt</option>
<option value="conversation">conversation — Aus Gespraech destilliert</option>
<option value="reminder">reminder — Termin / Aufgabe</option>
</select>
<label style="display:block;font-size:11px;color:#8888AA;margin-bottom:4px;">Titel</label>
<input type="text" id="memory-title" style="width:100%;background:#0D0D1A;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;margin-bottom:10px;" placeholder="Kurze Ueberschrift">
<label style="display:block;font-size:11px;color:#8888AA;margin-bottom:4px;">Inhalt</label>
<textarea id="memory-content" rows="8" style="width:100%;background:#0D0D1A;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;resize:vertical;margin-bottom:10px;" placeholder="Der eigentliche Text — das wird embedded und durchsucht."></textarea>
<label style="display:block;font-size:11px;color:#8888AA;margin-bottom:4px;">Kategorie (frei, optional)</label>
<input type="text" id="memory-category" style="width:100%;background:#0D0D1A;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;margin-bottom:10px;" placeholder="z.B. persoenlichkeit, sicherheit, infrastruktur">
<label style="display:block;font-size:11px;color:#8888AA;margin-bottom:4px;">Tags (komma-getrennt)</label>
<input type="text" id="memory-tags" style="width:100%;background:#0D0D1A;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;margin-bottom:10px;" placeholder="rvs, voice, bug">
<label style="display:flex;align-items:center;gap:8px;color:#E0E0F0;font-size:13px;cursor:pointer;">
<input type="checkbox" id="memory-pinned">
<span>📌 Pinned (Hot Memory — IMMER im System-Prompt)</span>
</label>
<div id="memory-modal-error" style="color:#FF6B6B;font-size:11px;margin-top:10px;display:none;"></div>
</div>
<div class="modal-footer" style="padding:10px 16px;border-top:1px solid #1E1E2E;display:flex;justify-content:flex-end;gap:8px;">
<button class="btn secondary" onclick="closeMemoryModal()">Abbrechen</button>
<button class="btn" onclick="saveMemory()">Speichern</button>
</div>
</div>
</div>
<!-- Terminal Modal -->
<div class="modal-overlay" id="term-modal">
<div class="modal-box">
<div class="modal-header">
<h3>Claude Login Terminal</h3>
<button class="modal-close" onclick="closeTermModal()">&times;</button>
</div>
<div class="modal-body" id="terminal-container"></div>
<div class="modal-footer" id="term-status"></div>
</div>
</div>
<!-- Lightbox fuer Medien -->
<div class="lightbox-overlay" id="lightbox" onclick="closeLightbox()"></div>
<script>
const chatBox = document.getElementById('chat-box');
const pauseHint = document.getElementById('pause-hint');
const btnScroll = document.getElementById('btn-scroll');
let ws;
let activeTab = 'all';
const DOCKER_TABS = ['gateway', 'proxy', 'bridge'];
const autoScroll = { all: true, gateway: true, rvs: true, proxy: true, bridge: true, server: true, trace: true };
const logCounts = { all: 0, gateway: 0, rvs: 0, proxy: 0, bridge: 0, server: 0, trace: 0 };
const logBoxes = {
all: document.getElementById('log-all'),
gateway: document.getElementById('log-gateway'),
rvs: document.getElementById('log-rvs'),
proxy: document.getElementById('log-proxy'),
bridge: document.getElementById('log-bridge'),
server: document.getElementById('log-server'),
trace: document.getElementById('log-trace'),
};
// Scroll-Pause pro aktivem Tab
Object.entries(logBoxes).forEach(([tab, box]) => {
box.addEventListener('scroll', () => {
const atBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 30;
autoScroll[tab] = atBottom;
if (tab === activeTab) {
pauseHint.classList.toggle('visible', !atBottom);
btnScroll.style.display = atBottom ? 'none' : 'inline';
}
});
});
function resumeScroll() {
autoScroll[activeTab] = true;
const box = logBoxes[activeTab];
box.scrollTop = box.scrollHeight;
pauseHint.classList.remove('visible');
btnScroll.style.display = 'none';
}
function switchTab(tab) {
activeTab = tab;
document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === tab));
Object.entries(logBoxes).forEach(([t, box]) => box.classList.toggle('hidden', t !== tab));
// Docker Logs Button nur bei Tabs mit Containern
document.getElementById('btn-docker-logs').style.display = DOCKER_TABS.includes(tab) ? 'inline' : 'none';
// Pause-Hinweis aktualisieren
const box = logBoxes[tab];
const atBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 30;
pauseHint.classList.toggle('visible', !atBottom);
btnScroll.style.display = atBottom ? 'none' : 'inline';
if (autoScroll[tab]) box.scrollTop = box.scrollHeight;
}
function updateCount(source) {
logCounts.all++;
document.getElementById('count-all').textContent = logCounts.all;
// source → tab mapping
const tab = mapSourceToTab(source);
if (tab && logCounts[tab] !== undefined) {
logCounts[tab]++;
document.getElementById(`count-${tab}`).textContent = logCounts[tab];
}
}
function mapSourceToTab(source) {
if (source === 'gateway') return 'gateway';
if (source === 'rvs') return 'rvs';
if (source === 'proxy') return 'proxy';
if (source === 'bridge') return 'bridge';
if (source === 'server' || source === 'browser') return 'server';
if (source === 'trace') return 'trace';
return null;
}
const STATUS_LABELS = {
connected: 'Verbunden',
connecting: 'Verbinde...',
testing: 'Teste...',
disconnected: 'Getrennt',
error: 'Fehler',
not_configured: 'Nicht konfiguriert',
unknown: 'Nicht getestet',
};
function connectWS() {
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
ws = new WebSocket(`${proto}://${location.host}`);
ws.onopen = () => {
addLog('info', 'browser', 'Verbunden mit Diagnostic Server');
send({ action: 'load_chat_history' });
// Brain-Card initial laden (sonst zeigt sie "Lade...")
try { loadBrainStatus(); } catch {}
};
// Brain-Status periodisch refreshen damit die Card live bleibt
if (!window.__brainStatusInterval) {
window.__brainStatusInterval = setInterval(() => {
try { loadBrainStatus(); } catch {}
}, 15000);
}
ws.onclose = () => {
addLog('warn', 'browser', 'Verbindung zum Diagnostic Server verloren — Reconnect in 2s');
setTimeout(connectWS, 2000);
};
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === 'init') {
updateState(msg.state);
msg.logs.forEach(entry => addLog(entry.level, entry.source, entry.message, entry.ts));
return;
}
if (msg.type === 'state') { updateState(msg.state); return; }
if (msg.type === 'log') { addLog(msg.entry.level, msg.entry.source, msg.entry.message, msg.entry.ts); return; }
if (msg.type === 'agent_activity') {
updateThinkingIndicator(msg);
return;
}
if (msg.type === 'disk_status') {
updateDiskBanner(msg);
return;
}
if (msg.type === 'mode' && msg.payload) {
// Bridge hat den Modus geaendert (evtl. von anderer App/Diagnostic) — UI syncen
const mode = (msg.payload.mode || '').toLowerCase();
if (MODE_LABELS[mode]) {
updateModeUI(mode);
log("info", "server", `Mode-Sync: ${mode}`);
}
return;
}
if (msg.type === 'xtts_voices_list') {
const select = document.getElementById('diag-xtts-voice');
// Aktuelle Auswahl merken damit Rebuild sie nicht zerstoert
const previouslySelected = select.value;
while (select.options.length > 1) select.remove(1);
const voices = msg.payload?.voices || [];
for (const v of voices) {
const opt = document.createElement('option');
opt.value = v.name;
opt.textContent = `${v.name} (${(v.size / 1024).toFixed(0)}KB)`;
select.appendChild(opt);
}
// Wenn die vorherige Auswahl weiter existiert → wiederherstellen
if (previouslySelected && voices.some(v => v.name === previouslySelected)) {
select.value = previouslySelected;
}
document.getElementById('xtts-status').textContent = `XTTS: ${voices.length} Stimme(n) verfuegbar`;
document.getElementById('xtts-status').style.color = '#34C759';
renderVoiceList(voices);
return;
}
if (msg.type === 'xtts_voice_saved') {
document.getElementById('xtts-clone-status').textContent = `Stimme "${msg.payload?.name}" gespeichert!`;
document.getElementById('xtts-clone-status').style.color = '#34C759';
loadXTTSVoices(); // Liste neu laden
return;
}
if (msg.type === 'xtts_voice_exported') {
const p = msg.payload || {};
const status = document.getElementById('voice-status');
if (!p.ok || !p.data) {
if (status) status.textContent = '✗ Export fehlgeschlagen: ' + (p.error || 'unbekannt');
return;
}
// base64 → Blob → Download
try {
const bin = atob(p.data);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
const blob = new Blob([bytes], { type: 'application/gzip' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = (p.name || 'voice') + '.tar.gz';
document.body.appendChild(a); a.click();
setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 100);
if (status) status.textContent = '✓ ' + p.name + ' exportiert (' + (bytes.length/1024).toFixed(0) + ' KB)';
} catch (e) {
if (status) status.textContent = '✗ Decode fehlgeschlagen: ' + e.message;
}
return;
}
if (msg.type === 'xtts_voice_imported') {
const p = msg.payload || {};
const status = document.getElementById('voice-status');
if (p.ok) {
if (status) status.textContent = '✓ Stimme "' + p.name + '" importiert';
loadXTTSVoices();
} else {
if (status) status.textContent = '✗ Import fehlgeschlagen: ' + (p.error || 'unbekannt');
}
return;
}
if (msg.type === 'voice_config') {
document.getElementById('diag-tts-enabled').checked = msg.ttsEnabled !== false;
// XTTS-Voice setzen — Option hinzufuegen falls nicht vorhanden
const xttsSelect = document.getElementById('diag-xtts-voice');
const xttsVoice = msg.xttsVoice || '';
if (xttsVoice && !Array.from(xttsSelect.options).some(o => o.value === xttsVoice)) {
const opt = document.createElement('option');
opt.value = xttsVoice;
opt.textContent = xttsVoice;
xttsSelect.appendChild(opt);
}
xttsSelect.value = xttsVoice;
// Whisper-Modell wiederherstellen (falls gesetzt)
if (msg.whisperModel) {
const wSel = document.getElementById('diag-whisper-model');
if (wSel) wSel.value = msg.whisperModel;
}
// F5-TTS Tuning-Felder wiederherstellen (falls gesetzt)
const setIfPresent = (id, val) => {
const el = document.getElementById(id);
if (el && val !== undefined && val !== null && val !== '') el.value = val;
};
setIfPresent('diag-f5tts-model', msg.f5ttsModel);
setIfPresent('diag-f5tts-ckpt', msg.f5ttsCkptFile);
setIfPresent('diag-f5tts-vocab', msg.f5ttsVocabFile);
setIfPresent('diag-f5tts-cfg', msg.f5ttsCfgStrength);
setIfPresent('diag-f5tts-nfe', msg.f5ttsNfeStep);
return;
}
if (msg.type === 'service_status') {
updateServiceStatus(msg.payload || {});
return;
}
if (msg.type === 'voice_preview_audio') {
const statusEl = document.getElementById('voice-preview-status');
const audio = document.getElementById('voice-preview-audio');
const playBtn = document.getElementById('voice-preview-play');
if (playBtn) playBtn.disabled = false;
if (msg.error) {
if (statusEl) statusEl.textContent = '❌ Fehler: ' + msg.error;
return;
}
if (msg.base64 && audio) {
audio.src = 'data:audio/wav;base64,' + msg.base64;
audio.style.display = 'block';
audio.play().catch(() => {});
if (statusEl) statusEl.textContent = '✅ fertig';
}
return;
}
if (msg.type === 'voice_ready') {
const v = msg.payload?.voice || '';
const err = msg.payload?.error;
const ms = msg.payload?.loadMs;
const statusEl = document.getElementById('voice-status');
if (statusEl) {
if (err) {
statusEl.textContent = `⚠️ Stimme "${v}" Fehler: ${err}`;
statusEl.style.color = '#FF3B30';
} else {
statusEl.textContent = `✅ Stimme "${v || 'Standard'}" bereit${ms ? ` (${(ms/1000).toFixed(1)}s)` : ''}`;
statusEl.style.color = '#34C759';
}
setTimeout(() => { if (statusEl) statusEl.textContent = ''; }, 5000);
}
addLog('info', 'xtts', err ? `Voice "${v}": ${err}` : `Voice "${v || 'Standard'}" bereit`);
return;
}
if (msg.type === 'watchdog') {
const colors = { warning: '#FFD60A', fixing: '#FF9500', fixed: '#34C759', error: '#FF3B30' };
const color = colors[msg.status] || '#FFD60A';
addChat('error', `\u26A0\uFE0F Watchdog: ${msg.message}`, `system — ${msg.status}`);
addLog('warn', 'server', `Watchdog: ${msg.message}`);
return;
}
if (msg.type === 'chat_final') {
addChat('received', msg.text || '', 'chat:final');
return;
}
if (msg.type === 'file_from_aria') {
const p = msg.payload || {};
addAriaFile(p);
return;
}
if (msg.type === 'file_deleted') {
if (msg.path) markFileDeletedInChat(msg.path);
// Falls Datei-Manager grade offen ist: Liste refreshen
if (typeof loadFiles === 'function' && document.getElementById('tab-files') && document.getElementById('tab-files').classList.contains('visible')) {
loadFiles();
}
return;
}
if (msg.type === 'skill_created') {
addSkillCreatedBubble(msg.payload || {});
// Falls Skills-Tab offen: refreshen
if (document.getElementById('tab-skills') && document.getElementById('tab-skills').classList.contains('visible')) {
loadSkills();
}
return;
}
if (msg.type === 'chat_delta') { return; }
if (msg.type === 'chat_error') {
addChat('error', msg.error, 'chat:error');
return;
}
if (msg.type === 'rvs_chat') {
const p = msg.msg.payload || {};
const sender = p.sender || '?';
// ARIA-Antworten kommen schon via Gateway (chat:final) — nicht nochmal via RVS anzeigen
if (sender === 'aria') return;
const chatType = 'sent';
const label = sender === 'stt' ? '\uD83C\uDFA4 Spracheingabe' : `via RVS (${sender})`;
addChat(chatType, p.text || '?', label, { location: p.location });
return;
}
if (msg.type === 'proxy_result') {
if (msg.ok) {
addChat('received', msg.reply, 'Claude Proxy direkt');
} else {
addChat('error', msg.error, 'Claude Proxy Fehler');
}
if (msg.models && msg.models.length) showProxyModels(msg.models);
return;
}
if (msg.type === 'proxy_auth') {
const el = document.getElementById('proxy-auth');
el.style.display = 'block';
el.textContent = msg.error ? `Fehler: ${msg.error}` : msg.info;
return;
}
// core_auth WS-Event entfernt — aria-core ist raus.
// Live SSH + Desktop
if (msg.type?.startsWith('live_ssh_')) { handleLiveSSH(msg); return; }
if (msg.type === 'desktop_status') { handleDesktop(msg); return; }
if (msg.type === 'term_ready') {
document.getElementById('term-status').textContent = 'Verbunden — interaktives Terminal';
if (term) term.writeln('\x1b[32mVerbunden!\x1b[0m\r\n');
return;
}
if (msg.type === 'term_data') {
if (term) {
// base64 → Uint8Array (UTF-8 safe)
const raw = atob(msg.data);
const bytes = new Uint8Array(raw.length);
for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
term.write(bytes);
}
return;
}
if (msg.type === 'term_exit') {
if (term) term.writeln('\r\n\x1b[33m--- Session beendet ---\x1b[0m');
document.getElementById('term-status').textContent = 'Session beendet — Fenster schliessen oder erneut einloggen';
return;
}
if (msg.type === 'term_error') {
if (term) term.writeln('\r\n\x1b[31mFehler: ' + msg.error + '\x1b[0m');
document.getElementById('btn-proxy-login').disabled = false;
return;
}
if (msg.type === 'login_status') {
if (msg.status === 'done') {
const cs = document.getElementById('creds-status');
if (cs) { cs.textContent = 'Erfolgreich!'; cs.style.color = '#34C759'; }
} else if (msg.status === 'error') {
const cs = document.getElementById('creds-status');
if (cs) { cs.textContent = msg.error; cs.style.color = '#FF6B6B'; }
}
return;
}
if (msg.type === 'docker_logs') {
showDockerLogs(msg);
return;
}
// Chat-History (nach F5 / Reconnect)
if (msg.type === 'chat_history') {
chatBox.innerHTML = '';
if (msg.messages && msg.messages.length > 0) {
for (const m of msg.messages) {
if (m.type === 'aria_file') {
// ARIA-Datei-Bubble rekonstruieren (statt addAriaFile damit
// kein Auto-Scroll-Race waehrend des Bulk-Loads)
addAriaFile({ serverPath: m.serverPath, name: m.name, mimeType: m.mimeType, size: m.size });
continue;
}
const el = document.createElement('div');
el.className = `chat-msg ${m.type}`;
// [FILE: ...]-Marker rausfiltern (gleicher Filter wie addChat)
const cleaned = (m.text || '').replace(/\[FILE:\s*\/shared\/uploads\/[^\]]+\]/gi, '').replace(/\n{3,}/g, '\n\n').trim();
const escaped = escapeHtml(cleaned);
let linked = linkifyText(escaped);
// /shared/uploads/-Bildpfade auch im History inline rendern
// (gleicher Replace wie in addChat — sonst sieht man nach F5 nur Text-Pfade)
linked = linked.replace(/\/shared\/uploads\/[^\s<"]+\.(jpg|jpeg|png|gif|webp|svg|bmp)/gi, (match) => {
return `<a href="${match}" target="_blank">${match}</a><img src="${match}" class="chat-media" onclick="openLightbox('image','${match}')" onerror="this.style.display='none'">`;
});
const time = m.ts ? new Date(m.ts).toLocaleTimeString('de-DE') : '?';
el.innerHTML = `${linked}<div class="meta">${escapeHtml(m.meta)}${time}</div>`;
chatBox.appendChild(el);
}
chatBox.scrollTop = chatBox.scrollHeight;
}
return;
}
// session_restarted / openclaw_config WS-Events entfernt — aria-core ist raus.
if (msg.type === 'model_info') {
const el = document.getElementById('setting-model');
const st = document.getElementById('model-status');
if (el && msg.model) el.value = msg.model;
if (st) {
st.textContent = msg.info || msg.error || '';
st.style.color = msg.error ? '#FF6B6B' : '#34C759';
}
return;
}
if (msg.type === 'response') { return; }
};
}
function send(obj) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(obj));
}
}
function sendDiagAttachments() {
// Alle pending Dateien an RVS senden
for (const f of diagPendingFiles) {
send({ action: 'send_file', name: f.name, type: f.type, size: f.size, base64: f.base64 });
}
if (diagPendingFiles.length > 0) {
addChat('sent', `${diagPendingFiles.length} Anhang/Anhaenge`, 'Datei');
}
diagPendingFiles = [];
renderDiagPending();
}
function testGateway() {
const input = document.getElementById('chat-input');
const text = input.value.trim();
if (!text && diagPendingFiles.length === 0) return;
if (diagPendingFiles.length > 0) sendDiagAttachments();
if (text) {
addChat('sent', text, 'Gateway direkt');
send({ action: 'test_gateway', text });
}
input.value = '';
}
function testRVS() {
const input = document.getElementById('chat-input');
const text = input.value.trim();
if (!text && diagPendingFiles.length === 0) return;
if (diagPendingFiles.length > 0) sendDiagAttachments();
if (text) {
addChat('sent', text, 'via RVS');
send({ action: 'test_rvs', text });
}
input.value = '';
}
function testProxyBtn() {
send({ action: 'test_proxy', text: 'Antworte mit genau einem Wort: Ping' });
}
function checkProxyAuth() {
send({ action: 'check_proxy_auth' });
}
let term = null;
let termFitAddon = null;
let termAction = null; // Welche Aktion beim Terminal-Start gesendet wird
function openTermModal(title, action) {
termAction = action;
document.querySelector('#term-modal .modal-header h3').textContent = title;
document.getElementById('term-modal').classList.add('open');
document.getElementById('term-status').textContent = 'Starte Terminal...';
if (typeof Terminal === 'undefined') {
const s = document.createElement('script');
s.src = 'https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js';
s.onload = () => {
const s2 = document.createElement('script');
s2.src = 'https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js';
s2.onload = () => initTerminal();
document.head.appendChild(s2);
};
document.head.appendChild(s);
} else {
initTerminal();
}
}
function startProxyLogin() {
document.getElementById('btn-proxy-login').disabled = true;
openTermModal('Claude Login Terminal (aria-proxy)', { action: 'proxy_login' });
}
// openCoreTerminal entfernt — aria-core ist raus.
function closeTermModal() {
document.getElementById('term-modal').classList.remove('open');
const proxyBtn = document.getElementById('btn-proxy-login');
if (proxyBtn) proxyBtn.disabled = false;
// Terminal aufraeumen
if (term) { term.dispose(); term = null; }
}
function initTerminal() {
const container = document.getElementById('terminal-container');
container.innerHTML = '';
term = new Terminal({
cursorBlink: true,
fontSize: 14,
fontFamily: "'DejaVu Sans Mono', 'Courier New', monospace",
theme: { background: '#000000', foreground: '#E0E0F0' },
});
if (window.FitAddon) {
termFitAddon = new FitAddon.FitAddon();
term.loadAddon(termFitAddon);
term.open(container);
termFitAddon.fit();
} else {
term.open(container);
}
// Tastatureingabe → Server (UTF-8 safe)
term.onData((data) => {
const encoder = new TextEncoder();
const bytes = encoder.encode(data);
let b64 = '';
for (let i = 0; i < bytes.length; i++) b64 += String.fromCharCode(bytes[i]);
send({ action: 'term_input', data: btoa(b64) });
});
const containerName = termAction?.action === 'core_terminal' ? 'aria-core' : 'aria-proxy';
term.writeln('\x1b[33mVerbinde mit ' + containerName + '...\x1b[0m');
send(termAction);
}
// Resize bei Fensteraenderung
window.addEventListener('resize', () => { if (termFitAddon && term) termFitAddon.fit(); });
function toggleCredsBox() {
const box = document.getElementById('proxy-creds-box');
box.style.display = box.style.display === 'none' ? 'block' : 'none';
}
function submitCredentials() {
const input = document.getElementById('creds-input').value.trim();
const status = document.getElementById('creds-status');
if (!input) { status.textContent = 'Bitte JSON einfuegen'; status.style.color = '#FF6B6B'; return; }
try {
JSON.parse(input);
} catch (e) {
status.textContent = 'Ungueltig: Kein gueltiges JSON'; status.style.color = '#FF6B6B'; return;
}
status.textContent = 'Speichere...'; status.style.color = '#FFD60A';
send({ action: 'write_credentials', credentials: input });
}
function loadDockerLogs() {
if (!DOCKER_TABS.includes(activeTab)) return;
send({ action: 'docker_logs', tab: activeTab, tail: 200 });
}
function updateState(state) {
// Brain-Card holt ihre Daten via loadBrainStatus() (fetch /api/brain/health).
// state.gateway ist Reststruktur aus OpenClaw-Zeit — wir ignorieren das hier.
// RVS
const rvs = state.rvs || {};
document.getElementById('rvs-dot').className = `dot ${rvs.status || 'disconnected'}`;
document.getElementById('rvs-status').textContent = STATUS_LABELS[rvs.status] || rvs.status;
document.getElementById('rvs-error').textContent = rvs.lastError || '';
// Proxy
const proxy = state.proxy || {};
document.getElementById('proxy-dot').className = `dot ${proxy.status || 'unknown'}`;
document.getElementById('proxy-status').textContent = STATUS_LABELS[proxy.status] || proxy.status;
document.getElementById('proxy-error').textContent = proxy.lastError || '';
document.getElementById('btn-proxy-test').disabled = proxy.status === 'testing';
if (proxy.models && proxy.models.length) showProxyModels(proxy.models);
// Buttons
document.getElementById('btn-gw').disabled = gw.status !== 'connected';
document.getElementById('btn-rvs').disabled = rvs.status !== 'connected';
}
function addLog(level, source, message, ts) {
const time = ts ? new Date(ts).toLocaleTimeString('de-DE') : new Date().toLocaleTimeString('de-DE');
const line = `${time} [${source}] ${message}`;
// Trace-Eintraege nur in Trace-Tab (nicht in Alle)
if (source === 'trace') {
const pipeLevel = level === 'error' ? 'trace-err' : level === 'info' && message.includes('>>>') ? 'trace-ok' : 'trace-step';
appendToLog('trace', pipeLevel, `${time} ${message}`);
logCounts.trace++;
document.getElementById('count-trace').textContent = logCounts.trace;
return;
}
// In "Alle" und in den passenden Tab schreiben
appendToLog('all', level, line);
const tab = mapSourceToTab(source);
if (tab) appendToLog(tab, level, line);
updateCount(source);
}
function appendToLog(tab, level, line) {
const box = logBoxes[tab];
const el = document.createElement('div');
el.className = `log-entry ${level}`;
el.textContent = line;
box.appendChild(el);
if (autoScroll[tab]) box.scrollTop = box.scrollHeight;
}
const MEDIA_IMG = /\.(jpe?g|png|gif|webp|svg|bmp|ico)$/i;
const MEDIA_VID = /\.(mp4|webm|ogg|mov)$/i;
const FILE_PDF = /\.pdf$/i;
function linkifyText(escaped) {
// URLs in escaped HTML erkennen und ersetzen
return escaped.replace(/(https?:\/\/[^\s<&]+)/g, function(url) {
const decoded = url.replace(/&amp;/g, '&');
const pathname = new URL(decoded).pathname;
if (MEDIA_IMG.test(pathname)) {
return `<a href="${url}" target="_blank">${url}</a><img src="${url}" class="chat-media" onclick="event.stopPropagation();openLightbox('img','${url}')">`;
}
if (MEDIA_VID.test(pathname)) {
return `<a href="${url}" target="_blank">${url}</a><video src="${url}" class="chat-media" controls onclick="event.stopPropagation();openLightbox('video','${url}')"></video>`;
}
return `<a href="${url}" target="_blank" rel="noopener">${url}</a>`;
});
}
// Debug-Toggle: TTS-aufbereitete Variante unter ARIA-Nachrichten einblenden
let showTtsDebug = localStorage.getItem('aria-show-tts-debug') === '1';
function toggleTtsDebug() {
showTtsDebug = !showTtsDebug;
localStorage.setItem('aria-show-tts-debug', showTtsDebug ? '1' : '0');
const el = document.getElementById('tts-debug-toggle');
if (el) el.checked = showTtsDebug;
}
// Debug-Toggle: GPS-Position unter User-Nachrichten einblenden (nur Diagnostic).
// App zeigt's bewusst nicht — die Position geht nur an aria-core.
let showGpsDebug = localStorage.getItem('aria-show-gps-debug') === '1';
function toggleGpsDebug() {
showGpsDebug = !showGpsDebug;
localStorage.setItem('aria-show-gps-debug', showGpsDebug ? '1' : '0');
const el = document.getElementById('gps-debug-toggle');
if (el) el.checked = showGpsDebug;
}
// Minimal-JS-Port von clean_text_for_tts() (Bridge) — reine Anzeige
function previewTtsText(text) {
if (!text) return '';
// <voice>...</voice>
const vm = text.match(/<voice>([\s\S]*?)<\/voice>/i);
if (vm) text = vm[1];
let t = text;
t = t.replace(/```[\s\S]*?```/g, '. ');
t = t.replace(/`[^`]+`/g, '');
t = t.replace(/\*\*([^*]+)\*\*/g, '$1');
t = t.replace(/\*([^*]+)\*/g, '$1');
t = t.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
t = t.replace(/https?:\/\/\S+/g, 'ein Link');
t = t.replace(/^#{1,6}\s*/gm, '');
t = t.replace(/^>\s*/gm, '');
t = t.replace(/^[\-\*]\s+/gm, '');
t = t.replace(/(\d+)GB\b/g, '$1 Gigabyte');
t = t.replace(/(\d+)MB\b/g, '$1 Megabyte');
t = t.replace(/%/g, ' Prozent');
t = t.replace(/\bCPU\b/g, 'C P U').replace(/\bAPI\b/g, 'A P I').replace(/\bRAM\b/g, 'R A M');
t = t.replace(/\n{2,}/g, '. ').replace(/\n/g, ', ').replace(/\s{2,}/g, ' ');
return t.trim();
}
function addChat(type, text, meta, options) {
// [FILE: /shared/uploads/aria_xxx.ext]-Marker aus dem Antworttext entfernen —
// die Datei kommt separat via file_from_aria-Event als eigene Bubble.
// /gi entfernt mehrere Marker, falls ARIA mehrere Dateien in einer Antwort liefert.
if (text) text = text.replace(/\[FILE:\s*\/shared\/uploads\/[^\]]+\]/gi, '').replace(/\n{3,}/g, '\n\n').trim();
const escaped = escapeHtml(text);
let linked = linkifyText(escaped);
// /shared/uploads/ Pfade als Inline-Bilder anzeigen
linked = linked.replace(/\/shared\/uploads\/[^\s<"]+\.(jpg|jpeg|png|gif)/gi, (match) => {
return `<a href="${match}" target="_blank">${match}</a><img src="${match}" class="chat-media" onclick="openLightbox('image','${match}')" onerror="this.style.display='none'">`;
});
// Optional: TTS-Variante als zusaetzliches Block unter der Nachricht
let ttsBlock = '';
if (showTtsDebug && type === 'received') {
const ttsText = (options && options.ttsText) || previewTtsText(text);
if (ttsText && ttsText !== text) {
ttsBlock = `<div style="margin-top:6px;padding:4px 8px;background:rgba(0,150,255,0.08);border-left:2px solid #0096FF;font-size:11px;color:#88AACC;"><span style="color:#0096FF;font-weight:bold;">TTS:</span> ${escapeHtml(ttsText)}</div>`;
}
}
// Optional: GPS-Position als Block unter User-Nachrichten (nur Diagnostic)
let gpsBlock = '';
if (showGpsDebug && options && options.location) {
const loc = options.location;
const lat = typeof loc.lat === 'number' ? loc.lat.toFixed(6) : '?';
const lon = typeof loc.lon === 'number' ? loc.lon.toFixed(6) : (typeof loc.lng === 'number' ? loc.lng.toFixed(6) : '?');
if (lat !== '?' && lon !== '?') {
const mapLink = `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}#map=16/${lat}/${lon}`;
gpsBlock = `<div style="margin-top:6px;padding:4px 8px;background:rgba(52,199,89,0.08);border-left:2px solid #34C759;font-size:11px;color:#88BB99;"><span style="color:#34C759;font-weight:bold;">📍 GPS:</span> <a href="${mapLink}" target="_blank" rel="noopener" style="color:#88BB99;text-decoration:underline;">${lat}, ${lon}</a></div>`;
}
}
const html = `${linked}${ttsBlock}${gpsBlock}<div class="meta">${escapeHtml(meta)}${new Date().toLocaleTimeString('de-DE')}</div>`;
// Thinking-Indikator ausblenden bei neuer Nachricht
updateThinkingIndicator({ activity: 'idle' });
// In beide Chat-Boxen schreiben (normal + Vollbild)
for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
if (!box) continue;
const el = document.createElement('div');
el.className = `chat-msg ${type}`;
el.innerHTML = html;
box.appendChild(el);
box.scrollTop = box.scrollHeight;
}
}
/** ARIA hat eine Datei rausgegeben — als eigene Bubble mit Klick-Handler. */
function addAriaFile(p) {
const name = p.name || 'datei';
const serverPath = p.serverPath || '';
const mimeType = p.mimeType || '';
const sizeKB = p.size ? Math.round(p.size / 1024) : 0;
const isImage = mimeType.startsWith('image/');
const isPdf = mimeType === 'application/pdf';
const url = serverPath; // Diagnostic-Server liefert /shared/* aus
const sizeStr = sizeKB > 1024 ? `${(sizeKB/1024).toFixed(1)}MB` : `${sizeKB}KB`;
const icon = isImage ? '🖼️' : isPdf ? '📄' : '📎';
const deleted = !!p.deleted;
let html;
if (deleted) {
html = `<div style="font-weight:bold;color:#FF9500;">${icon} ${escapeHtml(name)} — vom Benutzer gelöscht</div>` +
` <span style="color:#888;font-size:11px;">(${escapeHtml(mimeType)})</span>` +
`<div style="margin-top:4px;font-size:10px;color:#666;font-family:monospace;text-decoration:line-through;">${escapeHtml(serverPath)}</div>` +
`<div class="meta">ARIA-Datei (gelöscht)</div>`;
} else {
const linkAttrs = (isImage || isPdf)
? `href="${url}" target="_blank" rel="noopener"`
: `href="${url}" download="${escapeHtml(name)}"`;
let preview = '';
if (isImage) {
preview = `<img src="${url}" class="chat-media" onclick="openLightbox('image','${url}')" onerror="this.style.display='none'" style="margin-top:6px;">`;
}
html = `<div style="font-weight:bold;">${icon} ARIA hat eine Datei erstellt</div>` +
`<a ${linkAttrs} style="color:#0096FF;text-decoration:underline;">${escapeHtml(name)}</a>` +
` <span style="color:#888;font-size:11px;">(${escapeHtml(mimeType)}, ${sizeStr})</span>` +
preview +
`<div style="margin-top:4px;font-size:10px;color:#666;font-family:monospace;">${escapeHtml(serverPath)}</div>` +
`<div class="meta">ARIA-Datei — ${new Date().toLocaleTimeString('de-DE')}</div>`;
}
for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
if (!box) continue;
const el = document.createElement('div');
el.className = 'chat-msg received';
el.dataset.ariaFilePath = serverPath;
if (deleted) el.dataset.deleted = '1';
el.innerHTML = html;
box.appendChild(el);
box.scrollTop = box.scrollHeight;
}
}
/** ARIA hat einen Skill erstellt — als auffaellige Bubble anzeigen. */
function addSkillCreatedBubble(skill) {
const name = skill.name || '(unbenannt)';
const desc = skill.description || '(ohne Beschreibung)';
const execMode = skill.execution || 'bash';
const active = skill.active !== false;
const setupErr = skill.setup_error
? `<div style="color:#FF6B6B;font-size:11px;margin-top:4px;">⚠ Setup-Fehler: ${escapeHtml(skill.setup_error.slice(0,200))}</div>`
: '';
const statusBadge = active
? '<span style="color:#3FFF3F;font-size:11px;">aktiv</span>'
: '<span style="color:#FF9500;font-size:11px;">deaktiviert</span>';
const html = `
<div style="font-weight:bold;color:#FFD60A;">🛠 ARIA hat einen neuen Skill erstellt</div>
<div style="margin-top:4px;color:#E0E0F0;">
<strong>${escapeHtml(name)}</strong>
<span style="color:#8888AA;font-size:11px;margin-left:6px;">(${escapeHtml(execMode)}, ${statusBadge})</span>
</div>
<div style="color:#8888AA;font-size:12px;margin-top:2px;">${escapeHtml(desc)}</div>
${setupErr}
<div class="meta">
ARIA-Skill — ${new Date().toLocaleTimeString('de-DE')} ·
<a href="#" onclick="event.preventDefault();switchMainTab('skills');" style="color:#FFD60A;">im Skills-Tab ansehen</a>
</div>`;
for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
if (!box) continue;
const el = document.createElement('div');
el.className = 'chat-msg received';
el.style.borderLeft = '3px solid #FFD60A';
el.innerHTML = html;
box.appendChild(el);
box.scrollTop = box.scrollHeight;
}
}
/** Wenn der Server file_deleted broadcastet: alle Bubbles mit
diesem serverPath rerendern als "geloescht" markieren. */
function markFileDeletedInChat(serverPath) {
for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
if (!box) continue;
const bubbles = box.querySelectorAll(`[data-aria-file-path="${CSS.escape(serverPath)}"]`);
bubbles.forEach(el => {
if (el.dataset.deleted === '1') return;
el.dataset.deleted = '1';
const name = (serverPath.split('/').pop()) || 'datei';
el.innerHTML = `<div style="font-weight:bold;color:#FF9500;">📎 ${escapeHtml(name)} — vom Benutzer gelöscht</div>` +
`<div style="margin-top:4px;font-size:10px;color:#666;font-family:monospace;text-decoration:line-through;">${escapeHtml(serverPath)}</div>` +
`<div class="meta">ARIA-Datei (gelöscht)</div>`;
});
}
}
let chatFullscreen = false;
function toggleChatFullscreen() {
const modal = document.getElementById('chat-fullscreen');
chatFullscreen = !chatFullscreen;
if (chatFullscreen) {
modal.style.display = 'flex';
// Chat-Inhalt synchronisieren
const fsBox = document.getElementById('chat-box-fs');
fsBox.innerHTML = chatBox.innerHTML;
fsBox.scrollTop = fsBox.scrollHeight;
document.getElementById('chat-input-fs').focus();
} else {
modal.style.display = 'none';
}
}
function testGatewayFS() {
const input = document.getElementById('chat-input-fs');
const text = input.value.trim();
if (!text) return;
addChat('sent', text, 'Gateway direkt');
send({ action: 'test_gateway', text });
input.value = '';
}
function testRVSFS() {
const input = document.getElementById('chat-input-fs');
const text = input.value.trim();
if (!text) return;
addChat('sent', text, 'via RVS');
send({ action: 'test_rvs', text });
input.value = '';
}
// Escape schliesst Vollbild-Chat
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && chatFullscreen) toggleChatFullscreen();
});
// ── Thinking-Indikator ─────────────────────────────
let thinkingTimeout = null;
const TOOL_LABELS = {
'Bash': '\uD83D\uDDA5\uFE0F Shell-Befehl',
'WebFetch': '\uD83C\uDF10 Webseite abrufen',
'WebSearch': '\uD83D\uDD0D Suche',
'Read': '\uD83D\uDCC4 Datei lesen',
'Write': '\u270D\uFE0F Datei schreiben',
'Edit': '\u270D\uFE0F Datei bearbeiten',
'Grep': '\uD83D\uDD0D Code durchsuchen',
'Glob': '\uD83D\uDCC1 Dateien suchen',
'Agent': '\uD83E\uDD16 Sub-Agent',
};
// ── Service-Status Banner (Gamebox: F5-TTS / Whisper Lade-Status) ──
// Aggregiert die Status-Infos der Bridges. Wenn irgendwas am Laden
// ist, zeigt das Banner unten rechts. Sobald alles auf 'ready' ist,
// bleibt's einen Moment und wird dann vom User weggeklickt (oder
// nach 8s automatisch).
const _serviceState = {}; // { f5tts: {state, model, ...}, whisper: {...} }
let _serviceFadeTimer = null;
function updateServiceStatus(p) {
const svc = p.service || '?';
_serviceState[svc] = p;
const banner = document.getElementById('service-status-banner');
const list = document.getElementById('service-status-list');
const icon = document.getElementById('service-status-icon');
const closeBtn = document.getElementById('service-status-close');
// Liste neu aufbauen
list.innerHTML = '';
let anyLoading = false, anyError = false;
const labels = { f5tts: 'F5-TTS', whisper: 'Whisper STT' };
for (const [s, info] of Object.entries(_serviceState)) {
const row = document.createElement('div');
row.style.cssText = 'display:flex;align-items:center;gap:6px;';
let dot = '⚫', color = '#666680', text = '';
if (info.state === 'loading') {
dot = '⏳'; color = '#FFD60A'; anyLoading = true;
text = `${labels[s] || s}: laedt${info.model ? ' ' + info.model : ''}...`;
} else if (info.state === 'ready') {
dot = '✅'; color = '#34C759';
const sec = info.loadSeconds ? ` (${info.loadSeconds.toFixed(1)}s)` : '';
text = `${labels[s] || s}: bereit${info.model ? ' ' + info.model : ''}${sec}`;
} else if (info.state === 'error') {
dot = '❌'; color = '#FF3B30'; anyError = true;
text = `${labels[s] || s}: Fehler ${info.error || ''}`;
} else {
text = `${labels[s] || s}: ${info.state}`;
}
row.innerHTML = `<span style="color:${color}">${dot}</span><span>${text}</span>`;
list.appendChild(row);
}
// Icon spiegelt Gesamt-Status
if (anyError) icon.innerHTML = '&#x274C;';
else if (anyLoading) icon.innerHTML = '&#x23F3;';
else icon.innerHTML = '&#x2705;';
banner.style.display = 'block';
// Wenn alles ready (kein Loading, kein Error): X-Button anzeigen
// + nach 8s automatisch wegfaden
if (!anyLoading && !anyError) {
closeBtn.style.display = 'block';
clearTimeout(_serviceFadeTimer);
_serviceFadeTimer = setTimeout(() => {
banner.style.display = 'none';
}, 8000);
} else {
closeBtn.style.display = 'none';
clearTimeout(_serviceFadeTimer);
}
}
function updateThinkingIndicator(msg) {
const indicators = [
document.getElementById('thinking-indicator'),
document.getElementById('thinking-indicator-fs'),
];
const texts = [
document.getElementById('thinking-text'),
document.getElementById('thinking-text-fs'),
];
if (msg.activity === 'idle') {
indicators.forEach(el => { if (el) el.style.display = 'none'; });
if (thinkingTimeout) { clearTimeout(thinkingTimeout); thinkingTimeout = null; }
return;
}
let label = 'ARIA denkt...';
if (msg.activity === 'tool' && msg.tool) {
label = TOOL_LABELS[msg.tool] || `\uD83D\uDD27 ${msg.tool}`;
} else if (msg.activity === 'assistant') {
label = 'ARIA schreibt...';
}
indicators.forEach((el, i) => {
if (!el) return;
// Haupt-Indicator ist flex (Abbrechen-Button rechts), Vollbild-Variante block
el.style.display = i === 0 ? 'flex' : 'block';
});
texts.forEach(el => { if (el) el.textContent = label; });
// Auto-Hide nach 2min (falls idle Event verpasst wird — ARIA arbeitet max 15min)
if (thinkingTimeout) clearTimeout(thinkingTimeout);
thinkingTimeout = setTimeout(() => {
indicators.forEach(el => { if (el) el.style.display = 'none'; });
}, 120000);
}
// ── XTTS Panel ─────────────────────────────
function renderVoiceList(voices) {
const box = document.getElementById('xtts-voice-list');
if (!box) return;
if (!voices || voices.length === 0) {
box.innerHTML = '<div style="color:#555570;font-size:11px;">Noch keine eigenen Stimmen vorhanden.</div>';
return;
}
let html = '<div style="color:#8888AA;font-size:11px;margin-bottom:4px;">Geclonte Stimmen:</div>';
html += '<div style="display:flex;flex-direction:column;gap:4px;">';
for (const v of voices) {
const esc = (s) => String(s).replace(/[&<>"']/g, c => ({ "&":"&amp;", "<":"&lt;", ">":"&gt;", '"':"&quot;", "'":"&#39;" }[c]));
const jsName = esc(v.name).replace(/'/g, "\\'");
html += `<div style="display:flex;align-items:center;gap:8px;background:#1E1E2E;border-radius:4px;padding:4px 8px;font-size:12px;">`
+ `<span style="flex:1;color:#E0E0F0;">${esc(v.name)}</span>`
+ `<span style="color:#555570;font-size:10px;">${(v.size/1024).toFixed(0)}KB</span>`
+ `<button class="btn secondary" onclick="openVoicePreview('${jsName}')" style="padding:2px 8px;font-size:12px;" title="Stimme anhoeren">▶</button>`
+ `<button class="btn secondary" onclick="exportXttsVoice('${jsName}')" style="padding:2px 8px;font-size:10px;color:#0096FF;" title="Stimme exportieren">⬇</button>`
+ `<button class="btn secondary" onclick="deleteXttsVoice('${jsName}')" style="padding:2px 8px;font-size:10px;color:#FF6B6B;" title="Stimme loeschen">X</button>`
+ `</div>`;
}
html += '</div>';
html += '<div style="margin-top:6px;display:flex;gap:6px;">';
html += '<input type="file" id="xtts-voice-import-file" accept=".tar.gz,.tgz,application/gzip" style="display:none" onchange="importXttsVoice(event)">';
html += '<button class="btn secondary" onclick="document.getElementById(\'xtts-voice-import-file\').click()" style="padding:2px 10px;font-size:11px;">⬆ Stimme importieren</button>';
html += '</div>';
box.innerHTML = html;
}
// ── Voice Export/Import ────────────────────────────
function exportXttsVoice(name) {
const status = document.getElementById('voice-status');
if (status) status.textContent = '⏳ Exportiere ' + name + ' ...';
// RVS request/response — Antwort kommt via 'xtts_voice_exported'
send({ action: 'xtts_export_voice', name });
}
async function importXttsVoice(event) {
const file = event.target.files[0];
if (!file) return;
const status = document.getElementById('voice-status');
try {
// Datei als base64 lesen (kann mehrere MB sein)
const buf = await file.arrayBuffer();
const bytes = new Uint8Array(buf);
let bin = '';
const chunk = 8192;
for (let i = 0; i < bytes.length; i += chunk) {
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
}
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.`)) {
event.target.value = '';
return;
}
if (status) status.textContent = '⏳ Lade hoch (' + (file.size/1024).toFixed(0) + ' KB)...';
send({ action: 'xtts_import_voice', name: baseName, data: b64 });
} catch (e) {
if (status) status.textContent = '✗ ' + e.message;
} finally {
event.target.value = '';
}
}
// ── Voice Preview Modal ─────────────────────────
const VOICE_PREVIEW_DEFAULT = 'Hallo, ich bin ARIA. Das hier ist ein kleiner Test damit du meine Stimme beurteilen kannst.';
const PREVIEW_SPEED_DEFAULT = 1.0;
const PREVIEW_SPEED_MIN = 0.1;
const PREVIEW_SPEED_MAX = 5.0;
let currentPreviewVoice = '';
let currentPreviewSpeed = PREVIEW_SPEED_DEFAULT;
function _refreshPreviewSpeedLabel() {
const el = document.getElementById('voice-preview-speed-value');
if (el) el.textContent = currentPreviewSpeed.toFixed(1) + ' x';
}
function adjustPreviewSpeed(delta) {
const next = Math.round((currentPreviewSpeed + delta) * 10) / 10;
if (next < PREVIEW_SPEED_MIN || next > PREVIEW_SPEED_MAX) return;
currentPreviewSpeed = next;
_refreshPreviewSpeedLabel();
}
function openVoicePreview(name) {
currentPreviewVoice = name;
// Speed bei jedem Oeffnen zuruecksetzen — bewusst kein persist
currentPreviewSpeed = PREVIEW_SPEED_DEFAULT;
_refreshPreviewSpeedLabel();
document.getElementById('voice-preview-name').textContent = name;
// Text bei jedem Oeffnen zuruecksetzen
document.getElementById('voice-preview-text').value = VOICE_PREVIEW_DEFAULT;
document.getElementById('voice-preview-status').textContent = '';
const audio = document.getElementById('voice-preview-audio');
audio.style.display = 'none';
audio.src = '';
document.getElementById('voice-preview-modal').style.display = 'flex';
}
function closeVoicePreview() {
document.getElementById('voice-preview-modal').style.display = 'none';
const audio = document.getElementById('voice-preview-audio');
try { audio.pause(); } catch {}
}
function playVoicePreview() {
const text = (document.getElementById('voice-preview-text').value || '').trim();
if (!text) {
document.getElementById('voice-preview-status').textContent = 'Text leer';
return;
}
document.getElementById('voice-preview-status').textContent = '⏳ Rendere...';
document.getElementById('voice-preview-play').disabled = true;
send({
action: 'preview_voice',
voice: currentPreviewVoice,
text,
speed: currentPreviewSpeed,
});
}
function deleteXttsVoice(name) {
if (!confirm(`Stimme "${name}" endgueltig loeschen?`)) return;
send({ action: 'xtts_delete_voice', name });
// Bei aktueller Auswahl: auf Default zuruecksetzen
const sel = document.getElementById('diag-xtts-voice');
if (sel.value === name) { sel.value = ''; sendVoiceConfig(); }
}
// Legacy no-op (XTTS ist jetzt die einzige Engine, kein Panel-Toggle noetig)
function toggleXTTSPanel() {
void 0;
if (engine === 'xtts') loadXTTSVoices();
}
function loadXTTSVoices() {
send({ action: 'xtts_list_voices' });
}
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.length; i += 8192) {
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 8192));
}
return btoa(binary);
}
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; }
const status = document.getElementById('xtts-clone-status');
status.textContent = `Lade ${files.length} Datei(en)...`;
status.style.color = '#FFD60A';
try {
const samples = [];
for (let i = 0; i < files.length; i++) {
status.textContent = `Lese Datei ${i + 1}/${files.length}: ${files[i].name}...`;
const buffer = await files[i].arrayBuffer();
const base64 = arrayBufferToBase64(buffer);
samples.push({ base64, name: files[i].name, size: files[i].size });
}
const totalSize = samples.reduce((s, f) => s + f.size, 0);
status.textContent = `Sende ${samples.length} Sample(s) (${(totalSize / 1024).toFixed(0)}KB)...`;
send({ action: 'voice_upload', name, samples });
status.textContent = `Gesendet — warte auf Bestaetigung vom XTTS-Server...`;
} catch (err) {
status.textContent = `Fehler: ${err.message}`;
status.style.color = '#FF3B30';
}
}
// ── Diagnostic Anhang-Handling ─────────────
let diagPendingFiles = [];
function handleDiagFileSelect(files) {
for (const file of files) {
const reader = new FileReader();
reader.onload = () => {
const base64 = reader.result.split(',')[1];
diagPendingFiles.push({ name: file.name, type: file.type, size: file.size, base64 });
renderDiagPending();
};
reader.readAsDataURL(file);
}
}
function handleDiagPaste(event) {
const items = event.clipboardData?.items;
if (!items) return;
for (const item of items) {
if (item.kind === 'file') {
event.preventDefault();
const file = item.getAsFile();
if (file) handleDiagFileSelect([file]);
}
}
}
function renderDiagPending() {
const container = document.getElementById('diag-pending-attachments');
if (diagPendingFiles.length === 0) {
container.style.display = 'none';
return;
}
container.style.display = 'flex';
container.innerHTML = diagPendingFiles.map((f, i) => {
const isImage = f.type.startsWith('image/');
const preview = isImage ? `<img src="data:${f.type};base64,${f.base64}" style="width:40px;height:40px;border-radius:4px;object-fit:cover;">` : `<span style="font-size:20px;">&#x1F4C4;</span>`;
return `<div style="position:relative;display:inline-block;">
${preview}
<span onclick="removeDiagPending(${i})" style="position:absolute;top:-4px;right:-4px;width:16px;height:16px;border-radius:8px;background:#FF3B30;color:#fff;font-size:10px;cursor:pointer;display:flex;align-items:center;justify-content:center;">X</span>
</div>`;
}).join('') + `<span style="color:#8888AA;font-size:11px;margin-left:4px;">${diagPendingFiles.length} Datei(en)</span>
<span onclick="diagPendingFiles=[];renderDiagPending();" style="color:#FF3B30;font-size:11px;cursor:pointer;margin-left:8px;">Alle X</span>`;
}
function removeDiagPending(idx) {
diagPendingFiles.splice(idx, 1);
renderDiagPending();
}
// doctorFix + ariaRestart entfernt — aria-core ist raus.
// Fuer Container-Restarts: restartContainer('aria-bridge'|'aria-brain'|'aria-qdrant').
// ── Voice Settings Export/Import ──────────────────────
function exportVoiceSettings() {
window.location.href = '/api/voice-config-export';
}
async function importVoiceSettings(event) {
const file = event.target.files[0];
if (!file) return;
try {
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.`)) {
event.target.value = '';
return;
}
const r = await fetch('/api/voice-config-import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: text,
});
const d = await r.json();
if (d.ok) {
alert('✓ ' + d.message);
send({ action: 'get_voice_config' });
} else {
alert('Import fehlgeschlagen: ' + d.error);
}
} catch (e) {
alert('Ungültige Datei: ' + e.message);
} finally {
event.target.value = '';
}
}
// ── 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;
statusEl.innerHTML = '⏳ Stoppe Container, lösche Daten...';
try {
const r = await fetch('/api/wipe-all', { method: 'POST' });
const d = await r.json();
if (d.ok) {
statusEl.innerHTML = `<span style="color:#3FFF3F;">✓ ${d.message}</span>`;
setTimeout(() => location.reload(), 3000);
} else {
statusEl.innerHTML = `<span style="color:#FF6B6B;">✗ ${d.error}</span>`;
}
} catch (e) {
statusEl.innerHTML = `<span style="color:#FF6B6B;">✗ ${e.message}</span>`;
}
}
// ── 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 (statusEl) statusEl.innerHTML = `⏳ Restart ${name}...`;
try {
const r = await fetch('/api/container-restart', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
});
const d = await r.json();
if (d.ok) {
if (statusEl) statusEl.innerHTML = `<span style="color:#3FFF3F;">✓ ${name} neu gestartet</span>`;
addLog('info', 'server', `${name} neu gestartet`);
} else {
if (statusEl) statusEl.innerHTML = `<span style="color:#FF6B6B;">✗ ${d.error}</span>`;
addLog('error', 'server', `${name} Restart: ${d.error}`);
}
} catch (e) {
if (statusEl) statusEl.innerHTML = `<span style="color:#FF6B6B;">✗ ${e.message}</span>`;
}
}
// ariaSessionReset entfernt — aria-core ist raus.
// ── Abbrechen ──────────────────────────────
function cancelRequest() {
send({ action: 'cancel_request' });
updateThinkingIndicator({ activity: 'idle' });
addChat('error', 'Anfrage abgebrochen', 'system');
}
// ── Stimmen-Config ──────────────────────────
function sendVoiceConfig() {
const ttsEnabled = document.getElementById('diag-tts-enabled').checked;
const xttsVoice = document.getElementById('diag-xtts-voice').value;
const whisperModel = document.getElementById('diag-whisper-model').value;
const f5ttsModel = document.getElementById('diag-f5tts-model')?.value || '';
const f5ttsCkptFile = document.getElementById('diag-f5tts-ckpt')?.value || '';
const f5ttsVocabFile = document.getElementById('diag-f5tts-vocab')?.value || '';
const f5ttsCfgRaw = document.getElementById('diag-f5tts-cfg')?.value || '';
const f5ttsNfeRaw = document.getElementById('diag-f5tts-nfe')?.value || '';
const f5ttsCfgStrength = f5ttsCfgRaw ? parseFloat(f5ttsCfgRaw) : undefined;
const f5ttsNfeStep = f5ttsNfeRaw ? parseInt(f5ttsNfeRaw, 10) : undefined;
send({
action: 'send_voice_config',
ttsEnabled, xttsVoice, whisperModel,
f5ttsModel, f5ttsCkptFile, f5ttsVocabFile,
f5ttsCfgStrength, f5ttsNfeStep,
});
const statusEl = document.getElementById('voice-status');
if (statusEl && xttsVoice) {
statusEl.textContent = `⏳ Stimme "${xttsVoice}" wird geladen...`;
statusEl.style.color = '#FFD60A';
}
}
// ── Passwort-Feld Anzeigen/Verbergen ─────────────────────
function toggleSecret(inputId, btn) {
const el = document.getElementById(inputId);
if (!el) return;
if (el.type === 'password') {
el.type = 'text';
btn.innerHTML = '&#128064;'; // 👀
btn.title = 'Verbergen';
} else {
el.type = 'password';
btn.innerHTML = '&#128065;'; // 👁
btn.title = 'Anzeigen';
}
}
// ── Runtime-Konfiguration ─────────────────────
async function loadRuntimeConfig() {
const statusEl = document.getElementById('rc-status');
statusEl.textContent = 'Lade...';
try {
const resp = await fetch('/api/runtime-config');
const cfg = await resp.json();
document.getElementById('rc-rvs-host').value = cfg.RVS_HOST || '';
document.getElementById('rc-rvs-port').value = cfg.RVS_PORT || '443';
document.getElementById('rc-rvs-tls').value = String(cfg.RVS_TLS) === 'false' ? 'false' : 'true';
document.getElementById('rc-rvs-token').value = cfg.RVS_TOKEN || '';
document.getElementById('rc-auth-token').value = cfg.ARIA_AUTH_TOKEN || '';
statusEl.textContent = 'Geladen.';
statusEl.style.color = '#34C759';
loadOnboardingQR(); // QR bei Config-Wechsel neu generieren
} catch (e) {
statusEl.textContent = 'Fehler: ' + e.message;
statusEl.style.color = '#FF6B6B';
}
}
async function saveRuntimeConfig() {
const statusEl = document.getElementById('rc-status');
statusEl.textContent = 'Speichere...';
const patch = {
RVS_HOST: document.getElementById('rc-rvs-host').value.trim(),
RVS_PORT: document.getElementById('rc-rvs-port').value.trim(),
RVS_TLS: document.getElementById('rc-rvs-tls').value,
RVS_TOKEN: document.getElementById('rc-rvs-token').value.trim(),
ARIA_AUTH_TOKEN: document.getElementById('rc-auth-token').value.trim(),
};
try {
const resp = await fetch('/api/runtime-config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(patch),
});
const data = await resp.json();
if (data.ok) {
statusEl.textContent = 'Gespeichert — Bridge-Container fuer Uebernahme neu starten.';
statusEl.style.color = '#FFD60A';
loadOnboardingQR(); // QR mit neuem Token
} else {
throw new Error(data.error || 'Unbekannt');
}
} catch (e) {
statusEl.textContent = 'Fehler: ' + e.message;
statusEl.style.color = '#FF6B6B';
}
}
// ── App-Onboarding QR-Code ────────────────────
let qrLibReady = false;
function ensureQRLib() {
return new Promise((resolve) => {
if (qrLibReady || window.qrcode) { qrLibReady = true; resolve(); return; }
const s = document.createElement('script');
s.src = 'https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js';
s.onload = () => { qrLibReady = true; resolve(); };
s.onerror = () => resolve(); // silent fail
document.head.appendChild(s);
});
}
async function loadOnboardingQR() {
const box = document.getElementById('onboarding-qr');
box.textContent = 'Lade...';
try {
await ensureQRLib();
if (!window.qrcode) throw new Error('QR-Library nicht geladen');
const resp = await fetch('/api/onboarding');
const cfg = await resp.json();
if (!cfg.rvsHost || !cfg.rvsToken) {
box.innerHTML = '<div style="color:#FF6B6B;">RVS nicht konfiguriert (ENV Variablen fehlen)</div>';
return;
}
// Format kompatibel mit android/src/components/QRScanner.tsx parseQRData()
const payload = JSON.stringify({
host: cfg.rvsHost,
port: Number(cfg.rvsPort) || 443,
tls: cfg.rvsTLS !== false,
token: cfg.rvsToken,
});
const qr = window.qrcode(0, 'M');
qr.addData(payload);
qr.make();
// Als SVG rendern — skaliert sauber auf Container-Groesse
box.innerHTML = qr.createSvgTag({ cellSize: 4, margin: 2, scalable: true });
const svg = box.querySelector('svg');
if (svg) {
svg.style.cssText = 'width:100%;height:100%;background:#fff;border-radius:4px;padding:6px;box-sizing:border-box;display:block;';
}
} catch (e) {
box.innerHTML = `<div style="color:#FF6B6B;">Fehler: ${e.message}</div>`;
}
}
// ── Modus-Wechsel ────────────────────────────
// Kanonische IDs (matchen bridge/modes.py canonical_id + android ModeSelector)
const MODE_LABELS = { normal: 'Normal', nicht_stoeren: 'Nicht stoeren', fluester: 'Fluestern', hangar: 'Hangar', gaming: 'Gaming' };
let currentMode = 'normal';
function updateModeUI(mode) {
currentMode = mode;
document.querySelectorAll('.mode-btn').forEach(btn => {
btn.style.borderColor = btn.dataset.mode === mode ? '#0096FF' : 'transparent';
});
const label = MODE_LABELS[mode] || mode;
document.getElementById('mode-status').textContent = `Aktueller Modus: ${label}`;
}
function setMode(mode) {
// Optimistic UI-Update — Bridge bestaetigt per Broadcast
updateModeUI(mode);
// Sauberer Weg: type=mode via RVS an Bridge — die broadcastet an alle Clients
send({ action: 'set_mode', mode });
}
function openLightbox(mediaType, url) {
const lb = document.getElementById('lightbox');
if (mediaType === 'video') {
lb.innerHTML = `<video src="${url}" controls autoplay style="max-width:95vw;max-height:95vh;border-radius:8px;" onclick="event.stopPropagation()"></video>`;
} else {
lb.innerHTML = `<img src="${url}" style="max-width:95vw;max-height:95vh;border-radius:8px;">`;
}
lb.classList.add('open');
}
function closeLightbox() {
const lb = document.getElementById('lightbox');
lb.classList.remove('open');
lb.innerHTML = '';
}
function showDockerLogs(msg) {
const tab = msg.tab;
const box = logBoxes[tab];
if (!box) return;
if (msg.error) {
appendToLog(tab, 'error', `Docker Logs Fehler: ${msg.error}`);
return;
}
// Bestehende Eintraege leeren und Docker-Logs einfuegen
box.innerHTML = '';
const header = document.createElement('div');
header.className = 'log-entry info';
header.textContent = `── Docker Logs: ${msg.container} (letzte ${msg.lines.length} Zeilen) ──`;
box.appendChild(header);
for (const line of msg.lines) {
const el = document.createElement('div');
el.className = 'log-entry ' + (line.match(/\b(error|Error|ERROR)\b/) ? 'error' :
line.match(/\b(warn|Warning|WARN)\b/) ? 'warn' : 'info');
el.textContent = line;
box.appendChild(el);
}
// Zähler aktualisieren
logCounts[tab] = msg.lines.length;
const countEl = document.getElementById(`count-${tab}`);
if (countEl) countEl.textContent = logCounts[tab];
box.scrollTop = box.scrollHeight;
autoScroll[tab] = true;
}
function showProxyModels(models) {
const container = document.getElementById('proxy-models');
const list = document.getElementById('proxy-models-list');
const hint = document.getElementById('proxy-models-hint');
container.style.display = 'block';
list.innerHTML = models.map(m => {
const clean = m.replace('openai/', '');
return `<div style="display:inline-block;background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:2px 8px;margin:2px;font-size:11px">${escapeHtml(m)}</div>`;
}).join('');
const cleanNames = models.map(m => m.replace('openai/', ''));
hint.textContent = `DEFAULT_MODEL fuer docker-compose.yml: ${cleanNames.join(' | ')}`;
}
function escapeHtml(str) {
return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
// Auto-Resize fuer Textarea — wuchst mit dem Inhalt bis zum max-height
function autoResizeTextarea(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 200) + 'px';
}
// Enter sendet, Shift+Enter macht neue Zeile (chat-Standard).
function chatInputKeydown(e, sendFn) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendFn();
// Textarea zurueck auf 2 rows setzen
e.target.style.height = 'auto';
}
}
document.getElementById('chat-input').addEventListener('keydown', (e) => chatInputKeydown(e, testRVS));
document.getElementById('chat-input-fs').addEventListener('keydown', (e) => chatInputKeydown(e, testRVSFS));
// Escape schliesst Lightbox
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeLightbox();
});
// ── ARIA Live-Ansicht (SSH + Desktop) ──────────────────
let liveSshTerm = null;
let liveSshFit = null;
function switchLiveTab(tab) {
document.getElementById('live-ssh').style.display = tab === 'ssh' ? 'block' : 'none';
document.getElementById('live-desktop').style.display = tab === 'desktop' ? 'block' : 'none';
document.getElementById('live-tab-ssh').className = 'tab-btn' + (tab === 'ssh' ? ' active' : '');
document.getElementById('live-tab-desktop').className = 'tab-btn' + (tab === 'desktop' ? ' active' : '');
if (tab === 'ssh' && liveSshTerm && liveSshFit) {
setTimeout(() => liveSshFit.fit(), 50);
}
}
function startLiveSSH() {
const statusEl = document.getElementById('live-ssh-status');
const btn = document.getElementById('btn-live-ssh');
// Wenn schon verbunden, trennen
if (liveSshTerm && liveSshTerm._sshConnected) {
send({ action: 'live_ssh_close' });
statusEl.textContent = 'Getrennt';
statusEl.style.color = '#FF6B6B';
btn.textContent = 'Verbinden';
liveSshTerm._sshConnected = false;
return;
}
statusEl.textContent = 'Verbinde...';
statusEl.style.color = '#FFD60A';
function initSSHTerm() {
const container = document.getElementById('live-ssh-term');
if (!liveSshTerm) {
liveSshTerm = new Terminal({
theme: { background: '#080810', foreground: '#E0E0F0', cursor: '#0096FF' },
fontFamily: 'Courier New, monospace',
fontSize: 12,
cursorBlink: true,
});
liveSshFit = new FitAddon.FitAddon();
liveSshTerm.loadAddon(liveSshFit);
liveSshTerm.open(container);
liveSshFit.fit();
liveSshTerm.onData((data) => {
send({ action: 'live_ssh_input', data });
});
}
liveSshTerm.clear();
send({ action: 'live_ssh_start' });
}
if (typeof Terminal === 'undefined') {
const s = document.createElement('script');
s.src = 'https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js';
s.onload = () => {
const s2 = document.createElement('script');
s2.src = 'https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js';
s2.onload = () => initSSHTerm();
document.head.appendChild(s2);
};
document.head.appendChild(s);
} else {
initSSHTerm();
}
}
function handleLiveSSH(msg) {
const statusEl = document.getElementById('live-ssh-status');
const btn = document.getElementById('btn-live-ssh');
if (msg.type === 'live_ssh_data' && liveSshTerm) {
const raw = atob(msg.data);
const bytes = new Uint8Array(raw.length);
for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
liveSshTerm.write(bytes);
} else if (msg.type === 'live_ssh_connected') {
statusEl.textContent = 'Verbunden mit aria-wohnung';
statusEl.style.color = '#34C759';
btn.textContent = 'Trennen';
if (liveSshTerm) liveSshTerm._sshConnected = true;
} else if (msg.type === 'live_ssh_error') {
statusEl.textContent = msg.error || 'Fehler';
statusEl.style.color = '#FF6B6B';
btn.textContent = 'Verbinden';
if (liveSshTerm) liveSshTerm._sshConnected = false;
} else if (msg.type === 'live_ssh_closed') {
statusEl.textContent = 'Getrennt';
statusEl.style.color = '#8888AA';
btn.textContent = 'Verbinden';
if (liveSshTerm) liveSshTerm._sshConnected = false;
}
}
function checkDesktop() {
send({ action: 'check_desktop' });
}
function handleDesktop(msg) {
if (msg.type === 'desktop_status') {
const placeholder = document.getElementById('desktop-placeholder');
const vnc = document.getElementById('desktop-vnc');
if (msg.available && msg.url) {
placeholder.style.display = 'none';
vnc.style.display = 'block';
vnc.src = msg.url;
} else {
placeholder.style.display = 'flex';
vnc.style.display = 'none';
placeholder.querySelector('div:nth-child(2)').textContent = msg.message || 'Kein Desktop verfuegbar';
}
}
}
// Sessions- und alter Brain-Viewer wurden entfernt — Memories laufen
// jetzt komplett ueber den Gehirn-Tab + Vector-DB im aria-brain.
// ── Haupt-Tab Navigation ──────────────────────────────────
function switchMainTab(tab) {
document.querySelectorAll('.main-tab').forEach(el => el.classList.remove('visible'));
document.querySelectorAll('.main-nav-btn').forEach(b => b.classList.remove('active'));
const target = document.getElementById('tab-' + tab);
if (target) target.classList.add('visible');
// Button aktivieren — Match per onclick-Attribut (robust gegen Beschriftungs-Aenderungen)
document.querySelectorAll('.main-nav-btn').forEach(b => {
const oc = b.getAttribute('onclick') || '';
if (oc.includes(`'${tab}'`)) b.classList.add('active');
});
// Einstellungen: Config + QR laden
if (tab === 'settings') {
send({ action: 'get_voice_config' });
loadRuntimeConfig();
loadOnboardingQR();
} else if (tab === 'brain') {
loadBrainStatus();
loadBrainMemoryList();
refreshImportFiles();
} else if (tab === 'files') {
loadFiles();
} else if (tab === 'skills') {
loadSkills();
}
}
// ── Skills-Verwaltung ────────────────────────────
let skillsCache = [];
const skillExpanded = new Set();
async function loadSkills() {
const el = document.getElementById('skills-list');
if (!el) return;
try {
const r = await fetch('/api/brain/skills/list');
if (!r.ok) throw new Error('HTTP ' + r.status);
const d = await r.json();
skillsCache = d.skills || [];
renderSkillsList();
} catch (e) {
el.innerHTML = `🔴 Brain nicht erreichbar (${e.message})`;
}
}
function renderSkillsList() {
const el = document.getElementById('skills-list');
if (!el) return;
if (!skillsCache.length) {
el.innerHTML = '<div style="padding:8px;color:#555570;">Keine Skills vorhanden. ARIA legt welche an wenn sie ein wiederkehrendes Problem löst — oder importiere einen.</div>';
return;
}
const fmtDate = (iso) => iso ? new Date(iso).toLocaleString('de-DE') : '';
el.innerHTML = skillsCache.map(s => {
const active = s.active !== false;
const expanded = skillExpanded.has(s.name);
const statusBadge = active
? '<span style="background:#34C75922;color:#34C759;padding:1px 6px;border-radius:3px;font-size:10px;">aktiv</span>'
: '<span style="background:#55557022;color:#888;padding:1px 6px;border-radius:3px;font-size:10px;">DEAKTIVIERT</span>';
const execBadge = `<span style="background:#0096FF22;color:#0096FF;padding:1px 6px;border-radius:3px;font-size:10px;">${escapeHtml(s.execution || 'bash')}</span>`;
const authorBadge = s.author === 'aria'
? '<span style="background:#FFD60A22;color:#FFD60A;padding:1px 6px;border-radius:3px;font-size:10px;">von ARIA</span>'
: '';
const setupErr = s.setup_error
? `<div style="color:#FF6B6B;font-size:10px;margin-top:4px;">⚠ Setup-Fehler: ${escapeHtml(s.setup_error.slice(0,200))}</div>`
: '';
let expandedSection = '';
if (expanded) {
expandedSection = `
<div style="margin-top:8px;padding-top:8px;border-top:1px solid #1E1E2E;">
<div id="skill-readme-${escapeHtml(s.name)}" style="font-size:11px;color:#8888AA;margin-bottom:8px;">(README lädt...)</div>
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px;">
<button class="btn secondary" onclick="runSkillPrompt('${escapeHtml(s.name)}')" style="padding:2px 10px;font-size:11px;color:#3FFF3F;border-color:#3FFF3F;">▶ Ausführen</button>
<button class="btn secondary" onclick="toggleSkillActive('${escapeHtml(s.name)}', ${!active})" style="padding:2px 10px;font-size:11px;color:#FF9500;border-color:#FF9500;">${active ? '⏸ Deaktivieren' : '▶ Aktivieren'}</button>
<button class="btn secondary" onclick="exportSkill('${escapeHtml(s.name)}')" style="padding:2px 10px;font-size:11px;color:#0096FF;border-color:#0096FF;">⬇ Export</button>
<button class="btn secondary" onclick="deleteSkill('${escapeHtml(s.name)}')" style="padding:2px 10px;font-size:11px;color:#FF6B6B;border-color:#FF6B6B;">🗑 Löschen</button>
</div>
<div style="color:#0096FF;font-size:11px;font-weight:bold;margin:6px 0 4px;">Logs (letzte 20)</div>
<div id="skill-logs-${escapeHtml(s.name)}" style="font-size:11px;color:#8888AA;">(Logs lädt...)</div>
</div>
`;
}
return `
<div style="border-bottom:1px solid #1E1E2E;padding:8px 0;">
<div style="display:flex;align-items:center;gap:8px;cursor:pointer;" onclick="toggleSkillExpand('${escapeHtml(s.name)}')">
<span style="font-size:14px;color:#E0E0F0;">${expanded ? '▼' : '▶'}</span>
<span style="flex:1;color:#E0E0F0;font-weight:bold;">${escapeHtml(s.name)}</span>
${statusBadge} ${execBadge} ${authorBadge}
<span style="color:#555570;font-size:10px;">${s.use_count || 0}× · zuletzt ${fmtDate(s.last_used)}</span>
</div>
<div style="color:#8888AA;font-size:11px;margin-top:2px;margin-left:20px;">${escapeHtml(s.description || '(ohne Beschreibung)')}</div>
${setupErr}
${expandedSection}
</div>
`;
}).join('');
}
async function toggleSkillExpand(name) {
if (skillExpanded.has(name)) skillExpanded.delete(name);
else skillExpanded.add(name);
renderSkillsList();
if (skillExpanded.has(name)) {
// README + Logs lazy laden
try {
const r = await fetch('/api/brain/skills/' + encodeURIComponent(name));
const d = await r.json();
const el = document.getElementById('skill-readme-' + name);
if (el && d.readme) el.innerHTML = '<pre style="margin:0;font-family:inherit;white-space:pre-wrap;">' + escapeHtml(d.readme) + '</pre>';
} catch {}
try {
const r2 = await fetch('/api/brain/skills/' + encodeURIComponent(name) + '/logs');
const d2 = await r2.json();
const el2 = document.getElementById('skill-logs-' + name);
if (!el2) return;
if (!d2.logs.length) { el2.innerHTML = '(keine Runs)'; return; }
el2.innerHTML = d2.logs.map(l => {
const okBadge = l.exit_code === 0 ? '<span style="color:#3FFF3F;">OK</span>' : `<span style="color:#FF6B6B;">FEHLER (${l.exit_code})</span>`;
return `<div style="padding:4px 6px;border:1px solid #1E1E2E;border-radius:3px;margin-bottom:4px;">
<div style="font-size:10px;color:#555570;">${escapeHtml(l.ts)} · ${l.duration_sec}s · ${okBadge}</div>
${l.stdout ? `<pre style="margin:2px 0;font-size:10px;color:#E0E0F0;white-space:pre-wrap;max-height:120px;overflow:auto;">${escapeHtml(l.stdout)}</pre>` : ''}
${l.stderr ? `<pre style="margin:2px 0;font-size:10px;color:#FF9500;white-space:pre-wrap;max-height:80px;overflow:auto;">${escapeHtml(l.stderr)}</pre>` : ''}
</div>`;
}).join('');
} catch {}
}
}
async function toggleSkillActive(name, newActive) {
try {
await fetch('/api/brain/skills/' + encodeURIComponent(name), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ active: newActive }),
});
loadSkills();
} catch (e) {
alert('Toggle fehlgeschlagen: ' + e.message);
}
}
async function deleteSkill(name) {
if (!confirm(`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);
}
}
async function runSkillPrompt(name) {
const argsStr = prompt(`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);
return;
}
try {
const r = await fetch('/api/brain/skills/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, args }),
});
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);
if (skillExpanded.has(name)) toggleSkillExpand(name); // refresh logs by collapse+expand
loadSkills();
} catch (e) {
alert('Run fehlgeschlagen: ' + e.message);
}
}
function exportSkill(name) {
window.location.href = '/api/brain/skills/' + encodeURIComponent(name) + '/export';
}
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.`)) {
event.target.value = ''; return;
}
try {
const r = await fetch('/api/brain/skills/import?overwrite=true', {
method: 'POST',
headers: { 'Content-Type': 'application/gzip' },
body: file,
});
const d = await r.json();
if (r.ok) {
alert(`✓ Skill "${d.imported?.name}" importiert.`);
loadSkills();
} else {
alert('Import fehlgeschlagen: ' + (d.detail || JSON.stringify(d)));
}
} catch (e) {
alert('Import fehlgeschlagen: ' + e.message);
} finally {
event.target.value = '';
}
}
// ── Datei-Manager ──────────────────────────────────────
let filesCache = [];
async function loadFiles() {
const listEl = document.getElementById('files-list');
if (listEl) listEl.innerHTML = '(Lade...)';
try {
const r = await fetch('/api/files-list');
const d = await r.json();
if (!d.ok) throw new Error(d.error || 'Unbekannter Fehler');
filesCache = d.files || [];
renderFilesList();
} catch (e) {
if (listEl) listEl.innerHTML = `🔴 ${e.message}`;
}
}
function renderFilesList() {
const listEl = document.getElementById('files-list');
const infoEl = document.getElementById('files-info');
if (!listEl) return;
const q = (document.getElementById('files-search').value || '').toLowerCase();
const filter = document.getElementById('files-filter').value;
let files = filesCache.slice();
if (filter === 'aria') files = files.filter(f => f.fromAria);
else if (filter === 'user') files = files.filter(f => !f.fromAria);
if (q) files = files.filter(f => f.name.toLowerCase().includes(q));
if (infoEl) infoEl.textContent = `${files.length} von ${filesCache.length} Dateien`;
if (!files.length) {
listEl.innerHTML = '(Keine Dateien gefunden)';
return;
}
const fmtSize = (b) => b < 1024 ? `${b} B` : b < 1024*1024 ? `${(b/1024).toFixed(1)} KB` : `${(b/1024/1024).toFixed(1)} MB`;
const fmtDate = (ms) => new Date(ms).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' });
listEl.innerHTML = files.map(f => {
const badge = f.fromAria
? '<span style="background:#0096FF22;color:#0096FF;padding:1px 6px;border-radius:3px;font-size:10px;margin-right:6px;">ARIA</span>'
: '<span style="background:#34C75922;color:#34C759;padding:1px 6px;border-radius:3px;font-size:10px;margin-right:6px;">User</span>';
return `<div style="padding:8px 0;border-bottom:1px solid #1E1E2E;display:flex;gap:6px;align-items:center;">
<div style="flex:1;min-width:0;">
<div style="color:#E0E0F0;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${badge}<strong>${escapeHtml(f.name)}</strong></div>
<div style="color:#555570;font-size:10px;">${fmtSize(f.size)} · ${fmtDate(f.mtime)}</div>
</div>
<button class="btn secondary" onclick="downloadFile('${encodeURIComponent(f.path)}')" style="padding:2px 8px;font-size:10px;" title="Herunterladen">⬇</button>
<button class="btn secondary" onclick="deleteFile('${escapeHtml(f.path)}','${escapeHtml(f.name)}')" style="padding:2px 8px;font-size:10px;color:#FF6B6B;border-color:#FF6B6B;" title="Loeschen">🗑</button>
</div>`;
}).join('');
}
function downloadFile(encPath) {
window.location.href = '/api/files-download?path=' + encPath;
}
async function deleteFile(p, name) {
if (!confirm(`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',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: p }),
});
const d = await r.json();
if (d.ok) {
loadFiles();
// Server broadcastet file_deleted Event → markFileDeletedInChat() updated bubbles
} else {
alert('Löschen fehlgeschlagen: ' + d.error);
}
} catch (e) {
alert('Löschen fehlgeschlagen: ' + e.message);
}
}
// ── Gehirn-Tab ────────────────────────────────────────────
async function loadBrainStatus() {
// Es gibt ZWEI Brain-Status-Anzeigen: kompakte Card im Main-Tab
// (brain-dot + brain-status-short + brain-error) und die ausfuehrliche
// im Gehirn-Tab (brain-status). Beide muessen synchron updated werden.
const mainShort = document.getElementById('brain-status-short');
const mainDot = document.getElementById('brain-dot');
const mainErr = document.getElementById('brain-error');
const detail = document.getElementById('brain-status');
try {
const r = await fetch('/api/brain/health');
if (!r.ok) throw new Error('HTTP ' + r.status);
const d = await r.json();
const ok = d.status === 'ok';
const st = ok ? '🟢 online' : '🟡 ' + (d.status || 'unknown');
const detailText = `${st} · ${d.memory_count ?? '?'} Memories · Qdrant: ${d.qdrant || '-'}`;
if (detail) detail.innerHTML = detailText;
if (mainShort) mainShort.textContent = ok ? 'online' : (d.status || 'unbekannt');
if (mainDot) mainDot.className = `dot ${ok ? 'connected' : 'disconnected'}`;
if (mainErr) mainErr.textContent = ok ? '' : (d.error || '');
} catch (e) {
if (detail) detail.innerHTML = `🔴 Brain nicht erreichbar (${e.message})`;
if (mainShort) mainShort.textContent = 'nicht erreichbar';
if (mainDot) mainDot.className = 'dot disconnected';
if (mainErr) mainErr.textContent = e.message;
}
// Conversation-Stats (separater Endpoint)
const conv = document.getElementById('conversation-status');
if (!conv) return;
const infoBtn = `<button class="info-btn-small" onclick="showInfo('conversation')" title="Konversation — wie funktioniert das?"></button>`;
try {
const r2 = await fetch('/api/brain/conversation/stats');
if (!r2.ok) throw new Error('HTTP ' + r2.status);
const d2 = await r2.json();
const distillIcon = d2.needs_distill ? ' ⚠ Destillat bald fällig' : '';
conv.innerHTML = `Konversation: <strong>${d2.turns}</strong> Turns · Window: ${d2.max_window} · Schwelle: ${d2.distill_threshold}${distillIcon} ${infoBtn}`;
} catch (e) {
conv.innerHTML = `Konversation: <span style="color:#555570;">${e.message}</span> ${infoBtn}`;
}
}
async function distillNow() {
if (!confirm('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 : ''}`);
loadBrainStatus();
loadBrainMemoryList();
} catch (e) {
alert('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;
try {
// Beides parallel — Brain Window + Diagnostic chat_backup
const [brainR, histR] = await Promise.all([
fetch('/api/brain/conversation/reset', { method: 'POST' }),
fetch('/api/chat-history-clear', { method: 'POST' }),
]);
const brainOk = brainR.ok;
const histOk = histR.ok;
if (brainOk && histOk) {
// Chat-View leeren (Server broadcasted das eh, aber sicherheitshalber)
if (chatBox) chatBox.innerHTML = '';
const fsBox = document.getElementById('chat-box-fs');
if (fsBox) fsBox.innerHTML = '';
loadBrainStatus();
} else {
alert(`Reset teilweise fehlgeschlagen — Brain: ${brainOk ? 'OK' : 'fail'}, History: ${histOk ? 'OK' : 'fail'}`);
}
} catch (e) {
alert('Reset fehlgeschlagen: ' + e.message);
}
}
// Cache aller geladenen Memories — fuer Edit-Lookup
let brainMemoryCache = {};
// Aktuelle Search-Treffer (IDs in Reihenfolge); leer = normale Liste
let brainSearchIds = null;
function resetBrainFilters() {
const s = document.getElementById('brain-search'); if (s) s.value = '';
const t = document.getElementById('brain-filter-type'); if (t) t.value = '';
const p = document.getElementById('brain-filter-pinned'); if (p) p.value = 'all';
const info = document.getElementById('brain-search-info'); if (info) info.style.display = 'none';
brainSearchIds = null;
}
async function runBrainSearch() {
const q = (document.getElementById('brain-search').value || '').trim();
const info = document.getElementById('brain-search-info');
if (!q) {
brainSearchIds = null;
if (info) info.style.display = 'none';
loadBrainMemoryList();
return;
}
const typeFilter = document.getElementById('brain-filter-type').value;
const params = new URLSearchParams({ q, k: '20', include_pinned: 'true' });
if (typeFilter) params.set('type', typeFilter);
try {
const r = await fetch('/api/brain/memory/search?' + params.toString());
if (!r.ok) throw new Error('HTTP ' + r.status);
const hits = await r.json();
hits.forEach(m => { brainMemoryCache[m.id] = m; });
brainSearchIds = hits.map(m => m.id);
if (info) {
info.style.display = 'block';
info.innerHTML = `🔍 ${hits.length} Treffer für "${escapeHtml(q)}"` +
(typeFilter ? ` · Typ=${escapeHtml(typeFilter)}` : '') +
` · sortiert nach Aehnlichkeit`;
}
renderBrainList(hits, true);
} catch (e) {
if (info) {
info.style.display = 'block';
info.innerHTML = `🔴 Suche fehlgeschlagen: ${escapeHtml(e.message)}`;
}
}
}
async function loadBrainMemoryList() {
const el = document.getElementById('brain-memory-list');
if (!el) return;
// Wenn aktive Search-Treffer da sind: die anzeigen, nicht neu laden
if (brainSearchIds && brainSearchIds.length) {
const items = brainSearchIds.map(id => brainMemoryCache[id]).filter(Boolean);
renderBrainList(items, true);
return;
}
try {
const typeFilter = document.getElementById('brain-filter-type').value;
const pinnedFilter = document.getElementById('brain-filter-pinned').value;
const params = new URLSearchParams({ limit: '500' });
if (typeFilter) params.set('type', typeFilter);
const r = await fetch('/api/brain/memory/list?' + params.toString());
if (!r.ok) throw new Error('HTTP ' + r.status);
let items = await r.json();
brainMemoryCache = {};
items.forEach(m => { brainMemoryCache[m.id] = m; });
if (pinnedFilter === 'pinned') items = items.filter(m => m.pinned);
else if (pinnedFilter === 'cold') items = items.filter(m => !m.pinned);
if (!items.length) {
el.innerHTML = '(Keine Memories — leere DB oder Filter zu eng)';
return;
}
renderBrainList(items, false);
} catch (e) {
el.innerHTML = `🔴 Brain nicht erreichbar (${e.message})`;
}
}
const BRAIN_TYPE_LABELS = {
identity: 'Identität', rule: 'Regeln / Werte', preference: 'Praeferenzen',
tool: 'Tools', skill: 'Skills', fact: 'Fakten',
conversation: 'Konversation', reminder: 'Reminder'
};
const BRAIN_TYPE_ORDER = ['identity','rule','preference','tool','skill','fact','conversation','reminder'];
function renderMemoryRow(m, withScore) {
const pin = m.pinned ? '📌 ' : '';
const preview = (m.content || '').slice(0, 140).replace(/\n/g, ' ');
const score = withScore && typeof m.score === 'number' ? `<span style="color:#FFD60A;font-size:10px;margin-left:6px;">${m.score.toFixed(2)}</span>` : '';
const typeBadge = withScore ? `<span style="color:#0096FF;font-size:10px;margin-right:6px;">${escapeHtml(BRAIN_TYPE_LABELS[m.type] || m.type)}</span>` : '';
return `<div style="padding:6px 0;border-bottom:1px solid #1E1E2E;display:flex;gap:6px;align-items:flex-start;">
<div style="flex:1;min-width:0;cursor:pointer;" onclick="openMemoryModal('${m.id}')">
<div style="color:#E0E0F0;font-size:12px;">${typeBadge}${pin}<strong>${escapeHtml(m.title || '(ohne Titel)')}</strong>${score}
${m.category ? `<span style="color:#555570;font-weight:normal;font-size:10px;margin-left:6px;">[${escapeHtml(m.category)}]</span>` : ''}
</div>
<div style="color:#888;font-size:11px;line-height:1.4;">${escapeHtml(preview)}${m.content && m.content.length > 140 ? '...' : ''}</div>
</div>
<button class="btn secondary" onclick="event.stopPropagation();openMemoryModal('${m.id}')" style="padding:2px 8px;font-size:10px;flex-shrink:0;" title="Bearbeiten">✎</button>
<button class="btn secondary" onclick="event.stopPropagation();deleteMemory('${m.id}')" style="padding:2px 8px;font-size:10px;flex-shrink:0;color:#FF6B6B;border-color:#FF6B6B;" title="Loeschen">🗑</button>
</div>`;
}
function renderBrainList(items, isSearchResult) {
const el = document.getElementById('brain-memory-list');
if (!el) return;
if (isSearchResult) {
// Such-Treffer: in Aehnlichkeits-Reihenfolge, kein Type-Gruppieren
const html = items.map(m => renderMemoryRow(m, true)).join('');
el.innerHTML = html || '(Keine Treffer)';
return;
}
// Normale Liste: nach Type gruppieren
const byType = {};
items.forEach(m => { (byType[m.type] = byType[m.type] || []).push(m); });
const html = BRAIN_TYPE_ORDER.flatMap(t => {
if (!byType[t]) return [];
const heading = `<div style="margin-top:14px;color:#0096FF;font-weight:bold;font-size:11px;text-transform:uppercase;letter-spacing:0.5px;">${BRAIN_TYPE_LABELS[t] || t} (${byType[t].length})</div>`;
const rows = byType[t].map(m => renderMemoryRow(m, false)).join('');
return [heading, rows];
}).join('');
// Falls Items mit unbekannten Types existieren, hinten dranhaengen
const extraTypes = Object.keys(byType).filter(t => !BRAIN_TYPE_ORDER.includes(t));
let extra = '';
for (const t of extraTypes) {
extra += `<div style="margin-top:14px;color:#0096FF;font-weight:bold;font-size:11px;text-transform:uppercase;">${escapeHtml(t)} (${byType[t].length})</div>`;
extra += byType[t].map(m => renderMemoryRow(m, false)).join('');
}
el.innerHTML = (html + extra) || '(Keine bekannten Typen gefunden)';
}
// ── Memory CRUD ───────────────────────────────────
function openMemoryModal(id) {
const modal = document.getElementById('memory-modal');
const titleEl = document.getElementById('memory-modal-title');
const idEl = document.getElementById('memory-edit-id');
const errEl = document.getElementById('memory-modal-error');
errEl.style.display = 'none';
if (id && brainMemoryCache[id]) {
const m = brainMemoryCache[id];
titleEl.textContent = 'Memory bearbeiten';
idEl.value = id;
document.getElementById('memory-type').value = m.type || 'fact';
document.getElementById('memory-title').value = m.title || '';
document.getElementById('memory-content').value = m.content || '';
document.getElementById('memory-category').value = m.category || '';
document.getElementById('memory-tags').value = (m.tags || []).join(', ');
document.getElementById('memory-pinned').checked = !!m.pinned;
} else {
titleEl.textContent = 'Neue Memory';
idEl.value = '';
document.getElementById('memory-type').value = 'fact';
document.getElementById('memory-title').value = '';
document.getElementById('memory-content').value = '';
document.getElementById('memory-category').value = '';
document.getElementById('memory-tags').value = '';
document.getElementById('memory-pinned').checked = false;
}
modal.classList.add('open');
}
function closeMemoryModal() {
document.getElementById('memory-modal').classList.remove('open');
}
async function saveMemory() {
const errEl = document.getElementById('memory-modal-error');
errEl.style.display = 'none';
const id = document.getElementById('memory-edit-id').value;
const type = document.getElementById('memory-type').value;
const title = document.getElementById('memory-title').value.trim();
const content = document.getElementById('memory-content').value.trim();
const category = document.getElementById('memory-category').value.trim();
const tags = document.getElementById('memory-tags').value.split(',').map(t => t.trim()).filter(Boolean);
const pinned = document.getElementById('memory-pinned').checked;
if (!title) { errEl.textContent = 'Titel fehlt.'; errEl.style.display = 'block'; return; }
if (!content) { errEl.textContent = 'Inhalt fehlt.'; errEl.style.display = 'block'; return; }
try {
let r;
if (id) {
r = await fetch('/api/brain/memory/update/' + encodeURIComponent(id), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content, pinned, category, tags }),
});
} else {
r = await fetch('/api/brain/memory/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type, title, content, pinned, category, tags, source: 'manual' }),
});
}
if (!r.ok) {
const txt = await r.text();
throw new Error('HTTP ' + r.status + ': ' + txt.slice(0, 200));
}
closeMemoryModal();
loadBrainMemoryList();
loadBrainStatus();
} catch (e) {
errEl.textContent = e.message;
errEl.style.display = 'block';
}
}
async function deleteMemory(id) {
const m = brainMemoryCache[id];
const label = m ? `"${m.title}" (${m.type})` : id;
if (!confirm(`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);
loadBrainMemoryList();
loadBrainStatus();
} catch (e) {
alert('Löschen fehlgeschlagen: ' + e.message);
}
}
// snapshotSessions entfernt — aria-core ist raus.
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
// ── Generisches Info-Modal — Aufruf: openInfoModal('Titel', '<p>HTML...</p>') ──
function openInfoModal(title, html) {
const t = document.getElementById('info-modal-title');
const b = document.getElementById('info-modal-body');
const m = document.getElementById('info-modal');
if (!t || !b || !m) return;
t.textContent = title;
b.innerHTML = html;
m.classList.add('open');
}
function closeInfoModal() {
const m = document.getElementById('info-modal');
if (m) m.classList.remove('open');
}
// Vor-definierte Info-Blocks
const INFO_TEXTS = {
'brain-status': {
title: 'Gehirn — Status',
html: `
<p><strong>online / offline</strong> — ob der <code>aria-brain</code> Container erreichbar ist (HTTP GET /health).</p>
<p><strong>N Memories</strong> — Anzahl der Punkte in der Vector-DB. Beinhaltet alle Typen: identity, rule, preference, tool, skill, fact, conversation, reminder.</p>
<p><strong>Qdrant: aria-qdrant:6333</strong> — Hostname + Port des Vector-DB-Containers. Der Brain spricht intern dorthin.</p>
`,
},
'conversation': {
title: 'Konversation — wie funktioniert das?',
html: `
<p><strong>Rolling Window:</strong> ARIA "sieht" pro Anfrage nur die letzten N Turns einer einzelnen, durchgehenden Konversation. Kein Sessions, kein Multi-Thread.</p>
<ul>
<li><strong>Turns</strong> — Anzahl aller Nachrichten (User + ARIA) seit dem letzten Destillat oder Reset.</li>
<li><strong>Window: 50</strong> — die letzten 50 Turns wandern in den Prompt. Aelteste fallen raus, sobald die Schwelle ueberschritten ist.</li>
<li><strong>Schwelle: 60</strong> — bei mehr als 60 Turns triggert Brain automatisch das Destillat (die 30 aeltesten werden zu fact-Memories verdichtet, Token-Budget bleibt konstant).</li>
</ul>
<p><strong>⚗ Jetzt destillieren:</strong> manueller Trigger fuer das Destillat (kostet einen Claude-Call). Verdichtet die aeltesten 30 Turns zu Fakten + entfernt sie aus dem Window.</p>
<p><strong>🧹 Konversation komplett zuruecksetzen:</strong> leert beides — ARIAs Rolling-Window (Brain) UND die Chat-Anzeige hier (chat_backup.jsonl). Destillierte Facts + alle anderen Memories in der Vector-DB <em>bleiben</em>.</p>
<p style="margin-top:8px;color:#FFD60A;font-size:12px;">⚠ Falls "Turns: 0" obwohl du oben Chat-Eintraege siehst: chat_backup.jsonl (Anzeige) und conversation.jsonl (Brain-Kontext) sind getrennte Stores. Alte chat_backup-Eintraege koennen aus OpenClaw-Zeit stammen. Reset-Button leert beides.</p>
`,
},
'memories': {
title: 'Memories — Hot vs. Cold',
html: `
<p><strong>Pinned (Hot Memory)</strong> 📌 — landet bei JEDER Anfrage im System-Prompt. Hier gehoeren rein: Identitaet, Sicherheitsregeln, Benutzer-Praeferenzen, Tool-Freigaben, Kern-Skills.</p>
<p><strong>Cold Memory</strong> — semantisch durchsucht. Pro Anfrage werden die 5 aehnlichsten Punkte zur User-Frage in den Prompt eingehaengt.</p>
<p><strong>Typen:</strong></p>
<ul>
<li><strong>identity</strong> — wer ARIA ist (Name, Persoenlichkeit)</li>
<li><strong>rule</strong> — Sicherheits-/Werte-Regeln</li>
<li><strong>preference</strong> — User-Profile</li>
<li><strong>tool</strong> — Tool-Freigaben + Infrastruktur</li>
<li><strong>skill</strong> — Faehigkeiten (verlinkt mit /data/skills/)</li>
<li><strong>fact</strong> — Wissens-Fakten (oft aus Destillaten)</li>
<li><strong>conversation</strong> — destillierte Konversations-Erkenntnisse</li>
<li><strong>reminder</strong> — Termine, Aufgaben</li>
</ul>
<p><strong>Such-Feld:</strong> semantische Suche via Embedder + Qdrant. Findet sinngemaess, nicht nur Stichworte.</p>
`,
},
'bootstrap': {
title: 'Bootstrap & Migration — die drei Wege',
html: `
<p><strong>1. Aus brain-import/ migrieren</strong> 🔵 — Parser fuer die <code>.md</code>-Dateien im Repo (AGENT.md, USER.md, TOOLING.md). Schreibt sie als atomare pinned Memories. Idempotent — Re-Run ersetzt nur die Migration-Punkte, eigene Memories bleiben.</p>
<p><strong>2. Bootstrap-Snapshot</strong> 🟡 — kleines JSON, NUR die pinned Memories. Export = aktueller Stand als Datei. Import = ALLE aktuell pinned werden ersetzt. Cold Memory bleibt unangetastet.</p>
<p><strong>3. Komplettes Gehirn</strong> 🔴 — tar.gz mit allem (Memories + Skills + Qdrant-DB). Backup + Restore. Import ueberschreibt ALLES.</p>
`,
},
};
function showInfo(key) {
const cfg = INFO_TEXTS[key];
if (cfg) openInfoModal(cfg.title, cfg.html);
}
function brainExport() {
// Browser folgt der Download-Header-Antwort automatisch
window.location.href = '/api/brain-export';
}
// ── 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 (status) status.innerHTML = '⏳ Migriere...';
try {
const r = await fetch('/api/brain/memory/migrate', { method: 'POST' });
if (!r.ok) throw new Error('HTTP ' + r.status);
const d = await r.json();
if (d.error) throw new Error(d.error);
const files = (d.files || []).join(', ');
if (status) status.innerHTML = `<span style="color:#3FFF3F;">✓ ${d.created} Memories erstellt aus: ${files}</span>`;
loadBrainMemoryList();
loadBrainStatus();
} catch (e) {
if (status) status.innerHTML = `<span style="color:#FF6B6B;">✗ ${e.message}</span>`;
}
}
async function refreshImportFiles() {
const el = document.getElementById('brain-import-files-status');
if (!el) return;
try {
const r = await fetch('/api/brain/memory/import-files');
if (!r.ok) throw new Error('HTTP ' + r.status);
const d = await r.json();
if (!d.exists) {
el.innerHTML = `<span style="color:#FF9500;">brain-import/ nicht gemountet (${d.import_dir})</span>`;
return;
}
if (!d.files.length) {
el.innerHTML = '<span style="color:#FF9500;">Keine .md-Dateien in brain-import/</span>';
return;
}
const fmt = d.files.map(f => `${f.name} (${(f.size/1024).toFixed(1)}KB)`).join(', ');
el.innerHTML = `<span style="color:#555570;">Verfügbar: ${escapeHtml(fmt)}</span>`;
} catch (e) {
el.innerHTML = `<span style="color:#555570;">Brain nicht erreichbar</span>`;
}
}
// ── Bootstrap Export / Import ──────────────────────────
async function exportBootstrap() {
const status = document.getElementById('bootstrap-status');
if (status) status.innerHTML = '⏳ Lade...';
try {
const r = await fetch('/api/brain/memory/export-bootstrap');
if (!r.ok) throw new Error('HTTP ' + r.status);
const data = await r.json();
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
const a = document.createElement('a');
a.href = url;
a.download = `aria-bootstrap-${ts}.json`;
document.body.appendChild(a); a.click();
setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 100);
if (status) status.innerHTML = `<span style="color:#3FFF3F;">✓ ${data.count} pinned Memories exportiert</span>`;
} catch (e) {
if (status) status.innerHTML = `<span style="color:#FF6B6B;">✗ ${e.message}</span>`;
}
}
async function importBootstrap(event) {
const file = event.target.files[0];
if (!file) return;
const status = document.getElementById('bootstrap-status');
try {
const text = await file.text();
const bundle = JSON.parse(text);
if (!Array.isArray(bundle.memories)) throw new Error('Datei hat kein "memories"-Array');
if (!confirm(`Bootstrap importieren?\n\n${bundle.memories.length} pinned Memories aus "${file.name}".\n\nALLE aktuell pinned Memories werden überschrieben. Cold Memory bleibt unverändert.`)) {
event.target.value = '';
return;
}
if (status) status.innerHTML = '⏳ Importiere...';
const r = await fetch('/api/brain/memory/import-bootstrap', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: text,
});
if (!r.ok) {
const errText = await r.text();
throw new Error('HTTP ' + r.status + ': ' + errText.slice(0, 200));
}
const d = await r.json();
if (status) status.innerHTML = `<span style="color:#3FFF3F;">✓ ${d.created} Memories importiert</span>`;
loadBrainMemoryList();
loadBrainStatus();
} catch (e) {
if (status) status.innerHTML = `<span style="color:#FF6B6B;">✗ ${e.message}</span>`;
} finally {
event.target.value = '';
}
}
async function brainImport(event) {
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)`)) {
event.target.value = '';
return;
}
statusEl.innerHTML = `⏳ Lade hoch (${(file.size/1024/1024).toFixed(1)} MB) — Container werden gestoppt...`;
try {
const r = await fetch('/api/brain-import', {
method: 'POST',
headers: { 'Content-Type': 'application/gzip' },
body: file,
});
const d = await r.json();
if (d.ok) {
statusEl.innerHTML = `<span style="color:#3FFF3F;">✓ ${d.message}</span>`;
setTimeout(() => { loadBrainStatus(); loadBrainMemoryList(); }, 3000);
} else {
statusEl.innerHTML = `<span style="color:#FF6B6B;">✗ Fehler: ${d.error}</span>`;
}
} catch (e) {
statusEl.innerHTML = `<span style="color:#FF6B6B;">✗ Upload fehlgeschlagen: ${e.message}</span>`;
} finally {
event.target.value = '';
}
}
// ── Einstellungen: Tool-Berechtigungen ──────────────────
// Granulare Permissions entfernt — Claude Code laeuft mit
// --dangerously-skip-permissions (Alles oder Nichts).
// Siehe docker-compose.yml: CLAUDE_CODE_BUBBLEWRAP=1
// ── Einstellungen: Model ────────────────────────────────
function loadModel() {
send({ action: 'get_model' });
}
function saveModel() {
const model = document.getElementById('setting-model').value.trim();
if (!model) return;
send({ action: 'set_model', model });
}
// ── Einstellungen: OpenClaw Config ──────────────────────
// loadOpenClawConfig entfernt — aria-core ist raus.
// Toggle-Checkbox initial korrekt setzen
const ttsToggleEl = document.getElementById('tts-debug-toggle');
if (ttsToggleEl) ttsToggleEl.checked = showTtsDebug;
const gpsToggleEl = document.getElementById('gps-debug-toggle');
if (gpsToggleEl) gpsToggleEl.checked = showGpsDebug;
// Disk-Space Banner aktualisieren (wird vom Server via disk_status gepusht)
function updateDiskBanner(status) {
const banner = document.getElementById('disk-banner');
const icon = document.getElementById('disk-banner-icon');
const text = document.getElementById('disk-banner-text');
if (!banner) return;
if (!status || status.level === 'ok') {
banner.style.display = 'none';
return;
}
const gb = (n) => (n / 1024 / 1024 / 1024).toFixed(1);
const pct = status.percent;
const used = gb(status.usedBytes);
const total = gb(status.totalBytes);
const avail = gb(status.availBytes);
let bg, col, msg;
if (status.level === 'critical') {
bg = '#5C1A1A'; col = '#FF6B6B'; icon.innerHTML = '&#x1F6A8;'; // 🚨
msg = `KRITISCH: Platte ${pct}% voll (${used}GB von ${total}GB, nur noch ${avail}GB frei). aria-core kann bald nicht mehr schreiben — sofort aufraeumen!`;
} else if (status.level === 'warn') {
bg = '#5C3A1A'; col = '#FFAA55'; icon.innerHTML = '&#x26A0;&#xFE0F;'; // ⚠️
msg = `Warnung: Platte ${pct}% voll (${avail}GB frei). Bald aufraeumen.`;
} else {
bg = '#4A3A1A'; col = '#FFD60A'; icon.innerHTML = '&#x2139;&#xFE0F;'; //
msg = `Hinweis: Platte ${pct}% voll (${avail}GB frei).`;
}
banner.style.background = bg;
banner.style.color = col;
banner.style.borderBottom = `2px solid ${col}`;
text.textContent = msg;
banner.style.display = 'block';
}
function copyDiskCmd(variant) {
const cmd = variant === 'aggressive'
? 'docker system prune -a --volumes -f'
: 'docker builder prune -a -f && docker image prune -a -f';
navigator.clipboard.writeText(cmd).then(() => {
const btn = event.target;
const old = btn.textContent;
btn.textContent = 'Kopiert!';
setTimeout(() => { btn.textContent = old; }, 1500);
}).catch(() => {
alert('Kopieren fehlgeschlagen — Befehl: ' + cmd);
});
}
connectWS();
</script>
</body>
</html>