"use strict";

/**
 * ARIA Diagnostic Server
 *
 * Leichtgewichtiges Diagnose-Tool:
 * - Verbindet sich direkt mit OpenClaw Gateway (ws://127.0.0.1:18789)
 * - Fuehrt den vollstaendigen Handshake durch
 * - Sendet Testnachrichten und zeigt Antworten
 * - Zeigt Verbindungsstatus aller Komponenten
 *
 * Laeuft im selben Netzwerk wie aria-core (network_mode: service:aria)
 */

const http = require("http");
const { WebSocket, WebSocketServer } = require("ws");
const crypto = require("crypto");
const fs = require("fs");
const path = require("path");

// ── Konfiguration ───────────────────────────────────────
const HTTP_PORT = parseInt(process.env.DIAG_PORT || "3001", 10);
const GATEWAY_URL = process.env.ARIA_CORE_WS || "ws://127.0.0.1:18789";
const GATEWAY_TOKEN = process.env.ARIA_AUTH_TOKEN || "";
const RVS_HOST = process.env.RVS_HOST || "";
const RVS_PORT = process.env.RVS_PORT || "443";
const RVS_TLS = process.env.RVS_TLS || "true";
const RVS_TLS_FALLBACK = process.env.RVS_TLS_FALLBACK || "true";
const RVS_TOKEN = process.env.RVS_TOKEN || "";
const PROXY_URL = process.env.PROXY_URL || "http://proxy:3456";

// ── State ───────────────────────────────────────────────
const state = {
  gateway: { status: "disconnected", lastError: null, handshakeOk: false },
  rvs: { status: "disconnected", lastError: null },
  proxy: { status: "unknown", lastError: null },
};
const SESSION_KEY_FILE = "/data/active-session";
// /data Verzeichnis sicherstellen (Volume Mount)
try { fs.mkdirSync("/data", { recursive: true }); } catch (e) {
  console.error(`[startup] /data mkdir fehlgeschlagen: ${e.message}`);
}
// sessionFromFile zeigt an, ob der aktive Key aus der Datei kam.
// Wenn true, darf resolveActiveSession NICHT mehr auto-picken (Wahl respektieren).
let sessionFromFile = false;
let activeSessionKey = (() => {
  try {
    const saved = fs.readFileSync(SESSION_KEY_FILE, "utf-8").trim();
    if (saved) {
      console.log(`[startup] Gespeicherte Session geladen: '${saved}'`);
      sessionFromFile = true;
      return saved;
    }
  } catch (e) {
    console.error(`[startup] SESSION_KEY_FILE read: ${e.code || e.message}`);
  }
  console.log("[startup] Keine gespeicherte Session — Fallback 'main'");
  return "main";
})();

// ── Runtime-Config: /shared/config/runtime.json ─────────────
// ENV-Werte sind Defaults; Werte aus runtime.json haben Vorrang.
// Bridge und ggf. andere Komponenten lesen dieselbe Datei.
const RUNTIME_CONFIG_FILE = "/shared/config/runtime.json";
const RUNTIME_CONFIG_FIELDS = [
  "RVS_HOST", "RVS_PORT", "RVS_TLS", "RVS_TOKEN",
  "ARIA_AUTH_TOKEN", "WHISPER_MODEL", "WHISPER_LANGUAGE",
];
function readRuntimeConfig() {
  const envDefaults = {
    RVS_HOST, RVS_PORT, RVS_TLS, RVS_TOKEN,
    ARIA_AUTH_TOKEN: process.env.ARIA_AUTH_TOKEN || "",
    WHISPER_MODEL: process.env.WHISPER_MODEL || "medium",
    WHISPER_LANGUAGE: process.env.WHISPER_LANGUAGE || "de",
  };
  try {
    const raw = fs.readFileSync(RUNTIME_CONFIG_FILE, "utf-8");
    const parsed = JSON.parse(raw);
    return { ...envDefaults, ...parsed };
  } catch {
    return envDefaults;
  }
}
function writeRuntimeConfig(patch) {
  let current = {};
  try { current = JSON.parse(fs.readFileSync(RUNTIME_CONFIG_FILE, "utf-8")); } catch {}
  for (const key of Object.keys(patch)) {
    if (RUNTIME_CONFIG_FIELDS.includes(key)) current[key] = patch[key];
  }
  fs.mkdirSync("/shared/config", { recursive: true });
  const tmp = RUNTIME_CONFIG_FILE + ".tmp";
  fs.writeFileSync(tmp, JSON.stringify(current, null, 2));
  fs.renameSync(tmp, RUNTIME_CONFIG_FILE);
}

// Atomic write: temp-file + rename, laute Logs bei Fehler.
function persistActiveSession(key) {
  try {
    const tmp = SESSION_KEY_FILE + ".tmp";
    fs.writeFileSync(tmp, key);
    fs.renameSync(tmp, SESSION_KEY_FILE);
    sessionFromFile = true;
    console.log(`[session] Aktive Session persistiert: '${key}'`);
    return true;
  } catch (e) {
    console.error(`[session] FEHLER beim Persistieren von '${key}': ${e.message}`);
    return false;
  }
}
const logs = [];
let gatewayWs = null;
let rvsWs = null;
let reqIdCounter = 0;
const browserClients = new Set();

// ── Pipeline Tracking ──────────────────────────────────
let pipelineActive = false;
let pipelineStartTime = 0;

// Nach chat:final kommen oft noch Trailing Agent-Events. Waehrend dieses
// Fensters unterdruecken wir agent_activity-Broadcasts, damit der
// Thinking-Indicator nicht wieder anspringt.
let lastChatFinalAt = 0;
const SETTLED_WINDOW_MS = 3000;

function plog(message, level) {
  const elapsed = pipelineActive ? `+${Date.now() - pipelineStartTime}ms` : "";
  const entry = { ts: new Date().toISOString(), level: level || "info", source: "pipeline", message: `${elapsed ? `[${elapsed}] ` : ""}${message}` };
  logs.push(entry);
  if (logs.length > 500) logs.shift();
  console.log(`[PIPELINE] ${entry.message}`);
  broadcast({ type: "log", entry });
}

let pipelineTimeout = null;

function pipelineStart(method, text) {
  // Falls noch eine Pipeline laeuft, beenden
  if (pipelineActive) pipelineEnd(false, "Abgebrochen (neue Nachricht)");
  pipelineActive = true;
  pipelineStartTime = Date.now();
  if (pipelineTimeout) clearTimeout(pipelineTimeout);
  pipelineTimeout = setTimeout(() => {
    if (pipelineActive) pipelineEnd(false, "Timeout — keine Antwort nach 10min");
  }, 600000);
  plog(`━━━ Pipeline Start: ${method} ━━━`);
  plog(`Nachricht: "${text}"`);
}

function pipelineEnd(ok, detail) {
  if (!pipelineActive) return;
  if (pipelineTimeout) { clearTimeout(pipelineTimeout); pipelineTimeout = null; }
  const elapsed = Date.now() - pipelineStartTime;
  if (ok) {
    plog(`>>> Fertig (${elapsed}ms): ${detail}`);
  } else {
    plog(`>>> FEHLER (${elapsed}ms): ${detail}`, "error");
  }
  plog(`━━━ Pipeline Ende ━━━`);
  pipelineActive = false;
  // Thinking-Indikator IMMER zuruecksetzen — auch bei Timeout/Fehler/Abbruch
  broadcast({ type: "agent_activity", activity: "idle" });
  pendingMessageTime = 0;
}

// ── Auto-Restart bei Netzwerk-Namespace-Verlust ──────
// Bei network_mode: "service:aria" verliert dieser Container
// den Netzwerkzugriff wenn aria-core neustartet.
// Nach MAX_GATEWAY_FAILURES aufeinanderfolgenden Fehlern → process.exit
// Docker restart: unless-stopped startet uns mit neuem Namespace neu.
const MAX_GATEWAY_FAILURES = 6; // 6 × 5s = 30s
let gatewayFailCount = 0;

function checkGatewayHealth() {
  if (state.gateway.status === "connected") {
    gatewayFailCount = 0;
    return;
  }
  gatewayFailCount++;
  if (gatewayFailCount >= MAX_GATEWAY_FAILURES) {
    log("error", "server", `Gateway ${MAX_GATEWAY_FAILURES}x nicht erreichbar — Neustart (Netzwerk-Namespace veraltet?)`);
    // Kurze Verzoegerung damit die Log-Nachricht noch gesendet wird
    setTimeout(() => process.exit(1), 500);
  }
}

function nextReqId() {
  return `diag-${++reqIdCounter}`;
}

function log(level, source, message) {
  const entry = { ts: new Date().toISOString(), level, source, message };
  logs.push(entry);
  if (logs.length > 500) logs.shift();
  console.log(`[${entry.ts}] [${level}] [${source}] ${message}`);
  // An alle Browser-Clients senden
  broadcast({ type: "log", entry });
}

function broadcast(msg) {
  const data = JSON.stringify(msg);
  for (const client of browserClients) {
    if (client.readyState === WebSocket.OPEN) {
      client.send(data);
    }
  }
}

function broadcastState() {
  broadcast({ type: "state", state });
}

// ── OpenClaw Gateway Verbindung ─────────────────────────

async function connectGateway() {
  if (gatewayWs) {
    try { gatewayWs.close(); } catch (_) {}
    gatewayWs = null;
  }

  state.gateway.status = "connecting";
  state.gateway.handshakeOk = false;
  broadcastState();
  log("info", "gateway", `Verbinde: ${GATEWAY_URL}`);

  try {
    const ws = new WebSocket(GATEWAY_URL);

    await new Promise((resolve, reject) => {
      const timeout = setTimeout(() => {
        ws.close();
        reject(new Error("Verbindungs-Timeout (10s)"));
      }, 10000);

      ws.on("open", () => {
        clearTimeout(timeout);
        resolve();
      });

      ws.on("error", (err) => {
        clearTimeout(timeout);
        reject(err);
      });
    });

    log("info", "gateway", "TCP-Verbindung hergestellt — warte auf Challenge");

    // Schritt 1: Auf Challenge warten
    const challengeRaw = await waitForMessage(ws, 10000);
    const challenge = JSON.parse(challengeRaw);

    if (challenge.type !== "event" || challenge.event !== "connect.challenge") {
      throw new Error(`Unerwartete erste Nachricht: ${challengeRaw.slice(0, 200)}`);
    }

    const nonce = challenge.payload?.nonce || "";
    log("info", "gateway", `Challenge empfangen (nonce: ${nonce.slice(0, 8)}...)`);

    // Schritt 2: Connect Request senden
    const connectReq = {
      type: "req",
      id: nextReqId(),
      method: "connect",
      params: {
        minProtocol: 3,
        maxProtocol: 3,
        client: {
          id: "cli",
          version: "0.0.1",
          platform: "linux",
          mode: "cli",
        },
        role: "operator",
        scopes: ["operator.read", "operator.write"],
        caps: [],
        commands: [],
        permissions: {},
        auth: GATEWAY_TOKEN ? { token: GATEWAY_TOKEN } : {},
        locale: "de-DE",
        userAgent: "aria-diagnostic/0.0.1",
      },
    };

    ws.send(JSON.stringify(connectReq));
    log("info", "gateway", "Connect-Request gesendet");

    // Schritt 3: hello-ok warten
    const responseRaw = await waitForMessage(ws, 10000);
    const response = JSON.parse(responseRaw);

    if (response.type === "res" && response.ok) {
      log("info", "gateway", "Handshake erfolgreich — hello-ok!");
      state.gateway.status = "connected";
      state.gateway.handshakeOk = true;
      state.gateway.lastError = null;
      gatewayFailCount = 0;
    } else {
      const error = typeof response.error === "string"
        ? response.error
        : JSON.stringify(response.error || response).slice(0, 300);
      throw new Error(`Handshake fehlgeschlagen: ${error}`);
    }

    gatewayWs = ws;
    broadcastState();

    // Nach (Re-)Connect: letzte aktive Session aus OpenClaw wiederherstellen
    resolveActiveSession().catch(err => {
      log("warn", "server", `Session-Aufloesung fehlgeschlagen: ${err.message}`);
    });

    // Nachrichten-Loop — RAW logging fuer Debugging
    ws.on("message", (raw) => {
      try {
        const rawStr = raw.toString();
        log("debug", "gateway", `RAW <<< ${rawStr.slice(0, 300)}`);
        const msg = JSON.parse(rawStr);
        handleGatewayMessage(msg);
      } catch (err) {
        log("error", "gateway", `Parse-Fehler: ${err.message}`);
      }
    });

    ws.on("close", (code, reason) => {
      log("warn", "gateway", `Verbindung geschlossen (Code: ${code}, Reason: ${reason || "-"})`);
      state.gateway.status = "disconnected";
      state.gateway.handshakeOk = false;
      gatewayWs = null;
      broadcastState();
      // Stuck "ARIA denkt..." vermeiden, falls Gateway waehrend Pipeline abkackt
      if (pipelineActive) pipelineEnd(false, `Gateway-Verbindung verloren (${code})`);
      else broadcast({ type: "agent_activity", activity: "idle" });
      checkGatewayHealth();
      setTimeout(connectGateway, 5000);
    });

    ws.on("error", (err) => {
      log("error", "gateway", `WebSocket-Fehler: ${err.message}`);
      state.gateway.lastError = err.message;
      broadcastState();
    });

  } catch (err) {
    log("error", "gateway", `Fehler: ${err.message}`);
    state.gateway.status = "error";
    state.gateway.lastError = err.message;
    state.gateway.handshakeOk = false;
    gatewayWs = null;
    broadcastState();
    checkGatewayHealth();
    // Retry nach 5s
    setTimeout(connectGateway, 5000);
  }
}

