+
+
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 }, () => {