diff --git a/diagnostic/index.html b/diagnostic/index.html index 186d01f..2f430b3 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -283,6 +283,7 @@ +
@@ -302,6 +303,36 @@ +
@@ -340,6 +371,31 @@
+ +
+

Betriebsmodus

+
+
+ + + + + +
+
Aktueller Modus: Normal
+
+
+

Tool-Berechtigungen

@@ -420,6 +476,7 @@ 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 @@ -513,11 +570,43 @@ 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-default-voice').textContent = msg.defaultVoice || '?'; + document.getElementById('tts-highlight-voice').textContent = msg.highlightVoice || '?'; + document.getElementById('tts-status').textContent = msg.ok ? 'OK' : 'Fehler'; + document.getElementById('tts-status').style.color = msg.ok ? '#34C759' : '#FF3B30'; + if (msg.voices) ttsLog(`Stimmen: ${msg.voices.join(', ')}`); + 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 === '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; @@ -991,6 +1080,42 @@ }, 120000); } + // ── Modus-Wechsel ──────────────────────────── + let currentMode = 'normal'; + const MODE_LABELS = { normal: 'Normal', dnd: 'Nicht stoeren', whisper: 'Fluestern', hangar: 'Hangar', gaming: 'Gaming' }; + + function setMode(mode) { + currentMode = mode; + // Visuelles Feedback + document.querySelectorAll('.mode-btn').forEach(btn => { + btn.style.borderColor = btn.dataset.mode === mode ? '#0096FF' : 'transparent'; + }); + document.getElementById('mode-status').textContent = `Aktueller Modus: ${MODE_LABELS[mode] || mode}`; + // An Bridge senden via RVS + sendToRVS(`ARIA, ${MODE_LABELS[mode]}-Modus`, false); + log("info", "server", `Modus gewechselt: ${mode}`); + } + + // ── TTS Diagnose ───────────────────────────── + function ttsLog(msg) { + const el = document.getElementById('tts-log'); + const time = new Date().toLocaleTimeString('de-DE'); + el.innerHTML += `
[${time}] ${escapeHtml(msg)}
`; + 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') { diff --git a/diagnostic/server.js b/diagnostic/server.js index 993d857..f3aab37 100644 --- a/diagnostic/server.js +++ b/diagnostic/server.js @@ -336,6 +336,7 @@ function handleGatewayMessage(msg) { // Genereller Activity-Heartbeat (ARIA denkt) broadcast({ type: "agent_activity", activity: stream || "thinking" }); + updateAgentActivity(); return; } @@ -352,6 +353,8 @@ function handleGatewayMessage(msg) { if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`); broadcast({ type: "chat_final", text, payload }); broadcast({ type: "agent_activity", activity: "idle" }); + pendingMessageTime = 0; // Watchdog: Antwort erhalten + updateAgentActivity(); return; } @@ -424,6 +427,7 @@ function sendToGateway(text, isPipeline) { const payload = JSON.stringify(msg); log("debug", "gateway", `RAW >>> ${payload}`); gatewayWs.send(payload); + pendingMessageTime = Date.now(); // Watchdog: Nachricht gesendet log("info", "gateway", `chat.send [${reqId}]: "${text}"`); if (isPipeline) plog(`chat.send [${reqId}] an Gateway gesendet — warte auf ACK...`); @@ -1017,6 +1021,46 @@ function waitForMessage(ws, timeoutMs) { }); } +// ── Watchdog: Stuck Run Erkennung ──────────────────────── + +let lastAgentActivity = Date.now(); +let watchdogWarned = false; +let pendingMessageTime = 0; // Wann wurde die letzte Nachricht gesendet + +function updateAgentActivity() { + lastAgentActivity = Date.now(); + watchdogWarned = false; +} + +// Watchdog prüft alle 30s ob ARIA nach einer gesendeten Nachricht reagiert +setInterval(async () => { + if (pendingMessageTime === 0) return; // Keine Nachricht gesendet + const waitingMs = Date.now() - pendingMessageTime; + + // Nach 2min ohne Agent-Activity: Warnung + if (waitingMs > 120000 && !watchdogWarned) { + watchdogWarned = true; + log("warn", "server", `Watchdog: Keine ARIA-Aktivitaet seit ${Math.round(waitingMs / 1000)}s — moeglicherweise stuck`); + broadcast({ type: "watchdog", status: "warning", waitingMs, message: "ARIA reagiert nicht — moeglicherweise stuck Run" }); + } + + // Nach 5min: Auto-Fix anbieten + if (waitingMs > 300000 && watchdogWarned) { + log("error", "server", "Watchdog: 5min ohne Antwort — fuehre openclaw doctor --fix aus"); + broadcast({ type: "watchdog", status: "fixing", message: "Auto-Fix: openclaw doctor --fix" }); + try { + await dockerExec("aria-core", "openclaw doctor --fix 2>/dev/null || true"); + log("info", "server", "Watchdog: doctor --fix ausgefuehrt"); + broadcast({ type: "watchdog", status: "fixed", message: "doctor --fix ausgefuehrt — sende Nachricht erneut" }); + } catch (err) { + log("error", "server", `Watchdog: doctor --fix fehlgeschlagen: ${err.message}`); + broadcast({ type: "watchdog", status: "error", message: `Auto-Fix fehlgeschlagen: ${err.message}` }); + } + pendingMessageTime = 0; // Reset + watchdogWarned = false; + } +}, 30000); + // ── HTTP Server + WebSocket fuer Browser ──────────────── const htmlPath = path.join(__dirname, "index.html"); @@ -1103,6 +1147,10 @@ wss.on("connection", (ws) => { if (ws._sshSock) ws._sshSock.write(msg.data); } else if (msg.action === "live_ssh_close") { if (ws._sshSock) { ws._sshSock.end(); ws._sshSock = null; } + } else if (msg.action === "test_tts") { + handleTestTTS(ws, msg.voice || "ramona", msg.text || "Test"); + } else if (msg.action === "check_tts") { + handleCheckTTS(ws); } else if (msg.action === "check_desktop") { checkDesktopAvailable(ws); } else if (msg.action === "load_chat_history") { @@ -1229,6 +1277,69 @@ function startLiveSSH(clientWs) { createReq.end(createBody); } +// ── TTS Diagnose ────────────────────────────────────── +async function handleTestTTS(clientWs, voice, text) { + try { + log("info", "server", `TTS-Test: ${voice} — "${text}"`); + const result = await dockerExec("aria-bridge", `python3 -c " +import time, sys +sys.path.insert(0, '/app') +from piper import PiperVoice +import wave, tempfile, os +voices = {'ramona': '/voices/de_DE-ramona-low.onnx', 'thorsten': '/voices/de_DE-thorsten-high.onnx'} +path = voices.get('${voice}') +if not path or not os.path.exists(path): + print('FEHLER: Stimme nicht gefunden') + sys.exit(1) +v = PiperVoice.load(path) +start = time.time() +tmp = tempfile.NamedTemporaryFile(suffix='.wav', delete=False) +with wave.open(tmp.name, 'wb') as wf: + wf.setnchannels(1) + wf.setsampwidth(2) + wf.setframerate(v.config.sample_rate) + v.synthesize('${text.replace(/'/g, "\\'")}', wf) +size = os.path.getsize(tmp.name) +dur = int((time.time() - start) * 1000) +os.unlink(tmp.name) +print(f'OK:{dur}:{size}') +"`); + const parts = result.trim().split(":"); + if (parts[0] === "OK") { + clientWs.send(JSON.stringify({ type: "tts_result", ok: true, voice, duration: parts[1], size: parts[2] })); + } else { + clientWs.send(JSON.stringify({ type: "tts_result", ok: false, voice, error: result.trim() })); + } + } catch (err) { + clientWs.send(JSON.stringify({ type: "tts_result", ok: false, voice, error: err.message })); + } +} + +async function handleCheckTTS(clientWs) { + try { + const result = await dockerExec("aria-bridge", `python3 -c " +import os, json +voices = {} +for name, path in [('ramona', '/voices/de_DE-ramona-low.onnx'), ('thorsten', '/voices/de_DE-thorsten-high.onnx')]: + voices[name] = os.path.exists(path) +print(json.dumps(voices)) +"`); + const voices = JSON.parse(result.trim()); + const available = Object.entries(voices).filter(([,v]) => v).map(([k]) => k); + const missing = Object.entries(voices).filter(([,v]) => !v).map(([k]) => k); + clientWs.send(JSON.stringify({ + type: "tts_status", + ok: missing.length === 0, + voices: available, + defaultVoice: "ramona", + highlightVoice: "thorsten", + error: missing.length > 0 ? `Fehlend: ${missing.join(", ")}` : null, + })); + } catch (err) { + clientWs.send(JSON.stringify({ type: "tts_status", ok: false, error: err.message })); + } +} + function checkDesktopAvailable(clientWs) { // Pruefen ob VNC auf der VM laeuft (Port 5900/5901) const checkSock = net.connect({ host: "host.docker.internal", port: 5901 }, () => {