// Extrahiert Text aus OpenClaw chat-Event message.content Array
function extractChatText(payload) {
  try {
    const content = payload.message?.content;
    if (Array.isArray(content)) {
      return content
        .filter(c => c.type === "text")
        .map(c => c.text || "")
        .join("");
    }
    if (typeof payload.message === "string") return payload.message;
    return payload.text || "";
  } catch { return ""; }
}

// Deduplizierung: nur ein chat_final pro runId broadcasten
const seenFinalRuns = new Set();

function handleGatewayMessage(msg) {
  if (msg.type === "res") {
    const status = msg.ok ? "OK" : `FEHLER: ${JSON.stringify(msg.error).slice(0, 100)}`;
    log("info", "gateway", `Response [${msg.id}]: ${status}`);
    if (pipelineActive) {
      if (msg.ok) plog(`Gateway ACK [${msg.id}] — Nachricht angenommen`);
      else plog(`Gateway NACK [${msg.id}] — ${JSON.stringify(msg.error).slice(0, 100)}`, "error");
    }
    broadcast({ type: "response", msg });
    return;
  }

  if (msg.type === "event") {
    const event = msg.event || "?";
    const payload = msg.payload || {};

    // ── agent Events: Streaming-Deltas vom LLM ──
    if (event === "agent") {
      const data = payload.data || {};
      const delta = data.delta || "";
      const stream = payload.stream || "";

      if (delta && stream === "assistant") {
        broadcast({ type: "chat_delta", delta, payload });
      }

      // Nach chat:final trickeln noch Aufraeum-Events rein — unterdruecken,
      // damit der Thinking-Indicator nicht wieder anspringt.
      const settled = lastChatFinalAt && (Date.now() - lastChatFinalAt) < SETTLED_WINDOW_MS;

      // Tool-Nutzung erkennen und broadcasten
      if (stream === "tool_use" || data.type === "tool_use") {
        const toolName = data.name || data.tool || payload.tool || "";
        if (toolName && !settled) {
          broadcast({ type: "agent_activity", activity: "tool", tool: toolName, data });
          log("info", "gateway", `Tool: ${toolName}`);
        }
      }

      if (!settled) {
        broadcast({ type: "agent_activity", activity: stream || "thinking" });
      }
      updateAgentActivity();
      return;
    }

    // ── chat Events: Snapshots mit state=delta|final ──
    if (event === "chat") {
      const state = payload.state || "";
      const text = extractChatText(payload);

      if (state === "final") {
        const runId = payload.runId || "";
        if (runId && seenFinalRuns.has(runId)) return; // Duplikat
        if (runId) { seenFinalRuns.add(runId); setTimeout(() => seenFinalRuns.delete(runId), 60000); }

        // NO_REPLY → ARIA signalisiert "nicht antworten", Pipeline beenden aber nichts zeigen
        const trimmed = (text || "").trim().replace(/^["'`*.\s]+|["'`*.\s]+$/g, "").toUpperCase();
        if (trimmed === "NO_REPLY" || trimmed.startsWith("NO_REPLY")) {
          log("info", "gateway", "NO_REPLY empfangen — still verworfen");
          lastChatFinalAt = Date.now();
          if (pipelineActive) pipelineEnd(true, "NO_REPLY (stumm)");
          broadcast({ type: "agent_activity", activity: "idle" });
          pendingMessageTime = 0;
          updateAgentActivity();
          return;
        }

        log("info", "gateway", `ANTWORT: "${text.slice(0, 200)}"`);
        lastChatFinalAt = Date.now();
        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();
        // Antwort in Backup-Log schreiben
        try {
          const entry = JSON.stringify({ ts: Date.now(), role: "assistant", text: text.slice(0, 2000), session: activeSessionKey }) + "\n";
          fs.appendFileSync("/shared/config/chat_backup.jsonl", entry);
        } catch {}
        return;
      }

      if (state === "delta") {
        // Periodischer Snapshot — nicht einzeln loggen
        return;
      }

      if (state === "error") {
        const error = payload.error || text || "Unbekannt";
        log("error", "gateway", `Chat-Fehler: ${error}`);
        if (pipelineActive) pipelineEnd(false, error);
        else broadcast({ type: "agent_activity", activity: "idle" });
        broadcast({ type: "chat_error", error, payload });
        return;
      }

      log("debug", "gateway", `chat state=${state}`);
      return;
    }

    // ── Legacy event names (chat:delta, chat:final, chat:error) ──
    if (event === "chat:delta") {
      const delta = payload.delta || payload.text || "";
      if (delta) broadcast({ type: "chat_delta", delta, payload });
      return;
    }
    if (event === "chat:final") {
      const runId = payload.runId || "";
      if (runId && seenFinalRuns.has(runId)) return; // Duplikat
      if (runId) { seenFinalRuns.add(runId); setTimeout(() => seenFinalRuns.delete(runId), 60000); }
      const text = extractChatText(payload) || payload.text || "";
      log("info", "gateway", `ANTWORT: "${text.slice(0, 200)}"`);
      lastChatFinalAt = Date.now();
      if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`);
      else broadcast({ type: "agent_activity", activity: "idle" });
      broadcast({ type: "chat_final", text, payload });
      return;
    }
    if (event === "chat:error") {
      const error = payload.error || payload.message || "Unbekannt";
      log("error", "gateway", `Chat-Fehler: ${error}`);
      if (pipelineActive) pipelineEnd(false, error);
      else broadcast({ type: "agent_activity", activity: "idle" });
      broadcast({ type: "chat_error", error, payload });
      return;
    }

    // ── Andere Events (tick, health, presence) ──
    if (event === "tick" || event === "health") return; // Noise
    log("debug", "gateway", `Event: ${event}`);
  }
}

function sendToGateway(text, isPipeline) {
  if (!gatewayWs || gatewayWs.readyState !== WebSocket.OPEN) {
    log("error", "gateway", "Nicht verbunden — kann nicht senden");
    if (isPipeline) pipelineEnd(false, "Gateway nicht verbunden");
    return false;
  }

  const reqId = nextReqId();
  const msg = {
    type: "req",
    id: reqId,
    method: "chat.send",
    params: {
      sessionKey: activeSessionKey,
      message: text,
      idempotencyKey: crypto.randomUUID(),
    },
  };

  const payload = JSON.stringify(msg);
  log("debug", "gateway", `RAW >>> ${payload}`);
  gatewayWs.send(payload);
  pendingMessageTime = Date.now(); // Watchdog: Nachricht gesendet
  // Nachricht sofort in Backup-Log schreiben (OpenClaw speichert erst nach Run-Ende)
  try {
    fs.mkdirSync("/shared/config", { recursive: true });
    const entry = JSON.stringify({ ts: Date.now(), role: "user", text, session: activeSessionKey }) + "\n";
    fs.appendFileSync("/shared/config/chat_backup.jsonl", entry);
  } catch {}
  log("info", "gateway", `chat.send [${reqId}]: "${text}"`);
  if (isPipeline) plog(`chat.send [${reqId}] an Gateway gesendet — warte auf ACK...`);

  // Gateway-Nachrichten NICHT an RVS senden (sonst doppelter ARIA-Request via Bridge)
  return true;
}

// ── RVS Verbindung (optional) ───────────────────────────

function connectRVS(forcePlain) {
  if (!RVS_HOST || !RVS_TOKEN) {
    log("info", "rvs", "Nicht konfiguriert — ueberspringe");
    state.rvs.status = "not_configured";
    broadcastState();
    return;
  }

  // Alte Verbindung sauber schliessen
  if (rvsWs) {
    try { rvsWs.removeAllListeners(); rvsWs.close(); } catch (_) {}
    rvsWs = null;
  }

  // TLS-Logik: wss zuerst, bei Fehler Fallback auf ws
  const useTls = RVS_TLS === "true" && !forcePlain;
  const proto = useTls ? "wss" : "ws";
  const url = `${proto}://${RVS_HOST}:${RVS_PORT}?token=${RVS_TOKEN}`;

  state.rvs.status = "connecting";
  broadcastState();
  log("info", "rvs", `Verbinde: ${proto}://${RVS_HOST}:${RVS_PORT}`);

  let ws;
  try {
    ws = new WebSocket(url);
  } catch (err) {
    log("error", "rvs", `WebSocket erstellen fehlgeschlagen: ${err.message}`);
    if (useTls && RVS_TLS_FALLBACK === "true") {
      connectRVS(true);
    }
    return;
  }

  let fallbackTriggered = false;

  ws.on("open", () => {
    log("info", "rvs", `Verbunden (${proto})`);
    state.rvs.status = "connected";
    state.rvs.lastError = null;
    rvsWs = ws;
    broadcastState();

    // Keepalive: alle 25s ein Ping senden damit die Verbindung nicht stirbt
    const keepalive = setInterval(() => {
      if (ws.readyState === WebSocket.OPEN) {
        try { ws.ping(); } catch (_) {}
      } else {
        clearInterval(keepalive);
      }
    }, 25000);
    ws._keepalive = keepalive;
  });

  ws.on("message", (raw) => {
    try {
      const msg = JSON.parse(raw.toString());
      if (msg.type === "chat" && msg.payload) {
        const sender = msg.payload.sender || "?";
        // Eigene Nachrichten ignorieren (Echo)
        if (sender === "diagnostic") return;
        log("info", "rvs", `Chat von ${sender}: "${(msg.payload.text || "").slice(0, 100)}"`);
        if (pipelineActive) {
          pipelineEnd(true, `Antwort via RVS von ${sender}: "${(msg.payload.text || "").slice(0, 120)}"`);
        }
        broadcast({ type: "rvs_chat", msg });
      } else if (msg.type === "file_saved" && msg.payload) {
        // Bild/Datei-Upload von der App — im Chat anzeigen
        const name = msg.payload.name || "?";
        const serverPath = msg.payload.serverPath || "";
        const mimeType = msg.payload.mimeType || "";
        log("info", "rvs", `Datei empfangen: ${name} (${serverPath})`);
        // Als User-Nachricht mit Pfad broadcasten (Diagnostic zeigt Bilder inline)
        broadcast({ type: "rvs_chat", msg: {
          type: "chat",
          payload: { text: `Anhang: ${name}\n${serverPath}`, sender: "user" }
        }});
      } else if (msg.type === "heartbeat") {
        // ignorieren
      } else if (msg.type === "mode") {
        // Mode-Broadcast von der Bridge → an Browser-Clients weiterreichen
        log("info", "rvs", `Mode-Broadcast: ${msg.payload?.mode} (${msg.payload?.name})`);
        broadcast({ type: "mode", payload: msg.payload });
      } else if (msg.type === "voice_ready") {
        // XTTS-Bridge meldet Stimme fertig geladen → an Browser durchreichen
        const v = msg.payload?.voice || "";
        const err = msg.payload?.error;
        const ms = msg.payload?.loadMs;
        if (err) {
          log("warn", "rvs", `Voice-Ready Fehler fuer "${v}": ${err}`);
        } else {
          log("info", "rvs", `Voice "${v || "default"}" geladen${ms ? ` in ${(ms/1000).toFixed(1)}s` : ""}`);
        }
        broadcast({ type: "voice_ready", payload: msg.payload });
      } else if (msg.type === "service_status") {
        // Gamebox-Bridges (f5tts/whisper) melden ihren Lade-Status —
        // an Browser durchreichen fuer das Banner unten rechts
        const svc = msg.payload?.service || "?";
        const state = msg.payload?.state || "?";
        const model = msg.payload?.model || "";
        const sec = msg.payload?.loadSeconds;
        const err = msg.payload?.error;
        if (err) {
          log("warn", "rvs", `service_status ${svc}: ${err}`);
        } else if (state === "ready" && sec) {
          log("info", "rvs", `service_status ${svc} ready (${model}, ${sec.toFixed(1)}s)`);
        } else {
          log("info", "rvs", `service_status ${svc} ${state}${model ? ` (${model})` : ""}`);
        }
        broadcast({ type: "service_status", payload: msg.payload });
      } else if (msg.type === "audio_pcm" && msg.payload && _previewPending.size > 0) {
        // PCM-Chunks einer laufenden Voice-Preview — sammeln + WAV bauen
        _handlePreviewChunk(msg.payload);
      } else {
        log("debug", "rvs", `Nachricht: ${JSON.stringify(msg).slice(0, 150)}`);
      }
    } catch {}
  });

  ws.on("close", () => {
    log("warn", "rvs", "Verbindung geschlossen");
    if (ws._keepalive) clearInterval(ws._keepalive);
    state.rvs.status = "disconnected";
    if (rvsWs === ws) rvsWs = null;
    broadcastState();
    if (!fallbackTriggered) {
      setTimeout(() => connectRVS(), 5000);
    }
  });

  ws.on("error", (err) => {
    log("error", "rvs", `Fehler (${proto}): ${err.message}`);
    state.rvs.lastError = err.message;
    broadcastState();

    // TLS Fallback
    if (useTls && RVS_TLS_FALLBACK === "true" && !fallbackTriggered) {
      fallbackTriggered = true;
      log("warn", "rvs", "TLS fehlgeschlagen — Fallback auf ws://");
      try { ws.removeAllListeners(); ws.close(); } catch (_) {}
      if (rvsWs === ws) rvsWs = null;
      connectRVS(true);
    }
  });
}

function sendToRVS_withResponse(sendType, sendPayload, expectType, clientWs) {
  if (!RVS_HOST || !RVS_TOKEN) return;
  const proto = RVS_TLS === "true" ? "wss" : "ws";
  const url = `${proto}://${RVS_HOST}:${RVS_PORT}?token=${RVS_TOKEN}`;
  const freshWs = new WebSocket(url);
  const timeout = setTimeout(() => {
    try { freshWs.close(); } catch (_) {}
    clientWs.send(JSON.stringify({ type: expectType, payload: { voices: [], error: "Timeout" }, timestamp: Date.now() }));
  }, 15000);
  freshWs.on("open", () => {
    freshWs.send(JSON.stringify({ type: sendType, payload: sendPayload, timestamp: Date.now() }));
  });
  freshWs.on("message", (raw) => {
    try {
      const resp = JSON.parse(raw.toString());
      if (resp.type === expectType) {
        clearTimeout(timeout);
        clientWs.send(JSON.stringify(resp));
        setTimeout(() => { try { freshWs.close(); } catch (_) {} }, 1000);
      }
    } catch {}
  });
  freshWs.on("error", () => {});
}

function sendToRVS_raw(msgObj) {
  if (!RVS_HOST || !RVS_TOKEN) return;
  const payload = JSON.stringify(msgObj);
  // Persistente Connection bevorzugen — die ist garantiert connected
  // und wird vom RVS direkt an alle anderen Clients (App, Bridge) broadcastet.
  // Frische Connections hatten Race-Probleme: die WS war nach dem send manchmal
  // schon zu, bevor RVS broadcasten konnte → App-Nachrichten verloren.
  if (rvsWs && rvsWs.readyState === WebSocket.OPEN) {
    try {
      rvsWs.send(payload);
      return;
    } catch (err) {
      log("warn", "rvs", `persistente Verbindung send failed (${err.message}) — Fallback frische WS`);
    }
  }
  const proto = RVS_TLS === "true" ? "wss" : "ws";
  const url = `${proto}://${RVS_HOST}:${RVS_PORT}?token=${RVS_TOKEN}`;
  const freshWs = new WebSocket(url);
  freshWs.on("open", () => {
    freshWs.send(payload);
    setTimeout(() => { try { freshWs.close(); } catch (_) {} }, 5000);
  });
  freshWs.on("error", () => {});
}

function sendToRVS(text, isPipeline) {
  // Ueber Gateway senden (zuverlaessig) UND an RVS fuer App-Sichtbarkeit
  // Die Bridge empfaengt RVS-Nachrichten von der App zuverlaessig,
  // aber die Diagnostic→RVS→Bridge Route hat Zombie-Probleme.
  // Deshalb: Gateway fuer ARIA, RVS nur fuer App-Anzeige.

  // 1. An Gateway senden (damit ARIA antwortet)
  const gatewayOk = sendToGateway(text, isPipeline);

  // 2. An RVS senden (damit die App die Nachricht sieht)
  sendToRVS_raw({
    type: "chat",
    payload: { text, sender: "diagnostic" },
    timestamp: Date.now(),
  });

  return gatewayOk;
}

// ── Claude Proxy Test ────────────────────────────────────

async function testProxy(prompt) {
  state.proxy.status = "testing";
  state.proxy.lastError = null;
  broadcastState();
  log("info", "proxy", `Teste Proxy: ${PROXY_URL}`);

  try {
    // Schritt 1: Erreichbarkeit pruefen
    const healthUrl = `${PROXY_URL}/v1/models`;
    log("info", "proxy", `Rufe ab: ${healthUrl}`);

    const modelsRes = await fetch(healthUrl, {
      headers: { "Authorization": "Bearer not-needed" },
      signal: AbortSignal.timeout(30000),
    });

    if (!modelsRes.ok) {
      throw new Error(`Models-Endpoint: HTTP ${modelsRes.status} ${modelsRes.statusText}`);
    }

    const modelsData = await modelsRes.json();
    const models = (modelsData.data || []).map(m => m.id).filter(Boolean);
    log("info", "proxy", `Proxy erreichbar — ${models.length} Model(s) verfuegbar`);

    // Modellnamen loggen + OpenClaw-Config Hinweis
    if (models.length > 0) {
      log("info", "proxy", `Modelle: ${models.join(", ")}`);
      log("info", "proxy", `Fuer docker-compose.yml (DEFAULT_MODEL): ${models.map(m => m.replace("openai/", "")).join(" | ")}`);
    }

    // Schritt 1b: Auth-Dateien im Proxy-Container pruefen
    try {
      const authInfo = await dockerExec("aria-proxy", "echo '--- /root/.config/claude/ ---' && ls -la /root/.config/claude/ 2>&1 && echo '--- /root/.claude/ ---' && ls -la /root/.claude/ 2>&1 && echo '--- Credential-Dateien ---' && find /root/.config/claude /root/.claude -name '*.json' -o -name '*credential*' -o -name '*auth*' -o -name '*token*' 2>/dev/null | head -20");
      log("info", "proxy", `Auth-Dateien im Container:\n${authInfo}`);
      broadcast({ type: "proxy_auth", info: authInfo });
    } catch (authErr) {
      log("warn", "proxy", `Auth-Check fehlgeschlagen: ${authErr.message}`);
    }

    // Schritt 2: Chat Completion testen (kurzer Prompt)
    const testPrompt = prompt || "Antworte in einem Satz: Wer bist du und funktionierst du?";
    log("info", "proxy", `Sende Test-Prompt: "${testPrompt}"`);

    const chatRes = await fetch(`${PROXY_URL}/v1/chat/completions`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": "Bearer not-needed",
      },
      body: JSON.stringify({
        model: "claude-sonnet-4-6",
        messages: [{ role: "user", content: testPrompt }],
        max_tokens: 200,
      }),
      signal: AbortSignal.timeout(120000), // 2min — Cold Start braucht Zeit
    });

    if (!chatRes.ok) {
      const errBody = await chatRes.text().catch(() => "");
      throw new Error(`Chat-Completion: HTTP ${chatRes.status} — ${errBody.slice(0, 300)}`);
    }

    const chatData = await chatRes.json();
    const reply = chatData.choices?.[0]?.message?.content || "(leer)";
    log("info", "proxy", `Antwort: "${reply.slice(0, 200)}"`);

    state.proxy.status = "connected";
    state.proxy.lastError = null;
    state.proxy.models = models;
    broadcastState();
    broadcast({ type: "proxy_result", ok: true, reply, models });

  } catch (err) {
    log("error", "proxy", `Fehler: ${err.message}`);
    state.proxy.status = "error";
    state.proxy.lastError = err.message;
    broadcastState();
    broadcast({ type: "proxy_result", ok: false, error: err.message });
  }
}

// ── Claude Login im Proxy-Container ─────────────────────



// ── Interaktives Terminal (xterm.js ↔ Docker Exec) ──────

const net = require("net");

function attachTerminal(clientWs, containerName, cmd) {
  const createBody = JSON.stringify({
    AttachStdin: true, AttachStdout: true, AttachStderr: true, Tty: true,
    Cmd: Array.isArray(cmd) ? cmd : ["sh", "-c", cmd],
  });

  const createReq = http.request({
    socketPath: "/var/run/docker.sock",
    path: `/containers/${containerName}/exec`,
    method: "POST",
    headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(createBody) },
  }, (res) => {
    let data = "";
    res.on("data", (c) => data += c);
    res.on("end", () => {
      if (res.statusCode !== 201) {
        clientWs.send(JSON.stringify({ type: "term_error", error: `Exec create failed: HTTP ${res.statusCode}` }));
        return;
      }
      const execId = JSON.parse(data).Id;

      // Exec starten — raw TCP socket fuer bidirektionalen Stream
      // Docker Exec mit Tty macht ein HTTP Upgrade auf raw stream
      const startBody = JSON.stringify({ Detach: false, Tty: true });

      const sock = net.connect({ path: "/var/run/docker.sock" }, () => {
        // Raw HTTP Request senden (Upgrade-artig)
        const req = `POST /exec/${execId}/start HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nConnection: Upgrade\r\nUpgrade: tcp\r\nContent-Length: ${Buffer.byteLength(startBody)}\r\n\r\n${startBody}`;
        sock.write(req);
      });

      let headersParsed = false;
      let headerBuf = "";

      sock.on("data", (chunk) => {
        if (!headersParsed) {
          // HTTP Response Header parsen
          headerBuf += chunk.toString("utf-8");
          const headerEnd = headerBuf.indexOf("\r\n\r\n");
          if (headerEnd === -1) return; // Noch nicht komplett

          const headers = headerBuf.slice(0, headerEnd);
          const remaining = chunk.slice(chunk.length - (headerBuf.length - headerEnd - 4));

          headersParsed = true;

          if (!headers.includes("200") && !headers.includes("101")) {
            clientWs.send(JSON.stringify({ type: "term_error", error: `Exec start failed: ${headers.split("\r\n")[0]}` }));
            sock.end();
            return;
          }

          log("info", "proxy", "Terminal-Session gestartet");
          clientWs.send(JSON.stringify({ type: "term_ready" }));
          clientWs._termSock = sock;

          // Verbleibende Daten nach dem Header
          if (remaining.length > 0) {
            clientWs.send(JSON.stringify({ type: "term_data", data: remaining.toString("base64") }));
          }
          return;
        }

        // Tty=true: Rohdaten direkt durchreichen
        if (clientWs.readyState === WebSocket.OPEN) {
          clientWs.send(JSON.stringify({ type: "term_data", data: chunk.toString("base64") }));
        }
      });

      sock.on("end", () => {
        log("info", "proxy", "Terminal-Session beendet");
        if (clientWs.readyState === WebSocket.OPEN) {
          clientWs.send(JSON.stringify({ type: "term_exit" }));
        }
        clientWs._termSock = null;
        setTimeout(() => checkProxyAuth(), 1000);
      });

      sock.on("error", (err) => {
        log("error", "proxy", `Terminal-Socket-Fehler: ${err.message}`);
        if (clientWs.readyState === WebSocket.OPEN) {
          clientWs.send(JSON.stringify({ type: "term_error", error: err.message }));
        }
      });

      // Wenn Browser-Client disconnected, Socket schliessen
      clientWs.on("close", () => {
        if (sock && !sock.destroyed) sock.end();
      });
    });
  });
  createReq.on("error", (err) => {
    clientWs.send(JSON.stringify({ type: "term_error", error: err.message }));
  });
  createReq.write(createBody);
  createReq.end();
}

function handleTermInput(clientWs, data) {
  if (clientWs._termSock && !clientWs._termSock.destroyed) {
    clientWs._termSock.write(Buffer.from(data, "base64"));
  }
}

// Credentials manuell einfuegen (von einem Rechner wo Claude eingeloggt ist)
async function writeProxyCredentials(credentialsJson) {
  try {
    // Validieren
    const parsed = JSON.parse(credentialsJson);
    if (!parsed.claudeAiOauth && !parsed.oauth) {
      throw new Error("Ungueltig: Kein OAuth-Objekt gefunden. Erwartet 'claudeAiOauth' oder 'oauth' Key.");
    }

    log("info", "proxy", "Schreibe Credentials in Proxy-Container...");

    // Escaped fuer Shell — Einfache Anfuehrungszeichen im JSON escapen
    const escaped = credentialsJson.replace(/'/g, "'\\''");
    // In beide moegliche Speicherorte schreiben
    await dockerExec("aria-proxy", `mkdir -p /root/.config/claude && echo '${escaped}' > /root/.config/claude/.credentials.json && mkdir -p /root/.claude && echo '${escaped}' > /root/.claude/credentials.json`);

    log("info", "proxy", "Credentials geschrieben!");
    broadcast({ type: "login_status", status: "done" });
    broadcast({ type: "login_output", text: "Credentials erfolgreich geschrieben! Proxy muss neu gestartet werden." });

    // Auth pruefen
    setTimeout(() => checkProxyAuth(), 500);
  } catch (err) {
    log("error", "proxy", `Credentials schreiben fehlgeschlagen: ${err.message}`);
    broadcast({ type: "login_status", status: "error", error: err.message });
  }
}

async function checkProxyAuth() {
  try {
    log("info", "proxy", "Pruefe Auth-Dateien im Proxy-Container...");
    // Breit suchen: Claude Code speichert Credentials je nach Version an verschiedenen Orten
    const authInfo = await dockerExec("aria-proxy", `
      echo '=== /root/.config/claude/ ===' &&
      ls -la /root/.config/claude/ 2>&1 &&
      echo '' &&
      echo '=== /root/.claude/ ===' &&
      ls -la /root/.claude/ 2>&1 &&
      echo '' &&
      echo '=== /root/.claude/auth/ ===' &&
      ls -la /root/.claude/auth/ 2>&1 &&
      echo '' &&
      echo '=== Credentials-Dateien (rekursiv) ===' &&
      find /root/.config/claude /root/.claude -name '*.json' -o -name '*credential*' -o -name '*auth*' -o -name '*token*' -o -name '*oauth*' -o -name '*session*' 2>/dev/null | head -20 &&
      echo '' &&
      echo '=== .credentials.json ===' &&
      cat /root/.config/claude/.credentials.json 2>/dev/null || echo '(nicht in .config/claude/)' &&
      echo '' &&
      echo '=== /root/.claude/credentials.json ===' &&
      cat /root/.claude/credentials.json 2>/dev/null || echo '(nicht in .claude/)' &&
      echo '' &&
      echo '=== /root/.claude/auth/*.json ===' &&
      cat /root/.claude/auth/*.json 2>/dev/null || echo '(keine auth/*.json)'
    `.trim());
    log("info", "proxy", `Auth-Dateien:\n${authInfo}`);
    broadcast({ type: "proxy_auth", info: authInfo });
  } catch (err) {
    log("error", "proxy", `Auth-Check fehlgeschlagen: ${err.message}`);
    broadcast({ type: "proxy_auth", info: null, error: err.message });
  }
}

// ── OpenClaw Agent-Auth pruefen ──────────────────────────

async function checkCoreAuth() {
  try {
    log("info", "gateway", "Pruefe OpenClaw Agent-Konfiguration...");
    const info = await dockerExec("aria-core", `
      echo '=== Agent-Verzeichnis ===' &&
      ls -la /home/node/.openclaw/agents/main/agent/ 2>&1 &&
      echo '' &&
      echo '=== auth-profiles.json ===' &&
      cat /home/node/.openclaw/agents/main/agent/auth-profiles.json 2>/dev/null || echo '(nicht vorhanden)' &&
      echo '' &&
      echo '=== Umgebungsvariablen ===' &&
      echo "OPENAI_BASE_URL=$OPENAI_BASE_URL" &&
      echo "OPENAI_API_KEY=$(echo $OPENAI_API_KEY | head -c 15)..." &&
      echo "DEFAULT_MODEL=$DEFAULT_MODEL" &&
      echo '' &&
      echo '=== OpenClaw Version ===' &&
      openclaw --version 2>/dev/null || echo '(openclaw CLI nicht gefunden)' &&
      echo '' &&
      echo '=== Agents Liste ===' &&
      openclaw agents list 2>/dev/null || echo '(Befehl fehlgeschlagen)'
    `.trim());
    log("info", "gateway", `OpenClaw Config:\n${info}`);
    broadcast({ type: "core_auth", info });
  } catch (err) {
    log("error", "gateway", `Core-Auth-Check fehlgeschlagen: ${err.message}`);
    broadcast({ type: "core_auth", info: null, error: err.message });
  }
}

// ── Docker Container Logs ────────────────────────────────

const CONTAINER_MAP = {
  gateway: "aria-core",
  proxy: "aria-proxy",
  bridge: "aria-bridge",
};

function fetchDockerLogs(tab, tail) {
  const containerName = CONTAINER_MAP[tab];
  if (!containerName) return;

  const lines = parseInt(tail, 10) || 100;
  const dockerPath = `/containers/${containerName}/logs?stdout=true&stderr=true&tail=${lines}&timestamps=true`;

  return new Promise((resolve, reject) => {
    const req = http.request(
      { socketPath: "/var/run/docker.sock", path: dockerPath, method: "GET" },
      (res) => {
        const chunks = [];
        res.on("data", (chunk) => chunks.push(chunk));
        res.on("end", () => {
          // Docker log stream hat 8-byte Header pro Frame (stream type + size)
          const raw = Buffer.concat(chunks);
          const logLines = [];
          let offset = 0;
          while (offset < raw.length) {
            if (offset + 8 > raw.length) break;
            const size = raw.readUInt32BE(offset + 4);
            if (offset + 8 + size > raw.length) break;
            const line = raw.slice(offset + 8, offset + 8 + size).toString("utf-8").trimEnd();
            if (line) logLines.push(line);
            offset += 8 + size;
          }
          resolve(logLines);
        });
      }
    );
    req.on("error", (err) => reject(err));
    req.end();
  });
}

async function handleDockerLogs(ws, tab, tail) {
  const containerName = CONTAINER_MAP[tab];
  if (!containerName) {
    ws.send(JSON.stringify({ type: "docker_logs", tab, error: `Unbekannter Tab: ${tab}` }));
    return;
  }

  try {
    log("info", "server", `Lade Docker-Logs: ${containerName} (tail ${tail || 100})`);
    const lines = await fetchDockerLogs(tab, tail);
    ws.send(JSON.stringify({ type: "docker_logs", tab, container: containerName, lines }));
  } catch (err) {
    log("error", "server", `Docker-Logs Fehler (${containerName}): ${err.message}`);
    ws.send(JSON.stringify({ type: "docker_logs", tab, error: err.message }));
  }
}

// ── Docker Exec (Befehl in Container ausfuehren) ────────

function dockerExec(containerName, cmd) {
  return new Promise((resolve, reject) => {
    const createBody = JSON.stringify({
      AttachStdout: true, AttachStderr: true,
      Cmd: Array.isArray(cmd) ? cmd : ["sh", "-c", cmd],
    });

    const createReq = http.request({
      socketPath: "/var/run/docker.sock",
      path: `/containers/${containerName}/exec`,
      method: "POST",
      headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(createBody) },
    }, (res) => {
      let data = "";
      res.on("data", (c) => data += c);
      res.on("end", () => {
        if (res.statusCode !== 201) return reject(new Error(`Exec create: HTTP ${res.statusCode} — ${data.slice(0, 200)}`));
        const execId = JSON.parse(data).Id;

        // Exec starten
        const startBody = JSON.stringify({ Detach: false });
        const startReq = http.request({
          socketPath: "/var/run/docker.sock",
          path: `/exec/${execId}/start`,
          method: "POST",
          headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(startBody) },
        }, (sRes) => {
          const chunks = [];
          sRes.on("data", (c) => chunks.push(c));
          sRes.on("end", () => {
            // Docker multiplexed stream: 8-byte header pro Frame
            const raw = Buffer.concat(chunks);
            const lines = [];
            let offset = 0;
            while (offset < raw.length) {
              if (offset + 8 > raw.length) { lines.push(raw.slice(offset).toString("utf-8").trim()); break; }
              const size = raw.readUInt32BE(offset + 4);
              if (size === 0 || offset + 8 + size > raw.length) { lines.push(raw.slice(offset).toString("utf-8").trim()); break; }
              const line = raw.slice(offset + 8, offset + 8 + size).toString("utf-8").trimEnd();
              if (line) lines.push(line);
              offset += 8 + size;
            }
            resolve(lines.join("\n"));
          });
        });
        startReq.on("error", reject);
        startReq.write(startBody);
        startReq.end();
      });
    });
    createReq.on("error", reject);
    createReq.write(createBody);
    createReq.end();
  });
}

