ARIA-AGENT/diagnostic/index.html

2490 lines
121 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.pipeline-step { color: #0096FF; border-left: 2px solid #0096FF; padding-left: 6px; margin: 2px 0; }
.log-entry.pipeline-ok { color: #34C759; border-left: 2px solid #34C759; padding-left: 6px; margin: 2px 0; }
.log-entry.pipeline-err { color: #FF3B30; border-left: 2px solid #FF3B30; padding-left: 6px; margin: 2px 0; }
.log-entry.pipeline-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: 8px; margin-bottom: 8px; }
.chat-msg { margin-bottom: 6px; padding: 6px 10px; border-radius: 6px; font-size: 13px; line-height: 1.5; word-wrap: break-word; }
.chat-msg.sent { background: #0096FF; color: #fff; margin-left: 20%; text-align: right; }
.chat-msg.received { background: #1E1E2E; margin-right: 20%; }
.chat-msg.error { background: #3B1010; color: #FF6B6B; }
.chat-msg .meta { font-size: 10px; color: rgba(255,255,255,0.4); margin-top: 2px; }
.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: 4px; margin-top: 4px; 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; }
.input-row input { flex: 1; background: #1E1E2E; border: 1px solid #333; border-radius: 6px;
padding: 8px 12px; color: #E0E0F0; font-family: inherit; font-size: 13px; }
/* 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; }
.perm-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(220px, 1fr)); gap:8px; }
.perm-item { display:flex; align-items:center; justify-content:space-between; background:#12122A;
border:1px solid #1E1E2E; border-radius:6px; padding:8px 12px; }
.perm-item .perm-info { flex:1; min-width:0; }
.perm-item .perm-name { font-size:13px; color:#E0E0F0; font-weight:bold; }
.perm-item .perm-desc { font-size:10px; color:#8888AA; margin-top:2px; }
.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;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('settings')">Einstellungen</button>
</div>
<!-- ══════ TAB: Main ══════ -->
<div id="tab-main" class="main-tab visible">
<!-- Verbindungsstatus -->
<div class="grid">
<div class="card">
<h2>OpenClaw Gateway</h2>
<div class="status-row">
<div class="dot" id="gw-dot"></div>
<span class="status-label" id="gw-status">-</span>
</div>
<div class="error-text" id="gw-error"></div>
<div id="core-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>
<button class="btn secondary" onclick="send({action:'reconnect_gateway'})">Reconnect</button>
<button class="btn secondary" onclick="send({action:'check_core_auth'})">Agent-Auth pruefen</button>
<button class="btn secondary" id="btn-core-term" onclick="openCoreTerminal()">Core Terminal</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>
<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>
<button class="btn secondary" onclick="cancelRequest()" style="padding:2px 10px;font-size:11px;color:#FF3B30;border-color:#FF3B30;">Abbrechen</button>
</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>
<input type="text" id="chat-input" placeholder="Nachricht an ARIA..." onpaste="handleDiagPaste(event)">
<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;">
<input type="text" id="chat-input-fs" placeholder="Nachricht an ARIA..." onkeydown="if(event.key==='Enter'){testRVSFS();event.preventDefault();}">
<button class="btn" onclick="testGatewayFS()">Gateway senden</button>
<button class="btn" onclick="testRVSFS()">Via RVS senden</button>
</div>
</div>
<!-- Session + Brain Viewer -->
<div class="grid" style="grid-template-columns: 1fr 1fr;">
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<h2>Sessions</h2>
<div style="display:flex;gap:4px;">
<button class="btn secondary" onclick="createSession()" style="padding:4px 10px;font-size:11px;">+ Neu</button>
<button class="btn secondary" onclick="loadSessions()" style="padding:4px 10px;font-size:11px;">Laden</button>
</div>
</div>
<div id="active-session-bar" style="font-size:10px;color:#34C759;margin-bottom:6px;padding:4px 6px;background:#0D0D1A;border-radius:4px;display:none;">
Aktiv: <span id="active-session-name" style="font-weight:bold;"></span>
</div>
<div id="sessions-list" style="max-height:300px;overflow-y:auto;font-size:12px;"></div>
<div id="session-detail" style="display:none;margin-top:8px;background:#080810;border:1px solid #1E1E2E;border-radius:4px;padding:8px;max-height:300px;overflow-y:auto;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">
<span style="font-size:11px;color:#0096FF;font-weight:bold;" id="session-detail-title"></span>
<button class="btn secondary" onclick="closeSessionDetail()" style="padding:2px 8px;font-size:10px;">Schliessen</button>
</div>
<div id="session-detail-content" style="font-size:11px;line-height:1.5;"></div>
</div>
</div>
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<h2>Brain / Memory</h2>
<button class="btn secondary" onclick="loadBrain()" style="padding:4px 10px;font-size:11px;">Laden</button>
</div>
<div id="brain-list" style="max-height:200px;overflow-y:auto;font-size:12px;"></div>
<div id="brain-content" style="display:none;margin-top:8px;background:#080810;border:1px solid #1E1E2E;border-radius:4px;padding:8px;max-height:300px;overflow-y:auto;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">
<span style="font-size:11px;color:#0096FF;font-weight:bold;" id="brain-content-title"></span>
<button class="btn secondary" onclick="closeBrainContent()" style="padding:2px 8px;font-size:10px;">Schliessen</button>
</div>
<pre id="brain-content-text" style="font-size:11px;line-height:1.5;white-space:pre-wrap;color:#E0E0F0;margin:0;"></pre>
</div>
<div id="brain-empty" style="display:none;text-align:center;padding:20px;color:#555570;">
<div style="font-size:24px;margin-bottom:8px;">🧠</div>
<div style="font-size:12px;">Gehirn ist leer</div>
<div style="font-size:10px;margin-top:4px;">ARIA speichert Erinnerungen wenn sie etwas Wichtiges lernt</div>
</div>
</div>
</div>
<!-- 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="pipeline" onclick="switchTab('pipeline')" style="margin-left:auto;border-color:#0096FF44;color:#0096FF">Pipeline <span class="tab-count" id="count-pipeline">0</span></button>
<button class="tab-btn" data-tab="tts" onclick="switchTab('tts')" style="border-color:#34C75944;color:#34C759">TTS</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-pipeline"></div>
<div class="log-box hidden" id="log-tts" style="padding:12px;">
<h3 style="color:#34C759;margin:0 0 12px;">TTS Diagnose (XTTS)</h3>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px;">
<div style="background:#1E1E2E;padding:8px;border-radius:6px;">
<div style="color:#8888AA;font-size:10px;text-transform:uppercase;">Status</div>
<div style="font-size:14px;margin-top:4px;" id="tts-status">Unbekannt</div>
</div>
<div style="background:#1E1E2E;padding:8px;border-radius:6px;">
<div style="color:#8888AA;font-size:10px;text-transform:uppercase;">Letzter Fehler</div>
<div style="color:#FF6B6B;font-size:12px;margin-top:4px;word-break:break-all;" id="tts-last-error">-</div>
</div>
</div>
<div style="margin-bottom:8px;">
<input type="text" id="tts-test-text" value="Hallo Stefan, ich bin ARIA." placeholder="Test-Text..." style="background:#1E1E2E;border:1px solid #2A2A3E;border-radius:6px;padding:8px;color:#fff;font-size:13px;width:100%;box-sizing:border-box;">
</div>
<div style="display:flex;gap:8px;">
<button class="btn" onclick="testTTS('')" style="flex:1;">XTTS testen</button>
<button class="btn secondary" onclick="checkTTSStatus()" style="flex:1;">Status pruefen</button>
</div>
<div id="tts-log" style="margin-top:12px;max-height:200px;overflow-y:auto;font-size:11px;font-family:monospace;color:#8888AA;"></div>
</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">
<!-- 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">
<h2>Sprachausgabe</h2>
<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 (migriert von .env) -->
<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 <code>aria.env</code>. Bridge liest
sie beim naechsten Start — nach Aenderung <b>Bridge-Container neu starten</b>
(Diagnostic-Container bleibt auf ENV).
</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>
<!-- Highlight-Trigger -->
<div class="settings-section">
<h2>Highlight-Trigger</h2>
<div style="font-size:11px;color:#8888AA;margin-bottom:8px;">
Woerter die automatisch die Highlight-Stimme (Thorsten) ausloesen.
Eines pro Zeile. Aenderungen werden in der Bridge gespeichert.
</div>
<div class="card" style="max-width:500px;">
<textarea id="highlight-triggers" rows="8" style="width:100%;box-sizing:border-box;background:#1E1E2E;border:1px solid #2A2A3E;border-radius:6px;padding:8px;color:#fff;font-size:13px;font-family:monospace;resize:vertical;"
placeholder="Lade..."></textarea>
<div style="display:flex;gap:8px;margin-top:8px;">
<button class="btn" onclick="saveHighlightTriggers()" style="flex:1;">Speichern</button>
<button class="btn secondary" onclick="loadHighlightTriggers()" style="flex:1;">Neu laden</button>
</div>
<div id="trigger-status" style="font-size:11px;color:#555570;margin-top:6px;"></div>
</div>
</div>
<!-- Tool-Berechtigungen -->
<div class="settings-section">
<h2>Tool-Berechtigungen</h2>
<div style="font-size:11px;color:#8888AA;margin-bottom:12px;">
Claude Code laeuft mit <code>--dangerously-skip-permissions</code> im Proxy — Alles oder Nichts.
Granulare Tool-Kontrolle ist in dieser Architektur nicht moeglich.
</div>
<div class="card" style="max-width:500px;">
<div class="perm-item" style="border:none;padding:0;">
<div class="perm-info">
<div class="perm-name">Claude darf alle Tools benutzen</div>
<div class="perm-desc">WebFetch, Bash, Read/Write/Edit, WebSearch, Agent, etc.</div>
</div>
<label class="toggle"><input type="checkbox" checked disabled><span class="slider"></span></label>
</div>
<div style="font-size:10px;color:#555570;margin-top:8px;">
Deaktivieren = <code>--dangerously-skip-permissions</code> aus docker-compose.yml entfernen.
Dann kann ARIA keine Tools mehr benutzen (auch kein WebFetch fuer Wetter etc.).
</div>
</div>
</div>
<!-- Model-Einstellungen -->
<div class="settings-section">
<h2>Model</h2>
<div class="card" style="max-width:500px;">
<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="z.B. proxy/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 Info -->
<div class="settings-section">
<h2>OpenClaw Config</h2>
<div class="card">
<button class="btn secondary" onclick="loadOpenClawConfig()" style="padding:4px 12px;font-size:11px;margin-bottom:8px;">Config laden</button>
<pre id="openclaw-config" style="font-size:10px;color:#8888AA;white-space:pre-wrap;max-height:300px;overflow-y:auto;margin:0;">(Noch nicht geladen)</pre>
</div>
</div>
</div><!-- /tab-settings -->
<!-- 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, pipeline: true };
const logCounts = { all: 0, gateway: 0, rvs: 0, proxy: 0, bridge: 0, server: 0, pipeline: 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'),
pipeline: document.getElementById('log-pipeline'),
tts: document.getElementById('log-tts'),
};
// 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 === 'pipeline') return 'pipeline';
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: 'get_active_session' });
send({ action: 'load_chat_history' });
};
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 === 'tts_result') {
if (msg.ok) {
ttsLog(`\u2705 ${msg.voice}: ${msg.duration}ms, ${msg.size} bytes`);
document.getElementById('tts-status').textContent = 'OK';
document.getElementById('tts-status').style.color = '#34C759';
} else {
ttsLog(`\u274C Fehler: ${msg.error}`);
document.getElementById('tts-status').textContent = 'Fehler';
document.getElementById('tts-status').style.color = '#FF3B30';
document.getElementById('tts-last-error').textContent = msg.error;
}
return;
}
if (msg.type === 'tts_status') {
document.getElementById('tts-status').textContent = msg.ok ? 'OK' : 'Fehler';
document.getElementById('tts-status').style.color = msg.ok ? '#34C759' : '#FF3B30';
if (msg.error) { document.getElementById('tts-last-error').textContent = msg.error; ttsLog(`Fehler: ${msg.error}`); }
else { document.getElementById('tts-last-error').textContent = '-'; ttsLog('TTS OK'); }
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 === '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 === 'trigger_list') {
const textarea = document.getElementById('highlight-triggers');
textarea.value = (msg.triggers || []).join('\n');
document.getElementById('trigger-status').textContent = msg.triggers.length + ' Trigger geladen';
document.getElementById('trigger-status').style.color = '#8888AA';
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 === '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);
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;
}
if (msg.type === 'core_auth') {
const el = document.getElementById('core-auth');
el.style.display = 'block';
el.textContent = msg.error ? `Fehler: ${msg.error}` : msg.info;
return;
}
// 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) {
const el = document.createElement('div');
el.className = `chat-msg ${m.type}`;
const escaped = escapeHtml(m.text);
const linked = linkifyText(escaped);
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 + Brain Viewer
if (msg.type === 'sessions_list') { renderSessions(msg); return; }
if (msg.type === 'session_detail') { renderSessionDetail(msg); return; }
if (msg.type === 'session_deleted') {
if (msg.ok) loadSessions();
else alert('Loeschen fehlgeschlagen: ' + (msg.error || '?'));
return;
}
if (msg.type === 'session_export') {
if (!msg.ok) { alert('Export fehlgeschlagen: ' + (msg.error || '?')); return; }
const blob = new Blob([msg.markdown], { type: 'text/markdown;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = msg.filename;
document.body.appendChild(a);
a.click();
setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 100);
return;
}
if (msg.type === 'active_session') {
updateActiveSessionBar(msg.sessionKey);
loadSessions(); // Tabelle neu rendern
send({ action: 'load_chat_history' }); // Chat-Verlauf der neuen Session laden
return;
}
if (msg.type === 'session_created') {
if (msg.ok) {
loadSessions();
} else {
alert('Session erstellen fehlgeschlagen: ' + (msg.error || '?'));
}
return;
}
if (msg.type === 'brain_list') { renderBrainList(msg); return; }
if (msg.type === 'brain_content') { renderBrainContent(msg); return; }
// Settings (permissions_list/permissions_saved entfernt — Alles-oder-Nichts via --dangerously-skip-permissions)
if (msg.type === 'session_restarted') {
const s = document.getElementById('perms-status');
s.style.display = 'block';
if (msg.status === 'restarting') {
s.style.color = '#FFD60A';
s.textContent = 'aria-core wird neu gestartet...';
} else if (msg.status === 'ok') {
s.style.color = '#34C759';
s.textContent = msg.info || 'Session neu gestartet!';
} else {
s.style.color = '#FF6B6B';
s.textContent = 'Restart fehlgeschlagen: ' + (msg.error || '?');
}
return;
}
if (msg.type === 'model_info') {
const el = document.getElementById('setting-model');
const st = document.getElementById('model-status');
if (msg.model) el.value = msg.model;
st.textContent = msg.info || '';
st.style.color = msg.error ? '#FF6B6B' : '#34C759';
return;
}
if (msg.type === 'openclaw_config') {
document.getElementById('openclaw-config').textContent = msg.config || msg.error || '(leer)';
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' });
}
function openCoreTerminal() {
document.getElementById('btn-core-term').disabled = true;
openTermModal('aria-core Shell', { action: 'core_terminal' });
}
function closeTermModal() {
document.getElementById('term-modal').classList.remove('open');
document.getElementById('btn-proxy-login').disabled = false;
document.getElementById('btn-core-term').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) {
// Gateway
const gw = state.gateway || {};
document.getElementById('gw-dot').className = `dot ${gw.status || 'disconnected'}`;
document.getElementById('gw-status').textContent =
(STATUS_LABELS[gw.status] || gw.status) + (gw.handshakeOk ? ' (Handshake OK)' : '');
document.getElementById('gw-error').textContent = gw.lastError || '';
// 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}`;
// Pipeline-Eintraege nur in Pipeline-Tab (nicht in Alle)
if (source === 'pipeline') {
const pipeLevel = level === 'error' ? 'pipeline-err' : level === 'info' && message.includes('>>>') ? 'pipeline-ok' : 'pipeline-step';
appendToLog('pipeline', pipeLevel, `${time} ${message}`);
logCounts.pipeline++;
document.getElementById('count-pipeline').textContent = logCounts.pipeline;
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;
}
// 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) {
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>`;
}
}
const html = `${linked}${ttsBlock}<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;
}
}
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="deleteXttsVoice('${jsName}')" style="padding:2px 8px;font-size:10px;color:#FF6B6B;" title="Stimme loeschen">X</button>`
+ `</div>`;
}
html += '</div>';
box.innerHTML = html;
}
// ── Voice Preview Modal ─────────────────────────
const VOICE_PREVIEW_DEFAULT = 'Hallo, ich bin ARIA. Das hier ist ein kleiner Test damit du meine Stimme beurteilen kannst.';
let currentPreviewVoice = '';
function openVoicePreview(name) {
currentPreviewVoice = name;
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 });
}
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();
}
// ── 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>`;
}
}
// ── Highlight-Trigger ────────────────────────
function loadHighlightTriggers() {
send({ action: 'get_triggers' });
}
function saveHighlightTriggers() {
const text = document.getElementById('highlight-triggers').value;
const triggers = text.split('\n').map(t => t.trim()).filter(t => t.length > 0);
send({ action: 'save_triggers', triggers });
document.getElementById('trigger-status').textContent = 'Gespeichert (' + triggers.length + ' Trigger)';
document.getElementById('trigger-status').style.color = '#34C759';
}
// Beim Tab-Wechsel zu Einstellungen: Trigger laden
const origSwitchMainTab = typeof switchMainTab === 'function' ? switchMainTab : null;
// ── 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 });
}
// ── TTS Diagnose ─────────────────────────────
function ttsLog(msg) {
const el = document.getElementById('tts-log');
const time = new Date().toLocaleTimeString('de-DE');
el.innerHTML += `<div>[${time}] ${escapeHtml(msg)}</div>`;
el.scrollTop = el.scrollHeight;
}
function testTTS(voice) {
const text = document.getElementById('tts-test-text').value.trim();
if (!text) return;
ttsLog(`Teste ${voice}: "${text}"...`);
send({ action: 'test_tts', voice, text });
}
function checkTTSStatus() {
ttsLog('Pruefe TTS-Status...');
send({ action: 'check_tts' });
}
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;');
}
// Enter-Taste sendet via Gateway
document.getElementById('chat-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter') testRVS();
});
// 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';
}
}
}
// ── Session Viewer ────────────────────────────────────────
function loadSessions() {
document.getElementById('sessions-list').innerHTML = '<div style="color:#8888AA;padding:8px;">Lade...</div>';
send({ action: 'list_sessions' });
}
let currentActiveSession = '';
function renderSessions(data) {
const container = document.getElementById('sessions-list');
if (data.error) {
container.innerHTML = `<div style="color:#FF6B6B;padding:8px;">Fehler: ${escapeHtml(data.error)}</div>`;
return;
}
if (!data.sessions || data.sessions.length === 0) {
container.innerHTML = data.raw
? `<pre style="color:#555570;font-size:10px;white-space:pre-wrap;padding:8px;">${escapeHtml(data.raw)}</pre>`
: '<div style="color:#555570;padding:8px;text-align:center;">Keine Sessions gefunden</div>';
return;
}
const active = data.sessions.filter(s => !s.archived);
const archives = data.sessions.filter(s => s.archived);
const headerRow = '<tr style="color:#8888AA;font-size:10px;text-align:left;border-bottom:1px solid #1E1E2E;">'
+ '<th style="padding:4px 6px;">Session</th>'
+ '<th style="padding:4px 6px;">Msgs</th>'
+ '<th style="padding:4px 6px;">Zuletzt</th>'
+ '<th style="padding:4px 6px;"></th></tr>';
const rowFor = (s, opts) => {
const date = s.modified ? new Date(s.modified * 1000).toLocaleString('de-DE', {day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}) : '?';
const key = escapeHtml(s.sessionKey || s.path.split('/').pop());
const orphanBadge = s.orphan ? ' <span style="background:#FF3B30;color:#fff;font-size:9px;padding:1px 4px;border-radius:3px;">verwaist</span>' : '';
const archivedBadge = s.archived ? ' <span style="background:#555570;color:#fff;font-size:9px;padding:1px 4px;border-radius:3px;">archiv</span>' : '';
const modelBadge = s.model ? `<div style="font-size:9px;color:#555570;">${escapeHtml(s.model)}</div>` : '';
const isActive = (s.sessionKey === currentActiveSession) && !s.archived;
const keyColor = isActive ? '#34C759' : (s.archived || s.orphan ? '#8888AA' : '#E0E0F0');
const activeBadge = isActive ? ' <span style="background:#34C759;color:#000;font-size:9px;padding:1px 4px;border-radius:3px;">aktiv</span>' : '';
const rowBg = isActive ? 'background:rgba(52,199,89,0.08);' : (s.archived ? 'background:rgba(136,136,170,0.04);' : '');
let actions = '';
if (s.archived) {
// Archive: nur Export + Loeschen (kein Aktivieren — wuerde aktive Session ueberschreiben)
actions = `<button class="btn secondary" onclick="event.stopPropagation();deleteSession('${escapeHtml(s.path)}')" style="padding:2px 6px;font-size:10px;color:#FF6B6B;margin-right:2px;" title="Archiv endgueltig loeschen">X</button>`
+ `<button class="btn secondary" onclick="event.stopPropagation();exportSession('${escapeHtml(s.path)}','${escapeHtml(s.sessionKey)}')" style="padding:2px 6px;font-size:10px;color:#8888AA;" title="Als Markdown exportieren">&#x2B07;</button>`;
} else {
actions = (isActive ? '' : `<button class="btn secondary" onclick="event.stopPropagation();activateSession('${escapeHtml(s.sessionKey)}')" style="padding:2px 6px;font-size:10px;color:#34C759;margin-right:2px;" title="Aktivieren">&#9654;</button>`)
+ `<button class="btn secondary" onclick="event.stopPropagation();deleteSession('${escapeHtml(s.path)}')" style="padding:2px 6px;font-size:10px;color:#FF6B6B;margin-right:2px;" title="Loeschen">X</button>`
+ `<button class="btn secondary" onclick="event.stopPropagation();exportSession('${escapeHtml(s.path)}','${escapeHtml(s.sessionKey)}')" style="padding:2px 6px;font-size:10px;color:#8888AA;" title="Als Markdown exportieren">&#x2B07;</button>`;
}
return `<tr style="border-bottom:1px solid #0D0D1A;cursor:pointer;${rowBg}" onmouseover="this.style.background='#1E1E2E'" onmouseout="this.style.background='${isActive ? 'rgba(52,199,89,0.08)' : (s.archived ? 'rgba(136,136,170,0.04)' : '')}'">`
+ `<td style="padding:4px 6px;" onclick="viewSession('${escapeHtml(s.path)}')">`
+ `<div style="color:${keyColor};">${key}${activeBadge}${orphanBadge}${archivedBadge}</div>${modelBadge}</td>`
+ `<td style="padding:4px 6px;color:#8888AA;">${s.lines}</td>`
+ `<td style="padding:4px 6px;color:#8888AA;font-size:10px;">${date}</td>`
+ `<td style="padding:4px 6px;white-space:nowrap;">${actions}</td></tr>`;
};
let html = '<table style="width:100%;border-collapse:collapse;">' + headerRow;
for (const s of active) html += rowFor(s);
html += '</table>';
if (archives.length > 0) {
html += `<details style="margin-top:12px;" ${archives.length <= 5 ? 'open' : ''}>`
+ `<summary style="color:#8888AA;font-size:11px;cursor:pointer;padding:4px 0;">`
+ `Archivierte Versionen (${archives.length}) — von OpenClaw beim Session-Reset gesichert`
+ `</summary>`
+ `<table style="width:100%;border-collapse:collapse;margin-top:6px;">` + headerRow;
for (const s of archives) html += rowFor(s);
html += '</table></details>';
}
container.innerHTML = html;
}
function viewSession(path) {
const detail = document.getElementById('session-detail');
const title = document.getElementById('session-detail-title');
const content = document.getElementById('session-detail-content');
detail.style.display = 'block';
title.textContent = path.split('/').pop();
content.innerHTML = '<div style="color:#8888AA;">Lade...</div>';
send({ action: 'read_session', sessionPath: path });
}
function renderSessionDetail(data) {
const content = document.getElementById('session-detail-content');
if (data.error) {
content.innerHTML = `<div style="color:#FF6B6B;">${escapeHtml(data.error)}</div>`;
return;
}
if (data.raw) {
content.innerHTML = `<pre style="color:#555570;font-size:10px;white-space:pre-wrap;">${escapeHtml(data.raw)}</pre>`;
return;
}
if (!data.messages || data.messages.length === 0) {
content.innerHTML = '<div style="color:#555570;">Keine Nachrichten</div>';
return;
}
let html = '';
for (const msg of data.messages) {
const role = msg.role || msg.type || '?';
const text = extractMessageText(msg);
const roleColor = role === 'user' ? '#0096FF' : role === 'assistant' ? '#34C759' : '#8888AA';
html += `<div style="margin-bottom:4px;padding:4px 6px;border-left:2px solid ${roleColor};background:#0D0D1A;border-radius:0 4px 4px 0;">`
+ `<span style="color:${roleColor};font-size:10px;font-weight:bold;">${escapeHtml(role)}</span> `
+ `<span style="color:#E0E0F0;">${escapeHtml(text.slice(0, 500))}${text.length > 500 ? '...' : ''}</span>`
+ '</div>';
}
content.innerHTML = html;
}
function extractMessageText(msg) {
if (typeof msg.content === 'string') return msg.content;
if (Array.isArray(msg.content)) {
return msg.content.filter(c => c.type === 'text').map(c => c.text || '').join('');
}
if (msg.text) return msg.text;
if (msg.message) return typeof msg.message === 'string' ? msg.message : JSON.stringify(msg.message);
return JSON.stringify(msg).slice(0, 200);
}
function closeSessionDetail() {
document.getElementById('session-detail').style.display = 'none';
}
function deleteSession(path) {
const name = path.split('/').pop();
if (!confirm(`Session "${name}" wirklich loeschen?`)) return;
send({ action: 'delete_session', sessionPath: path });
}
function exportSession(path, sessionKey) {
send({ action: 'export_session', sessionPath: path, sessionKey });
}
function activateSession(sessionKey) {
send({ action: 'set_active_session', sessionKey });
}
function createSession() {
const name = prompt('Name fuer neue Session (a-z, 0-9, -, _):');
if (!name) return;
send({ action: 'create_session', sessionName: name.trim() });
}
function updateActiveSessionBar(sessionKey) {
currentActiveSession = sessionKey || '';
const bar = document.getElementById('active-session-bar');
const nameEl = document.getElementById('active-session-name');
if (currentActiveSession) {
bar.style.display = 'block';
nameEl.textContent = currentActiveSession;
} else {
bar.style.display = 'none';
}
}
// ── Brain Viewer ────────────────────────────────────────
function loadBrain() {
document.getElementById('brain-list').innerHTML = '<div style="color:#8888AA;padding:8px;">Lade...</div>';
document.getElementById('brain-empty').style.display = 'none';
send({ action: 'list_brain' });
}
function renderBrainList(data) {
const container = document.getElementById('brain-list');
const emptyEl = document.getElementById('brain-empty');
if (data.error) {
container.innerHTML = `<div style="color:#FF6B6B;padding:8px;">Fehler: ${escapeHtml(data.error)}</div>`;
emptyEl.style.display = 'none';
return;
}
if (!data.files || data.files.length === 0) {
container.innerHTML = '';
emptyEl.style.display = 'block';
return;
}
emptyEl.style.display = 'none';
const TYPE_COLORS = { user: '#0096FF', feedback: '#FFD60A', project: '#34C759', reference: '#FF9500' };
let html = '';
for (const f of data.files) {
if (f.name === '.gitkeep') continue;
const color = TYPE_COLORS[f.memType] || '#8888AA';
const date = f.modified ? new Date(f.modified * 1000).toLocaleString('de-DE') : '?';
html += `<div style="padding:6px 8px;border-bottom:1px solid #0D0D1A;cursor:pointer;display:flex;align-items:center;gap:8px;" onmouseover="this.style.background='#1E1E2E'" onmouseout="this.style.background=''" onclick="viewBrainFile('${escapeHtml(f.name)}')">`
+ `<span style="background:${color};width:8px;height:8px;border-radius:50%;flex-shrink:0;" title="${escapeHtml(f.memType || 'unbekannt')}"></span>`
+ `<div style="flex:1;min-width:0;">`
+ `<div style="color:#E0E0F0;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${escapeHtml(f.name)}</div>`
+ (f.description ? `<div style="color:#8888AA;font-size:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${escapeHtml(f.description)}</div>` : '')
+ `</div>`
+ `<div style="color:#555570;font-size:10px;white-space:nowrap;">${escapeHtml(f.size)}</div>`
+ '</div>';
}
container.innerHTML = html || '<div style="color:#555570;padding:8px;text-align:center;">Nur .gitkeep gefunden</div>';
}
function viewBrainFile(name) {
const panel = document.getElementById('brain-content');
const title = document.getElementById('brain-content-title');
const text = document.getElementById('brain-content-text');
panel.style.display = 'block';
title.textContent = name;
text.textContent = 'Lade...';
send({ action: 'read_brain_file', filename: name });
}
function renderBrainContent(data) {
const text = document.getElementById('brain-content-text');
if (data.error) {
text.textContent = `Fehler: ${data.error}`;
text.style.color = '#FF6B6B';
return;
}
text.style.color = '#E0E0F0';
text.textContent = data.content || '(leer)';
}
function closeBrainContent() {
document.getElementById('brain-content').style.display = 'none';
}
// ── 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
document.querySelectorAll('.main-nav-btn').forEach(b => {
if (b.textContent.trim().toLowerCase().includes(tab === 'main' ? 'main' : 'einstellung')) b.classList.add('active');
});
// Einstellungen: Config + Trigger + QR laden
if (tab === 'settings') {
loadHighlightTriggers();
send({ action: 'get_voice_config' });
loadRuntimeConfig();
loadOnboardingQR();
}
}
// ── 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 ──────────────────────
function loadOpenClawConfig() {
document.getElementById('openclaw-config').textContent = 'Lade...';
send({ action: 'get_openclaw_config' });
}
// Toggle-Checkbox initial korrekt setzen
const ttsToggleEl = document.getElementById('tts-debug-toggle');
if (ttsToggleEl) ttsToggleEl.checked = showTtsDebug;
// 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>