// ── Hilfsfunktionen ─────────────────────────────────────

function waitForMessage(ws, timeoutMs) {
  return new Promise((resolve, reject) => {
    const timeout = setTimeout(() => {
      reject(new Error(`Timeout (${timeoutMs}ms)`));
    }, timeoutMs);

    ws.once("message", (data) => {
      clearTimeout(timeout);
      resolve(data.toString());
    });
  });
}

// ── Watchdog: Stuck Run Erkennung ────────────────────────

let lastAgentActivity = Date.now();
let watchdogWarned = false;
let watchdogFixAttempted = false;
let pendingMessageTime = 0; // Wann wurde die letzte Nachricht gesendet

function updateAgentActivity() {
  lastAgentActivity = Date.now();
  watchdogWarned = false;
}

// ── Disk-Space Monitor ───────────────────────────────
// Prueft regelmaessig die Host-Disk (via gemountetem /shared) und
// broadcastet bei kritischen Schwellwerten ein disk_status Event.
let lastDiskStatus = null;
let currentDiskStatus = null; // Vollstaendig fuer neu verbundene Clients
function checkDiskSpace() {
  const { exec } = require("child_process");
  exec("df -B1 /shared", (err, stdout) => {
    if (err) return;
    const lines = stdout.trim().split("\n");
    if (lines.length < 2) return;
    const cols = lines[1].split(/\s+/);
    // Filesystem  Size  Used  Avail  Use%  MountedOn
    const total = parseInt(cols[1], 10);
    const used = parseInt(cols[2], 10);
    const avail = parseInt(cols[3], 10);
    if (!total) return;
    const pct = Math.round((used / total) * 100);
    let level = "ok";
    if (pct >= 95) level = "critical";
    else if (pct >= 85) level = "warn";
    else if (pct >= 70) level = "info";
    const status = {
      type: "disk_status",
      level,
      percent: pct,
      usedBytes: used,
      totalBytes: total,
      availBytes: avail,
    };
    currentDiskStatus = status;
    // Nur broadcasten wenn sich was geaendert hat (oder alle 60s Refresh)
    const key = `${level}-${pct}`;
    if (lastDiskStatus !== key) {
      lastDiskStatus = key;
      broadcast(status);
      if (level !== "ok") {
        log(level === "critical" ? "error" : "warn", "server",
          `Disk ${pct}% belegt (${(used/1024/1024/1024).toFixed(1)}GB von ${(total/1024/1024/1024).toFixed(1)}GB)`);
      }
    }
  });
}
// Beim Start + alle 30s
setTimeout(checkDiskSpace, 2000);
setInterval(checkDiskSpace, 30000);

// 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: doctor --fix
  if (waitingMs > 300000 && watchdogWarned && !watchdogFixAttempted) {
    watchdogFixAttempted = true;
    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 — warte auf Antwort..." });
    } catch (err) {
      log("error", "server", `Watchdog: doctor --fix fehlgeschlagen: ${err.message}`);
    }
  }

  // Nach 8min: Container neustarten
  if (waitingMs > 480000 && watchdogFixAttempted) {
    log("error", "server", "Watchdog: 8min ohne Antwort — starte aria-core + aria-proxy neu");
    broadcast({ type: "watchdog", status: "restarting", message: "Container-Restart: aria-core + aria-proxy" });
    try {
      const { execSync } = require("child_process");
      execSync("docker restart aria-core aria-proxy", { timeout: 60000 });
      log("info", "server", "Watchdog: Container neugestartet");
      broadcast({ type: "watchdog", status: "restarted", message: "Container neugestartet — warte auf Gateway-Reconnect..." });
      // Gateway wird sich automatisch neu verbinden
    } catch (err) {
      log("error", "server", `Watchdog: Container-Restart fehlgeschlagen: ${err.message}`);
      broadcast({ type: "watchdog", status: "error", message: `Restart fehlgeschlagen: ${err.message}` });
    }
    pendingMessageTime = 0;
    watchdogWarned = false;
    watchdogFixAttempted = false;
  }
}, 30000);

// ── HTTP Server + WebSocket fuer Browser ────────────────

const htmlPath = path.join(__dirname, "index.html");

const server = http.createServer((req, res) => {
  if (req.url === "/" || req.url === "/index.html") {
    res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
    res.end(fs.readFileSync(htmlPath, "utf-8"));
  } else if (req.url === "/api/state") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ state, logs: logs.slice(-100) }));
  } else if (req.url === "/api/session") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ sessionKey: activeSessionKey }));
  } else if (req.url === "/api/runtime-config" && req.method === "GET") {
    // Zentrale Runtime-Config (ENV + Override aus /shared/config/runtime.json)
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(readRuntimeConfig()));
  } else if (req.url === "/api/runtime-config" && req.method === "POST") {
    let body = "";
    req.on("data", chunk => { body += chunk; if (body.length > 32768) req.destroy(); });
    req.on("end", () => {
      try {
        const patch = JSON.parse(body);
        writeRuntimeConfig(patch);
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ ok: true, config: readRuntimeConfig() }));
        log("info", "server", `Runtime-Config aktualisiert: ${Object.keys(patch).join(", ")}`);
      } catch (err) {
        res.writeHead(400, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ ok: false, error: err.message }));
      }
    });
    return;
  } else if (req.url === "/api/onboarding") {
    // RVS-Credentials fuer QR-Code App-Onboarding
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({
      rvsHost: RVS_HOST,
      rvsPort: RVS_PORT,
      rvsTLS: RVS_TLS === "true" || RVS_TLS === true,
      rvsToken: RVS_TOKEN,
    }));
  } else if (req.url === "/api/cancel" && req.method === "POST") {
    log("warn", "server", "HTTP /api/cancel — Cancel-Request (von Bridge)");
    pendingMessageTime = 0;
    watchdogWarned = false;
    watchdogFixAttempted = false;
    if (pipelineActive) pipelineEnd(false, "Vom Benutzer abgebrochen (App)");
    else broadcast({ type: "agent_activity", activity: "idle" });
    dockerExec("aria-core", "openclaw doctor --fix 2>/dev/null || true").catch(() => {});
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ ok: true }));
  } else if (req.url.startsWith("/shared/")) {
    // Dateien aus Shared Volume ausliefern (Bilder, Uploads)
    const filePath = decodeURIComponent(req.url);
    const safePath = path.resolve(filePath);
    if (!safePath.startsWith("/shared/")) {
      res.writeHead(403);
      res.end("Forbidden");
      return;
    }
    try {
      if (!fs.existsSync(safePath)) { res.writeHead(404); res.end("Not Found"); return; }
      const ext = path.extname(safePath).toLowerCase();
      const mimeTypes = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".gif": "image/gif",
                          ".pdf": "application/pdf", ".txt": "text/plain", ".json": "application/json" };
      const contentType = mimeTypes[ext] || "application/octet-stream";
      const data = fs.readFileSync(safePath);
      res.writeHead(200, { "Content-Type": contentType, "Content-Length": data.length });
      res.end(data);
    } catch (err) {
      res.writeHead(500);
      res.end("Error");
    }
  } else {
    res.writeHead(404);
    res.end("Not Found");
  }
});

const wss = new WebSocketServer({ server });

wss.on("connection", (ws) => {
  browserClients.add(ws);
  // Initialen State + letzte Logs senden
  ws.send(JSON.stringify({ type: "init", state, logs: logs.slice(-100) }));
  // Letzten Disk-Status mitgeben damit der Client sofort weiss wie's um Platz steht
  if (currentDiskStatus) ws.send(JSON.stringify(currentDiskStatus));

  ws.on("message", (raw) => {
    try {
      const msg = JSON.parse(raw.toString());

      if (msg.action === "test_gateway") {
        pipelineStart("Gateway", msg.text || "aria lebst du noch?");
        sendToGateway(msg.text || "aria lebst du noch?", true);
      } else if (msg.action === "test_rvs") {
        pipelineStart("RVS", msg.text || "aria lebst du noch?");
        sendToRVS(msg.text || "aria lebst du noch?", true);
      } else if (msg.action === "reconnect_gateway") {
        connectGateway();
      } else if (msg.action === "reconnect_rvs") {
        connectRVS();
      } else if (msg.action === "test_proxy") {
        testProxy(msg.text);
      } else if (msg.action === "check_proxy_auth") {
        checkProxyAuth();
      } else if (msg.action === "proxy_login") {
        attachTerminal(ws, "aria-proxy", "claude login");
      } else if (msg.action === "core_terminal") {
        // Interaktive Shell in aria-core (fuer openclaw agents, etc.)
        attachTerminal(ws, "aria-core", msg.cmd || "sh");
      } else if (msg.action === "check_core_auth") {
        checkCoreAuth();
      } else if (msg.action === "term_input") {
        handleTermInput(ws, msg.data);
      } else if (msg.action === "write_credentials") {
        writeProxyCredentials(msg.credentials);
      } else if (msg.action === "docker_logs") {
        handleDockerLogs(ws, msg.tab, msg.tail);
      } else if (msg.action === "live_ssh_start") {
        startLiveSSH(ws);
      } else if (msg.action === "live_ssh_input") {
        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 === "send_file") {
        // Datei von Diagnostic an Bridge via RVS senden
        sendToRVS_raw({
          type: "file",
          payload: { name: msg.name, type: msg.type, size: msg.size, base64: msg.base64 },
          timestamp: Date.now(),
        });
        log("info", "server", `Datei gesendet: ${msg.name} (${msg.type})`);
      } else if (msg.action === "cancel_request") {
        // Laufende Anfrage abbrechen — doctor --fix beendet stuck runs
        log("warn", "server", "Anfrage abgebrochen — fuehre doctor --fix aus");
        pendingMessageTime = 0;
        watchdogWarned = false;
        watchdogFixAttempted = false;
        if (pipelineActive) pipelineEnd(false, "Vom Benutzer abgebrochen");
        broadcast({ type: "agent_activity", activity: "idle" });
        dockerExec("aria-core", "openclaw doctor --fix 2>/dev/null || true").catch(() => {});
      } else if (msg.action === "voice_upload") {
        // Voice-Samples an XTTS-Bridge via RVS weiterleiten, auf Bestätigung warten
        log("info", "server", `Voice-Upload '${msg.name}' (${(msg.samples || []).length} Samples) sende an RVS...`);
        sendToRVS_withResponse("voice_upload", { name: msg.name, samples: msg.samples }, "xtts_voice_saved", ws);
      } else if (msg.action === "xtts_list_voices") {
        // Frische Verbindung die auf Antwort wartet
        sendToRVS_withResponse("xtts_list_voices", {}, "xtts_voices_list", ws);
      } else if (msg.action === "xtts_delete_voice") {
        // Weiterleiten an XTTS-Bridge, die antwortet mit neuer Liste
        sendToRVS_raw({ type: "xtts_delete_voice", payload: { name: msg.name }, timestamp: Date.now() });
        log("info", "server", `Voice-Delete '${msg.name}' an XTTS-Bridge gesendet`);
      } else if (msg.action === "set_mode") {
        // Mode-Wechsel → Bridge bearbeitet und broadcastet an alle Clients
        sendToRVS_raw({ type: "mode", payload: { mode: msg.mode }, timestamp: Date.now() });
        log("info", "server", `Mode-Wechsel angefordert: ${msg.mode}`);
      } else if (msg.action === "get_voice_config") {
        handleGetVoiceConfig(ws);
      } else if (msg.action === "send_voice_config") {
        // Stimmen-Config persistent speichern + an Bridge via RVS senden
        let existing = {};
        try { existing = JSON.parse(fs.readFileSync("/shared/config/voice_config.json", "utf-8")); } catch {}
        const voiceConfig = {
          ...existing,
          ttsEnabled: msg.ttsEnabled !== false,
          xttsVoice: msg.xttsVoice || "",
        };
        if (msg.whisperModel !== undefined) voiceConfig.whisperModel = msg.whisperModel;
        // F5-TTS Tuning-Felder — immer mit dem vom User gesendeten Wert setzen,
        // auch leeren String. Leer = "reset auf Hard-Default". Sonst merkt die
        // Bridge nicht dass der User den Wert loeschen wollte (absent key war
        // vorher 'keep current' semantik → BigVGAN blieb drin obwohl User
        // leer eingetragen hatte).
        if (msg.f5ttsModel !== undefined)     voiceConfig.f5ttsModel = msg.f5ttsModel || "";
        if (msg.f5ttsCkptFile !== undefined)  voiceConfig.f5ttsCkptFile = msg.f5ttsCkptFile || "";
        if (msg.f5ttsVocabFile !== undefined) voiceConfig.f5ttsVocabFile = msg.f5ttsVocabFile || "";
        if (msg.f5ttsCfgStrength !== undefined && !isNaN(msg.f5ttsCfgStrength)) {
          voiceConfig.f5ttsCfgStrength = msg.f5ttsCfgStrength;
        }
        if (msg.f5ttsNfeStep !== undefined && !isNaN(msg.f5ttsNfeStep)) {
          voiceConfig.f5ttsNfeStep = msg.f5ttsNfeStep;
        }
        try {
          fs.mkdirSync("/shared/config", { recursive: true });
          fs.writeFileSync("/shared/config/voice_config.json", JSON.stringify(voiceConfig, null, 2));
        } catch {}
        sendToRVS_raw({ type: "config", payload: voiceConfig, timestamp: Date.now() });
        log("info", "server", `Voice-Config gespeichert: xttsVoice=${voiceConfig.xttsVoice || "default"}, whisper=${voiceConfig.whisperModel || "-"}`);
      } else if (msg.action === "test_tts") {
        handleTestTTS(ws, msg.text || "Test");
      } else if (msg.action === "preview_voice") {
        handleVoicePreview(ws, msg.voice || "", msg.text || "Hallo.", msg.speed);
      } else if (msg.action === "check_tts") {
        handleCheckTTS(ws);
      } else if (msg.action === "check_desktop") {
        checkDesktopAvailable(ws);
      } else if (msg.action === "load_chat_history") {
        handleLoadChatHistory(ws);
      } else if (msg.action === "list_sessions") {
        handleListSessions(ws);
      } else if (msg.action === "read_session") {
        handleReadSession(ws, msg.sessionPath);
      } else if (msg.action === "export_session") {
        handleExportSession(ws, msg.sessionPath, msg.sessionKey);
      } else if (msg.action === "delete_session") {
        handleDeleteSession(ws, msg.sessionPath);
      } else if (msg.action === "set_active_session") {
        handleSetActiveSession(ws, msg.sessionKey);
      } else if (msg.action === "get_active_session") {
        ws.send(JSON.stringify({ type: "active_session", sessionKey: activeSessionKey }));
      } else if (msg.action === "create_session") {
        handleCreateSession(ws, msg.sessionName);
      } else if (msg.action === "restart_session") {
        handleRestartSession(ws);
      } else if (msg.action === "list_brain") {
        handleListBrain(ws);
      } else if (msg.action === "read_brain_file") {
        handleReadBrainFile(ws, msg.filename);
      // ── Einstellungen ──
      // list_permissions / save_permissions entfernt — Alles-oder-Nichts via --dangerously-skip-permissions
      } else if (msg.action === "get_model") {
        handleGetModel(ws);
      } else if (msg.action === "set_model") {
        handleSetModel(ws, msg.model);
      } else if (msg.action === "get_openclaw_config") {
        handleGetOpenClawConfig(ws);
      }
    } catch {}
  });

  ws.on("close", () => {
    browserClients.delete(ws);
  });
});

// ── Live SSH ─────────────────────────────────────────────

function startLiveSSH(clientWs) {
  // Bestehende Session schliessen
  if (clientWs._sshSock) {
    try { clientWs._sshSock.end(); } catch (_) {}
    clientWs._sshSock = null;
  }

  const createBody = JSON.stringify({
    AttachStdin: true, AttachStdout: true, AttachStderr: true, Tty: true,
    Cmd: ["ssh", "-tt", "aria-wohnung"],
  });

  const createReq = http.request({
    socketPath: "/var/run/docker.sock",
    path: "/containers/aria-core/exec",
    method: "POST",
    headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(createBody) },
  }, (res) => {
    let data = "";
    res.on("data", (c) => data += c);
    res.on("end", () => {
      if (res.statusCode !== 201) {
        clientWs.send(JSON.stringify({ type: "live_ssh_error", error: `Exec create failed: HTTP ${res.statusCode}` }));
        return;
      }
      const execId = JSON.parse(data).Id;
      const startBody = JSON.stringify({ Detach: false, Tty: true });

      const sock = net.connect({ path: "/var/run/docker.sock" }, () => {
        const req = `POST /exec/${execId}/start HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nConnection: Upgrade\r\nUpgrade: tcp\r\nContent-Length: ${Buffer.byteLength(startBody)}\r\n\r\n${startBody}`;
        sock.write(req);
      });

      let headersParsed = false;
      let headerBuf = "";

      sock.on("data", (chunk) => {
        if (!headersParsed) {
          headerBuf += chunk.toString("utf-8");
          const headerEnd = headerBuf.indexOf("\r\n\r\n");
          if (headerEnd === -1) return;
          const headers = headerBuf.slice(0, headerEnd);
          const remaining = chunk.slice(chunk.length - (headerBuf.length - headerEnd - 4));
          headersParsed = true;

          if (!headers.includes("200") && !headers.includes("101")) {
            clientWs.send(JSON.stringify({ type: "live_ssh_error", error: `SSH start failed` }));
            sock.end();
            return;
          }

          log("info", "server", "Live SSH-Session gestartet");
          clientWs.send(JSON.stringify({ type: "live_ssh_connected" }));
          clientWs._sshSock = sock;

          if (remaining.length > 0) {
            clientWs.send(JSON.stringify({ type: "live_ssh_data", data: remaining.toString("base64") }));
          }
          return;
        }
        // Stream-Daten ans Frontend
        try {
          clientWs.send(JSON.stringify({ type: "live_ssh_data", data: chunk.toString("base64") }));
        } catch (_) { sock.end(); }
      });

      sock.on("close", () => {
        clientWs._sshSock = null;
        try { clientWs.send(JSON.stringify({ type: "live_ssh_closed" })); } catch (_) {}
        log("info", "server", "Live SSH-Session beendet");
      });

      sock.on("error", (err) => {
        clientWs._sshSock = null;
        try { clientWs.send(JSON.stringify({ type: "live_ssh_error", error: err.message })); } catch (_) {}
      });
    });
  });

  createReq.on("error", (err) => {
    clientWs.send(JSON.stringify({ type: "live_ssh_error", error: `Docker: ${err.message}` }));
  });
  createReq.end(createBody);
}

// ── Voice-Config laden ────────────────────────────────

function handleGetVoiceConfig(clientWs) {
  try {
    const configPath = "/shared/config/voice_config.json";
    if (fs.existsSync(configPath)) {
      const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
      clientWs.send(JSON.stringify({ type: "voice_config", ...config }));
    } else {
      clientWs.send(JSON.stringify({ type: "voice_config", ttsEnabled: true, xttsVoice: "" }));
    }
  } catch (err) {
    clientWs.send(JSON.stringify({ type: "voice_config", ttsEnabled: true, xttsVoice: "" }));
  }
}

// ── TTS Diagnose (XTTS) ───────────────────────────────
// ── Voice Preview ────────────────────────────────────────
// Sammelt audio_pcm Chunks einer Preview-Anfrage, baut am Ende eine WAV
// und schickt sie base64-kodiert an den Browser-Client.
//
// Map requestId → { clientWs, chunks: [Buffer], sampleRate, channels }
const _previewPending = new Map();

function _buildWavFromPcm(pcmBuf, sampleRate, channels) {
  const bitsPerSample = 16;
  const byteRate = sampleRate * channels * bitsPerSample / 8;
  const blockAlign = channels * bitsPerSample / 8;
  const dataSize = pcmBuf.length;
  const header = Buffer.alloc(44);
  header.write("RIFF", 0);
  header.writeUInt32LE(36 + dataSize, 4);
  header.write("WAVE", 8);
  header.write("fmt ", 12);
  header.writeUInt32LE(16, 16);            // subchunk1 size
  header.writeUInt16LE(1, 20);             // PCM
  header.writeUInt16LE(channels, 22);
  header.writeUInt32LE(sampleRate, 24);
  header.writeUInt32LE(byteRate, 28);
  header.writeUInt16LE(blockAlign, 32);
  header.writeUInt16LE(bitsPerSample, 34);
  header.write("data", 36);
  header.writeUInt32LE(dataSize, 40);
  return Buffer.concat([header, pcmBuf]);
}

function _handlePreviewChunk(payload) {
  const reqId = payload?.requestId || "";
  const entry = _previewPending.get(reqId);
  if (!entry) return;
  if (payload.base64) {
    try { entry.chunks.push(Buffer.from(payload.base64, "base64")); } catch {}
  }
  if (!entry.sampleRate && payload.sampleRate) entry.sampleRate = payload.sampleRate;
  if (!entry.channels && payload.channels) entry.channels = payload.channels;
  if (payload.final) {
    _previewPending.delete(reqId);
    try {
      const pcm = Buffer.concat(entry.chunks);
      const wav = _buildWavFromPcm(pcm, entry.sampleRate || 24000, entry.channels || 1);
      const b64 = wav.toString("base64");
      if (entry.clientWs && entry.clientWs.readyState === 1) {
        entry.clientWs.send(JSON.stringify({
          type: "voice_preview_audio",
          base64: b64,
          size: wav.length,
        }));
      }
    } catch (err) {
      if (entry.clientWs && entry.clientWs.readyState === 1) {
        entry.clientWs.send(JSON.stringify({
          type: "voice_preview_audio",
          error: err.message,
        }));
      }
    }
  }
}

async function handleVoicePreview(clientWs, voice, text, speed) {
  try {
    // Speed clampen — Browser-Slider ist 0.1-5.0
    let spd = parseFloat(speed);
    if (!isFinite(spd) || spd < 0.1 || spd > 5.0) spd = 1.0;
    const requestId = crypto.randomUUID();
    _previewPending.set(requestId, { clientWs, chunks: [], sampleRate: 0, channels: 0 });
    // Timeout safety net
    setTimeout(() => {
      if (_previewPending.has(requestId)) {
        _previewPending.delete(requestId);
        if (clientWs && clientWs.readyState === 1) {
          clientWs.send(JSON.stringify({
            type: "voice_preview_audio",
            error: "Timeout (60s) — keine Antwort vom f5tts-bridge",
          }));
        }
      }
    }, 60000);
    log("info", "server", `Voice-Preview: voice="${voice}" speed=${spd.toFixed(1)}x text="${text.slice(0, 60)}"`);
    sendToRVS_raw({
      type: "xtts_request",
      payload: { text, language: "de", requestId, voice, speed: spd },
      timestamp: Date.now(),
    });
  } catch (err) {
    clientWs.send(JSON.stringify({ type: "voice_preview_audio", error: err.message }));
  }
}

async function handleTestTTS(clientWs, text) {
  try {
    log("info", "server", `TTS-Test via XTTS: "${text}"`);
    // Via RVS an die XTTS-Bridge: xtts_request mit Test-Text
    const requestId = crypto.randomUUID();
    sendToRVS_raw({
      type: "xtts_request",
      payload: { text, language: "de", requestId, voice: "" },
      timestamp: Date.now(),
    });
    clientWs.send(JSON.stringify({ type: "tts_result", ok: true, duration: "pending", size: "?" }));
  } catch (err) {
    clientWs.send(JSON.stringify({ type: "tts_result", ok: false, error: err.message }));
  }
}

async function handleCheckTTS(clientWs) {
  try {
    // XTTS-Status ueber RVS abfragen (xtts_list_voices)
    sendToRVS_raw({ type: "xtts_list_voices", payload: {}, timestamp: Date.now() });
    clientWs.send(JSON.stringify({
      type: "tts_status",
      ok: true,
      error: 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 }, () => {
    checkSock.end();
    clientWs.send(JSON.stringify({
      type: "desktop_status",
      available: true,
      url: `http://${clientWs._socket?.remoteAddress || "localhost"}:6080/vnc.html?autoconnect=true`,
      message: "VNC Desktop verfuegbar",
    }));
  });
  checkSock.on("error", () => {
    clientWs.send(JSON.stringify({
      type: "desktop_status",
      available: false,
      message: "Kein VNC-Server auf aria-wohnung gefunden (Port 5901)",
    }));
  });
  checkSock.setTimeout(3000, () => {
    checkSock.destroy();
    clientWs.send(JSON.stringify({
      type: "desktop_status",
      available: false,
      message: "VNC-Server nicht erreichbar (Timeout)",
    }));
  });
}

// ── Session Viewer ──────────────────────────────────────

const SESSIONS_DIR = "/home/node/.openclaw/agents/main/sessions";

async function handleListSessions(clientWs) {
  try {
    log("info", "server", "Lade Sessions aus aria-core...");

    // sessions.json als Index lesen + Datei-Details holen (inkl. .reset.* Archive)
    const raw = await dockerExec("aria-core", `
      cat ${SESSIONS_DIR}/sessions.json 2>/dev/null || echo '{}' &&
      echo '===FILE_DETAILS===' &&
      for f in ${SESSIONS_DIR}/*.jsonl ${SESSIONS_DIR}/*.jsonl.reset.*; do
        [ -f "$f" ] || continue
        name=$(basename "$f")
        msgs=$(grep -cE '"role":"(user|assistant)"' "$f" 2>/dev/null || echo 0)
        size=$(du -h "$f" 2>/dev/null | cut -f1)
        modified=$(stat -c '%Y' "$f" 2>/dev/null || echo 0)
        echo "FILE:$name|LINES:$msgs|SIZE:$size|MODIFIED:$modified"
      done
    `.trim());

    const parts = raw.split("===FILE_DETAILS===");
    let sessionsIndex = {};
    try { sessionsIndex = JSON.parse(parts[0].trim()); } catch {}

    // Datei-Details parsen
    const fileDetails = {};
    if (parts[1]) {
      for (const line of parts[1].trim().split("\n")) {
        if (!line.startsWith("FILE:")) continue;
        const segs = {};
        for (const seg of line.split("|")) {
          const idx = seg.indexOf(":");
          if (idx > 0) segs[seg.slice(0, idx)] = seg.slice(idx + 1);
        }
        if (segs.FILE) fileDetails[segs.FILE] = segs;
      }
    }

    // Sessions zusammenbauen: Index + Datei-Info kombinieren
    const sessions = [];

    // Aus sessions.json die Session-Keys und IDs
    const indexEntries = Array.isArray(sessionsIndex) ? sessionsIndex
      : Array.isArray(sessionsIndex.sessions) ? sessionsIndex.sessions
      : Object.entries(sessionsIndex).map(([k, v]) => ({ key: k, ...(typeof v === "object" ? v : { id: v }) }));

    for (const entry of indexEntries) {
      const id = entry.id || entry.sessionId || "";
      const rawKey = entry.key || entry.sessionKey || entry.name || id;
      // "agent:main:aria-diagnostic" → "aria-diagnostic"
      const key = rawKey.replace(/^agent:main:/, "");
      const filename = `${id}.jsonl`;
      const details = fileDetails[filename] || {};
      // updatedAt aus sessions.json (ms) ist genauer als stat
      const updatedAt = entry.updatedAt || 0;
      const model = entry.model || "";

      sessions.push({
        path: `${SESSIONS_DIR}/${filename}`,
        sessionKey: key,
        sessionId: id,
        lines: parseInt(details.LINES) || 0,
        size: details.SIZE || "?",
        modified: updatedAt ? Math.floor(updatedAt / 1000) : (parseInt(details.MODIFIED) || 0),
        model,
      });

      // Aus fileDetails entfernen (fuer Waisen-Check)
      delete fileDetails[filename];
    }

    // Dateien die nicht im Index stehen (Waisen ODER Reset-Archive)
    for (const [filename, details] of Object.entries(fileDetails)) {
      // .jsonl.reset.<ISO-Timestamp>Z → archivierte Session (OpenClaw-Reset)
      // Format: 528f4d70-...jsonl.reset.2026-04-18T09-49-44.814Z
      const resetMatch = filename.match(/^([a-f0-9-]+)\.jsonl\.reset\.(.+Z)$/);
      if (resetMatch) {
        const id = resetMatch[1];
        // Timestamp ISO-8601 parsen: 2026-04-18T09-49-44.814Z → 2026-04-18T09:49:44.814Z
        const tsStr = resetMatch[2].replace(/T(\d{2})-(\d{2})-(\d{2})/, "T$1:$2:$3");
        const resetAt = Math.floor(new Date(tsStr).getTime() / 1000) || parseInt(details.MODIFIED) || 0;
        sessions.push({
          path: `${SESSIONS_DIR}/${filename}`,
          sessionKey: id.slice(0, 8) + "… (archiv)",
          sessionId: id,
          lines: parseInt(details.LINES) || 0,
          size: details.SIZE || "?",
          modified: resetAt,
          archived: true,
          resetAt,
        });
        continue;
      }
      // Echte Waisen (UUID.jsonl ohne Eintrag in sessions.json)
      const id = filename.replace(".jsonl", "");
      sessions.push({
        path: `${SESSIONS_DIR}/${filename}`,
        sessionKey: id.slice(0, 12) + "...",
        sessionId: id,
        lines: parseInt(details.LINES) || 0,
        size: details.SIZE || "?",
        modified: parseInt(details.MODIFIED) || 0,
        orphan: true,
      });
    }

    sessions.sort((a, b) => b.modified - a.modified);

    clientWs.send(JSON.stringify({ type: "sessions_list", sessions }));
    log("info", "server", `${sessions.length} Session(s) gefunden`);
  } catch (err) {
    log("error", "server", `Sessions laden fehlgeschlagen: ${err.message}`);
    clientWs.send(JSON.stringify({ type: "sessions_list", sessions: [], error: err.message }));
  }
}

async function handleReadSession(clientWs, sessionPath) {
  if (!sessionPath || sessionPath.includes("..")) {
    clientWs.send(JSON.stringify({ type: "session_detail", error: "Ungueltiger Pfad" }));
    return;
  }
  try {
    // Letzte 100 Zeilen der Session (JSONL)
    const raw = await dockerExec("aria-core", `tail -100 '${sessionPath.replace(/'/g, "")}'`);
    const messages = [];
    for (const line of raw.split("\n")) {
      if (!line.trim()) continue;
      try {
        const obj = JSON.parse(line);
        messages.push(obj);
      } catch {}
    }
    clientWs.send(JSON.stringify({ type: "session_detail", path: sessionPath, messages, raw: messages.length === 0 ? raw : undefined }));
  } catch (err) {
    clientWs.send(JSON.stringify({ type: "session_detail", error: err.message }));
  }
}

async function handleExportSession(clientWs, sessionPath, sessionKey) {
  if (!sessionPath || sessionPath.includes("..") || !sessionPath.startsWith(SESSIONS_DIR)) {
    clientWs.send(JSON.stringify({ type: "session_export", ok: false, error: "Ungueltiger Pfad" }));
    return;
  }
  try {
    const safePath = sessionPath.replace(/'/g, "");
    const raw = await dockerExec("aria-core", `cat '${safePath}'`);
    const lines = raw.split("\n").filter(l => l.trim());

    const blocks = [];
    for (const line of lines) {
      let obj;
      try { obj = JSON.parse(line); } catch { continue; }
      if (obj.type !== "message" || !obj.message) continue;
      const role = obj.message.role;
      if (role !== "user" && role !== "assistant") continue;

      let text = "";
      const content = obj.message.content;
      if (typeof content === "string") text = content;
      else if (Array.isArray(content)) text = content.filter(c => c.type === "text").map(c => c.text || "").join("\n");
      if (!text) continue;

      if (role === "user") {
        text = text.replace(/^Sender \(untrusted metadata\):[\s\S]*?```[\s\S]*?```\s*\n*/m, "").trim();
        text = text.replace(/^\[.*?\]\s*/, "").trim();
      } else {
        text = text.replace(/^\[\[reply_to_\w+\]\]\s*/g, "").trim();
      }
      if (!text) continue;

      const ts = obj.message.timestamp || obj.timestamp || 0;
      const when = ts ? new Date(ts).toISOString().replace("T", " ").slice(0, 19) : "";
      const heading = role === "user" ? "## 🧑 User" : "## 🤖 ARIA";
      blocks.push(`${heading}${when ? ` — ${when}` : ""}\n\n${text}`);
    }

    const exportedAt = new Date().toISOString().replace("T", " ").slice(0, 19);
    const title = sessionKey || sessionPath.split("/").pop().replace(".jsonl", "");
    const markdown = [
      `# Session: ${title}`,
      ``,
      `Exportiert: ${exportedAt}  `,
      `Quelle: ${sessionPath}`,
      ``,
      `---`,
      ``,
      blocks.join("\n\n---\n\n"),
      ``,
    ].join("\n");

    const safeKey = (sessionKey || "session").replace(/[^a-zA-Z0-9_-]/g, "_");
    const filename = `${exportedAt.slice(0, 10)}_${safeKey}.md`;
    clientWs.send(JSON.stringify({ type: "session_export", ok: true, filename, markdown }));
    log("info", "server", `Session exportiert: ${filename} (${blocks.length} Nachrichten)`);
  } catch (err) {
    log("error", "server", `Session-Export fehlgeschlagen: ${err.message}`);
    clientWs.send(JSON.stringify({ type: "session_export", ok: false, error: err.message }));
  }
}

async function handleDeleteSession(clientWs, sessionPath) {
  if (!sessionPath || sessionPath.includes("..") || !sessionPath.startsWith(SESSIONS_DIR)) {
    clientWs.send(JSON.stringify({ type: "session_deleted", ok: false, error: "Ungueltiger Pfad" }));
    return;
  }
  try {
    log("warn", "server", `Loesche Session: ${sessionPath}`);
    const safePath = sessionPath.replace(/'/g, "");
    // Session-ID aus Pfad extrahieren (UUID.jsonl)
    const filename = safePath.split("/").pop();
    const sessionId = filename.replace(".jsonl", "");

    // 1. JSONL-Datei loeschen
    await dockerExec("aria-core", `rm -f '${safePath}'`);

    // 2. Eintrag aus sessions.json entfernen
    try {
      const sFile = `${SESSIONS_DIR}/sessions.json`;
      const script = [
        'const fs=require("fs");',
        `const f="${sFile}",sid="${sessionId}";`,
        'try{const d=JSON.parse(fs.readFileSync(f,"utf8"));',
        'for(const k of Object.keys(d)){const v=d[k];',
        'if(v&&(v.sessionId===sid||v.id===sid))delete d[k];}',
        'fs.writeFileSync(f,JSON.stringify(d,null,2));}catch(e){}',
      ].join("");
      const b64 = Buffer.from(script).toString("base64");
      await dockerExec("aria-core", `echo ${b64} | base64 -d | node`);
    } catch (e) {
      log("warn", "server", `sessions.json Update fehlgeschlagen: ${e.message}`);
    }

    clientWs.send(JSON.stringify({ type: "session_deleted", ok: true, path: sessionPath }));
    log("info", "server", "Session geloescht");
  } catch (err) {
    clientWs.send(JSON.stringify({ type: "session_deleted", ok: false, error: err.message }));
  }
}

// ── Session-Aufloesung: letzte aktive Session finden ────
// Wird nach Gateway-(Re-)Connect aufgerufen. Darf die explizit gewaehlte
// Session NIE ueberschreiben — nur beim absoluten Erststart auto-picken.
async function resolveActiveSession() {
  if (sessionFromFile) {
    log("info", "server", `Session '${activeSessionKey}' aus /data — keine Auto-Wahl`);
    return;
  }

  const indexRaw = await dockerExec("aria-core", `cat ${SESSIONS_DIR}/sessions.json 2>/dev/null || echo '{}'`);
  log("debug", "server", `sessions.json: ${indexRaw.slice(0, 500)}`);
  let sessionsIndex = {};
  try { sessionsIndex = JSON.parse(indexRaw.trim()); } catch { return; }

  const entries = Array.isArray(sessionsIndex) ? sessionsIndex
    : Array.isArray(sessionsIndex.sessions) ? sessionsIndex.sessions
    : Object.entries(sessionsIndex).map(([k, v]) => ({ key: k, ...(typeof v === "object" ? v : { id: v }) }));

  if (entries.length === 0) return;

  // Vorhandene Keys loggen
  const keys = entries.map(e => (e.key || e.sessionKey || e.name || "?").replace(/^agent:main:/, ""));
  log("info", "server", `Verfuegbare Sessions: [${keys.join(", ")}]`);

  // Neueste Session nehmen — aber user-definierte bevorzugen.
  // aria-bridge / aria-diagnostic werden von den Services auto-erstellt;
  // bei erstem Start soll lieber eine "echte" Session gewaehlt werden,
  // falls vorhanden.
  const AUTO_KEYS = new Set(["aria-bridge", "aria-diagnostic"]);
  const normalise = (e) => (e.key || e.sessionKey || e.name || "").replace(/^agent:main:/, "");

  const userEntries = entries.filter(e => !AUTO_KEYS.has(normalise(e)));
  const pool = userEntries.length > 0 ? userEntries : entries;

  let newest = null;
  let newestTime = 0;
  for (const entry of pool) {
    const t = entry.updatedAt || entry.createdAt || 0;
    if (t >= newestTime) {
      newestTime = t;
      newest = entry;
    }
  }

  if (newest) {
    const key = normalise(newest);
    if (key) {
      activeSessionKey = key;
      persistActiveSession(activeSessionKey);
      log("info", "server", `Auto-Wahl Erststart: '${activeSessionKey}'`);
      for (const c of browserClients) {
        c.send(JSON.stringify({ type: "active_session", sessionKey: activeSessionKey }));
      }
    }
  }
}

// ── Chat-History aus aktiver Session laden (Display-Only) ──
async function handleLoadChatHistory(clientWs) {
  try {
    // Session-ID fuer den aktiven sessionKey finden
    const indexRaw = await dockerExec("aria-core", `cat ${SESSIONS_DIR}/sessions.json 2>/dev/null || echo '{}'`);
    let sessionsIndex = {};
    try { sessionsIndex = JSON.parse(indexRaw.trim()); } catch {}

    const entries = Array.isArray(sessionsIndex) ? sessionsIndex
      : Array.isArray(sessionsIndex.sessions) ? sessionsIndex.sessions
      : Object.entries(sessionsIndex).map(([k, v]) => ({ key: k, ...(typeof v === "object" ? v : { id: v }) }));

    let sessionId = null;
    const availableKeys = [];
    for (const entry of entries) {
      const rawKey = entry.key || entry.sessionKey || entry.name || "";
      const key = rawKey.replace(/^agent:main:/, "");
      availableKeys.push(key);
      if (key === activeSessionKey) {
        sessionId = entry.id || entry.sessionId || "";
        break;
      }
    }

    if (!sessionId) {
      log("warn", "server", `Chat-History: Session '${activeSessionKey}' nicht in sessions.json gefunden. Verfuegbar: [${availableKeys.join(", ")}]`);
      clientWs.send(JSON.stringify({ type: "chat_history", messages: [] }));
      return;
    }

    log("info", "server", `Chat-History: Session '${activeSessionKey}' -> ID '${sessionId}'`);

    const sessionFile = `${SESSIONS_DIR}/${sessionId}.jsonl`;
    const raw = await dockerExec("aria-core", `cat '${sessionFile}' 2>/dev/null || echo ''`);
    const chatMessages = [];

    for (const line of raw.split("\n")) {
      if (!line.trim()) continue;
      try {
        const obj = JSON.parse(line);
        // OpenClaw Format: {"type":"message","message":{"role":"user|assistant","content":[...]}}
        if (obj.type !== "message" || !obj.message) continue;
        const msg = obj.message;
        const role = msg.role;
        if (!role) continue;

        // Text aus content-Array extrahieren
        let text = "";
        if (typeof msg.content === "string") text = msg.content;
        else if (Array.isArray(msg.content)) text = msg.content.filter(c => c.type === "text").map(c => c.text || "").join("\n");
        if (!text) continue;

        if (role === "user") {
          // Metadata-Prefix entfernen: "Sender (untrusted metadata):\n```json\n{...}\n```\n\n[timestamp] Text"
          text = text.replace(/^Sender \(untrusted metadata\):[\s\S]*?```[\s\S]*?```\s*\n*/m, "").trim();
          // Timestamp-Prefix entfernen: "[Sat 2026-03-28 14:51 UTC] "
          text = text.replace(/^\[.*?\]\s*/, "").trim();
          chatMessages.push({ type: "sent", text, meta: "Gateway direkt", ts: msg.timestamp || obj.timestamp || 0 });
        } else if (role === "assistant") {
          // Reply-Prefix entfernen: "[[reply_to_current]] "
          text = text.replace(/^\[\[reply_to_\w+\]\]\s*/g, "").trim();
          if (text) chatMessages.push({ type: "received", text, meta: "chat:final", ts: msg.timestamp || obj.timestamp || 0 });
        }
      } catch {}
    }

    clientWs.send(JSON.stringify({ type: "chat_history", messages: chatMessages }));
    log("info", "server", `Chat-History geladen: ${chatMessages.length} Nachrichten`);
  } catch (err) {
    log("error", "server", `Chat-History laden fehlgeschlagen: ${err.message}`);
    clientWs.send(JSON.stringify({ type: "chat_history", messages: [], error: err.message }));
  }
}

// ── Session aktivieren ─────────────────────────────────
function handleSetActiveSession(clientWs, sessionKey) {
  if (!sessionKey || typeof sessionKey !== "string") {
    clientWs.send(JSON.stringify({ type: "active_session", ok: false, error: "Kein sessionKey" }));
    return;
  }
  activeSessionKey = sessionKey;
  const ok = persistActiveSession(activeSessionKey);
  log("info", "server", `Aktive Session: ${activeSessionKey}${ok ? "" : " (WARN: nicht persistiert!)"}`);
  if (!ok) {
    clientWs.send(JSON.stringify({ type: "active_session", ok: false, sessionKey: activeSessionKey, error: "Persistierung fehlgeschlagen — /data Volume pruefen" }));
  }
  // Allen Clients mitteilen
  for (const c of browserClients) {
    c.send(JSON.stringify({ type: "active_session", sessionKey: activeSessionKey }));
  }
}

// ── Session erstellen ──────────────────────────────────
async function handleCreateSession(clientWs, sessionName) {
  if (!sessionName || typeof sessionName !== "string" || !/^[a-zA-Z0-9_-]+$/.test(sessionName)) {
    clientWs.send(JSON.stringify({ type: "session_created", ok: false, error: "Ungueltiger Name (nur a-z, 0-9, -, _)" }));
    return;
  }
  try {
    // Session wird automatisch erstellt wenn man die erste Nachricht sendet
    activeSessionKey = sessionName;
    persistActiveSession(activeSessionKey);
    log("info", "server", `Neue Session erstellt und aktiviert: ${sessionName}`);
    // Allen Clients mitteilen
    for (const c of browserClients) {
      c.send(JSON.stringify({ type: "active_session", sessionKey: activeSessionKey }));
    }
    clientWs.send(JSON.stringify({ type: "session_created", ok: true, sessionKey: sessionName }));
  } catch (err) {
    clientWs.send(JSON.stringify({ type: "session_created", ok: false, error: err.message }));
  }
}

// ── Session neu starten (Container Restart) ────────────
async function handleRestartSession(clientWs) {
  try {
    log("info", "server", "Starte aria-core Container neu (Session Restart)...");
    clientWs.send(JSON.stringify({ type: "session_restarted", status: "restarting" }));

    // Container neu starten via Docker API
    const http = require("http");
    await new Promise((resolve, reject) => {
      const req = http.request({
        socketPath: "/var/run/docker.sock",
        path: "/containers/aria-core/restart?t=5",
        method: "POST",
      }, (res) => {
        let body = "";
        res.on("data", d => body += d);
        res.on("end", () => {
          if (res.statusCode === 204 || res.statusCode === 200) resolve();
          else reject(new Error(`Restart fehlgeschlagen: HTTP ${res.statusCode} ${body}`));
        });
      });
      req.on("error", reject);
      req.end();
    });

    log("info", "server", "aria-core Container neu gestartet");
    // Warten bis Gateway wieder erreichbar (max 30s)
    for (let i = 0; i < 15; i++) {
      await new Promise(r => setTimeout(r, 2000));
      try {
        await dockerExec("aria-core", "echo ok");
        log("info", "server", "aria-core ist wieder erreichbar");
        clientWs.send(JSON.stringify({
          type: "session_restarted", status: "ok",
          info: "aria-core neu gestartet — Permissions werden bei der naechsten Nachricht geladen",
        }));
        // Aktive Session beibehalten
        for (const c of browserClients) {
          c.send(JSON.stringify({ type: "active_session", sessionKey: activeSessionKey }));
        }
        return;
      } catch {}
    }
    clientWs.send(JSON.stringify({ type: "session_restarted", status: "timeout", error: "aria-core antwortet nicht nach 30s" }));
  } catch (err) {
    log("error", "server", `Session Restart fehlgeschlagen: ${err.message}`);
    clientWs.send(JSON.stringify({ type: "session_restarted", status: "error", error: err.message }));
  }
}

// ── Brain Viewer ────────────────────────────────────────

async function handleListBrain(clientWs) {
  try {
    log("info", "server", "Lade Brain-Dateien...");
    const raw = await dockerExec("aria-core", `
      for f in /home/node/.openclaw/workspace/memory/*; do
        [ -f "$f" ] || continue
        name=$(basename "$f")
        size=$(du -h "$f" 2>/dev/null | cut -f1)
        lines=$(wc -l < "$f" 2>/dev/null || echo 0)
        modified=$(stat -c '%Y' "$f" 2>/dev/null || echo 0)
        # Frontmatter extrahieren (erste 10 Zeilen)
        head10=$(head -10 "$f" 2>/dev/null | tr '\\n' '|')
        echo "FILE:$name|SIZE:$size|LINES:$lines|MODIFIED:$modified|HEAD:$head10"
      done
    `.trim());

    const files = [];
    for (const line of raw.split("\n")) {
      if (!line.startsWith("FILE:")) continue;
      const parts = {};
      for (const seg of line.split("|")) {
        const idx = seg.indexOf(":");
        if (idx > 0) {
          const key = seg.slice(0, idx);
          const val = seg.slice(idx + 1);
          // HEAD hat mehrere |, also nur die bekannten Keys parsen
          if (["FILE", "SIZE", "LINES", "MODIFIED"].includes(key)) {
            parts[key] = val;
          }
        }
      }
      if (!parts.FILE || parts.FILE === "*") continue;

      // Frontmatter-Info aus HEAD extrahieren
      let description = "";
      let memType = "";
      const headPart = line.slice(line.indexOf("|HEAD:") + 6);
      if (headPart) {
        const headLines = headPart.split("|");
        for (const hl of headLines) {
          if (hl.startsWith("description:")) description = hl.replace("description:", "").trim();
          if (hl.startsWith("type:")) memType = hl.replace("type:", "").trim();
        }
      }

      files.push({
        name: parts.FILE,
        size: parts.SIZE || "?",
        lines: parseInt(parts.LINES) || 0,
        modified: parseInt(parts.MODIFIED) || 0,
        description,
        memType,
      });
    }

    files.sort((a, b) => b.modified - a.modified);
    clientWs.send(JSON.stringify({ type: "brain_list", files }));
    log("info", "server", `${files.length} Brain-Datei(en) gefunden`);
  } catch (err) {
    log("error", "server", `Brain laden fehlgeschlagen: ${err.message}`);
    clientWs.send(JSON.stringify({ type: "brain_list", files: [], error: err.message }));
  }
}

async function handleReadBrainFile(clientWs, filename) {
  // Path Traversal verhindern
  if (!filename || filename.includes("..") || filename.includes("/")) {
    clientWs.send(JSON.stringify({ type: "brain_content", error: "Ungueltiger Dateiname" }));
    return;
  }
  try {
    const content = await dockerExec("aria-core",
      `cat '/home/node/.openclaw/workspace/memory/${filename.replace(/'/g, "")}'`);
    clientWs.send(JSON.stringify({ type: "brain_content", filename, content }));
  } catch (err) {
    clientWs.send(JSON.stringify({ type: "brain_content", filename, error: err.message }));
  }
}

// ── Einstellungen: Tool-Berechtigungen ──────────────────
// ENTFERNT: Granulare Permissions haben nie funktioniert.
// Claude Code laeuft mit --dangerously-skip-permissions (Alles oder Nichts).
// Root-Check wird via CLAUDE_CODE_BUBBLEWRAP=1 in docker-compose.yml umgangen.

// ── Einstellungen: Model ────────────────────────────────

async function handleGetModel(clientWs) {
  try {
    const raw = await dockerExec("aria-core", `echo $DEFAULT_MODEL`);
    clientWs.send(JSON.stringify({ type: "model_info", model: raw.trim(), info: "Aktuelles Model (ENV)" }));
  } catch (err) {
    clientWs.send(JSON.stringify({ type: "model_info", error: err.message }));
  }
}

async function handleSetModel(clientWs, model) {
  if (!model || typeof model !== "string") {
    clientWs.send(JSON.stringify({ type: "model_info", error: "Kein Model angegeben" }));
    return;
  }
  try {
    // Model in Settings speichern (OpenClaw liest das)
    const settingsPath = await findSettingsFile();
    const script = [
      'const fs=require("fs");',
      `const f="${settingsPath}";`,
      'let s={};try{s=JSON.parse(fs.readFileSync(f,"utf8"));}catch(e){}',
      `s.model=${JSON.stringify(model)};`,
      `const dir=f.substring(0,f.lastIndexOf("/"));`,
      'try{fs.mkdirSync(dir,{recursive:true});}catch(e){}',
      'fs.writeFileSync(f,JSON.stringify(s,null,2));',
    ].join("");
    const b64 = Buffer.from(script).toString("base64");
    await dockerExec("aria-core", `echo ${b64} | base64 -d | node`);

    clientWs.send(JSON.stringify({ type: "model_info", model, info: `Model auf "${model}" gesetzt (Neustart noetig)` }));
    log("info", "server", `Model gesetzt: ${model}`);
  } catch (err) {
    clientWs.send(JSON.stringify({ type: "model_info", error: err.message }));
  }
}

// ── Einstellungen: OpenClaw Config ──────────────────────

async function handleGetOpenClawConfig(clientWs) {
  try {
    const raw = await dockerExec("aria-core", `
      echo '=== Umgebungsvariablen ==='
      echo "DEFAULT_MODEL=$DEFAULT_MODEL"
      echo "RATE_LIMIT_PER_USER=$RATE_LIMIT_PER_USER"
      echo "OPENCLAW_GATEWAY_TOKEN=$(echo $OPENCLAW_GATEWAY_TOKEN | head -c 8)..."
      echo "OPENCLAW_GATEWAY_BIND=$OPENCLAW_GATEWAY_BIND"
      echo ""
      echo '=== openclaw.json ==='
      cat /home/node/.openclaw/openclaw.json 2>/dev/null || echo "(nicht vorhanden)"
      echo ""
      echo '=== exec-approvals.json ==='
      cat /home/node/.openclaw/exec-approvals.json 2>/dev/null || echo "(nicht vorhanden)"
      echo ""
      echo '=== Agent-Verzeichnis ==='
      ls -la /home/node/.openclaw/agents/main/agent/ 2>&1
      echo ""
      echo '=== Workspace ==='
      ls -la /home/node/.openclaw/workspace/ 2>&1
    `.trim());
    clientWs.send(JSON.stringify({ type: "openclaw_config", config: raw }));
  } catch (err) {
    clientWs.send(JSON.stringify({ type: "openclaw_config", error: err.message }));
  }
}

// ── Start ───────────────────────────────────────────────

server.listen(HTTP_PORT, "0.0.0.0", () => {
  log("info", "server", `Diagnostic Server laeuft auf http://0.0.0.0:${HTTP_PORT}`);
  log("info", "server", `Gateway: ${GATEWAY_URL}`);
  log("info", "server", `Token: ${GATEWAY_TOKEN ? GATEWAY_TOKEN.slice(0, 8) + "..." : "(keiner)"}`);
  log("info", "server", `RVS: ${RVS_HOST ? `${RVS_HOST}:${RVS_PORT}` : "(nicht konfiguriert)"}`);
  log("info", "server", `Proxy: ${PROXY_URL}`);

  // Verbindungen aufbauen
  connectGateway();
  connectRVS();
});
