ARIA-AGENT/diagnostic/index.html

649 lines
29 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ARIA Diagnostic</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0D0D1A; color: #E0E0F0; font-family: 'Courier New', monospace; padding: 16px; }
h1 { font-size: 20px; margin-bottom: 16px; color: #0096FF; }
h2 { font-size: 14px; margin-bottom: 8px; color: #8888AA; text-transform: uppercase; letter-spacing: 1px; }
.grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; margin-bottom: 16px; }
.card { background: #12122A; border: 1px solid #1E1E2E; border-radius: 8px; padding: 12px; }
.card.full { grid-column: 1 / -1; }
.status-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.dot.connected { background: #34C759; box-shadow: 0 0 6px #34C759; }
.dot.connecting, .dot.testing { background: #FFD60A; animation: pulse 1s infinite; }
.dot.disconnected { background: #FF3B30; }
.dot.error { background: #FF3B30; box-shadow: 0 0 6px #FF3B30; }
.dot.not_configured { background: #555570; }
.dot.unknown { background: #555570; }
@keyframes pulse { 50% { opacity: 0.5; } }
.status-label { font-size: 13px; }
.error-text { color: #FF6B6B; font-size: 11px; margin-top: 4px; word-break: break-all; }
.btn { background: #0096FF; color: #fff; border: none; border-radius: 6px; padding: 8px 16px;
font-family: inherit; font-size: 13px; cursor: pointer; margin: 4px 4px 4px 0; }
.btn:hover { background: #007ADB; }
.btn:disabled { background: #333; color: #666; cursor: not-allowed; }
.btn.secondary { background: #1E1E2E; border: 1px solid #333; }
.btn.secondary:hover { background: #2A2A3E; }
/* Tabs */
.tab-bar { display: flex; gap: 2px; margin-bottom: 0; }
.tab-btn { background: #1E1E2E; color: #8888AA; border: 1px solid #1E1E2E; border-bottom: none;
border-radius: 6px 6px 0 0; padding: 6px 14px; font-family: inherit; font-size: 12px;
cursor: pointer; position: relative; top: 1px; }
.tab-btn.active { background: #080810; color: #E0E0F0; border-color: #1E1E2E; }
.tab-btn .tab-count { background: #333; color: #888; border-radius: 8px; padding: 1px 6px;
font-size: 10px; margin-left: 4px; }
.log-header { display: flex; align-items: center; justify-content: space-between; padding: 6px 8px 4px; }
.log-header h2 { margin-bottom: 0; font-size: 12px; }
.pause-hint { font-size: 11px; color: #FFD60A; display: none; }
.pause-hint.visible { display: inline; }
.log-panel { background: #080810; border: 1px solid #1E1E2E; border-radius: 0 0 6px 6px; }
.log-box { height: 280px; overflow-y: auto; padding: 8px; font-size: 11px; line-height: 1.6; }
.log-box.hidden { display: none; }
.log-entry { white-space: pre-wrap; word-break: break-all; }
.log-entry.error { color: #FF6B6B; }
.log-entry.warn { color: #FFD60A; }
.log-entry.info { color: #AAB; }
.log-entry.debug { color: #555570; }
.log-entry.pipeline-step { color: #0096FF; border-left: 2px solid #0096FF; padding-left: 6px; margin: 2px 0; }
.log-entry.pipeline-ok { color: #34C759; border-left: 2px solid #34C759; padding-left: 6px; margin: 2px 0; }
.log-entry.pipeline-err { color: #FF3B30; border-left: 2px solid #FF3B30; padding-left: 6px; margin: 2px 0; }
.log-entry.pipeline-sep { color: #333; margin: 6px 0 2px; }
.chat-box { background: #080810; border: 1px solid #1E1E2E; border-radius: 6px;
min-height: 120px; max-height: 250px; overflow-y: auto; padding: 8px; margin-bottom: 8px; }
.chat-msg { margin-bottom: 6px; padding: 6px 10px; border-radius: 6px; font-size: 13px; }
.chat-msg.sent { background: #0096FF; color: #fff; margin-left: 20%; text-align: right; }
.chat-msg.received { background: #1E1E2E; margin-right: 20%; }
.chat-msg.error { background: #3B1010; color: #FF6B6B; }
.chat-msg .meta { font-size: 10px; color: rgba(255,255,255,0.4); margin-top: 2px; }
.input-row { display: flex; gap: 6px; }
.input-row input { flex: 1; background: #1E1E2E; border: 1px solid #333; border-radius: 6px;
padding: 8px 12px; color: #E0E0F0; font-family: inherit; font-size: 13px; }
/* Terminal Modal */
.modal-overlay { display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.85);
z-index:1000; justify-content:center; align-items:center; }
.modal-overlay.open { display:flex; }
.modal-box { background:#0D0D1A; border:1px solid #FFD60A44; border-radius:10px; width:90vw; max-width:860px;
max-height:90vh; display:flex; flex-direction:column; overflow:hidden; }
.modal-header { display:flex; justify-content:space-between; align-items:center; padding:10px 14px;
border-bottom:1px solid #1E1E2E; }
.modal-header h3 { font-size:14px; color:#FFD60A; margin:0; }
.modal-close { background:none; border:none; color:#888; font-size:20px; cursor:pointer; padding:0 6px; }
.modal-close:hover { color:#FF3B30; }
.modal-body { flex:1; padding:4px; min-height:400px; }
.modal-footer { padding:6px 14px; border-top:1px solid #1E1E2E; font-size:11px; color:#8888AA; }
</style>
</head>
<body>
<h1>ARIA Diagnostic</h1>
<!-- Verbindungsstatus -->
<div class="grid">
<div class="card">
<h2>OpenClaw Gateway</h2>
<div class="status-row">
<div class="dot" id="gw-dot"></div>
<span class="status-label" id="gw-status">-</span>
</div>
<div class="error-text" id="gw-error"></div>
<div id="core-auth" style="margin-top:6px;display:none;background:#080810;border:1px solid #1E1E2E;border-radius:4px;padding:6px 8px;font-size:10px;line-height:1.5;max-height:220px;overflow-y:auto;white-space:pre-wrap;color:#8888AA"></div>
<button class="btn secondary" onclick="send({action:'reconnect_gateway'})">Reconnect</button>
<button class="btn secondary" onclick="send({action:'check_core_auth'})">Agent-Auth pruefen</button>
<button class="btn secondary" id="btn-core-term" onclick="openCoreTerminal()">Core Terminal</button>
</div>
<div class="card">
<h2>RVS (Rendezvous)</h2>
<div class="status-row">
<div class="dot" id="rvs-dot"></div>
<span class="status-label" id="rvs-status">-</span>
</div>
<div class="error-text" id="rvs-error"></div>
<button class="btn secondary" onclick="send({action:'reconnect_rvs'})">Reconnect</button>
</div>
<div class="card">
<h2>Claude Proxy</h2>
<div class="status-row">
<div class="dot" id="proxy-dot"></div>
<span class="status-label" id="proxy-status">Nicht getestet</span>
</div>
<div class="error-text" id="proxy-error"></div>
<div id="proxy-models" style="margin-top:6px;display:none">
<div style="font-size:11px;color:#8888AA;margin-bottom:3px">Verfuegbare Modelle:</div>
<div id="proxy-models-list" style="font-size:12px;line-height:1.6"></div>
<div style="font-size:10px;color:#555570;margin-top:4px" id="proxy-models-hint"></div>
</div>
<div id="proxy-auth" style="margin-top:6px;display:none;background:#080810;border:1px solid #1E1E2E;border-radius:4px;padding:6px 8px;font-size:10px;line-height:1.5;max-height:220px;overflow-y:auto;white-space:pre-wrap;color:#8888AA"></div>
<!-- Terminal Modal wird weiter unten definiert -->
<div id="proxy-creds-box" style="margin-top:6px;display:none">
<div style="background:#0A1A1A;border:1px solid #0096FF33;border-radius:6px;padding:8px 10px">
<div style="font-size:11px;color:#0096FF;margin-bottom:4px;font-weight:bold">Credentials einfuegen</div>
<div style="font-size:10px;color:#8888AA;margin-bottom:6px">Auf einem Rechner mit Claude Login: <code style="background:#1E1E2E;padding:1px 4px;border-radius:3px">cat ~/.config/claude/.credentials.json</code><br>Inhalt hier einfuegen:</div>
<textarea id="creds-input" rows="4" style="width:100%;background:#080810;border:1px solid #333;border-radius:4px;padding:6px;color:#E0E0F0;font-family:monospace;font-size:11px;resize:vertical" placeholder='{"claudeAiOauth":{"accessToken":"...","refreshToken":"...","expiresAt":"..."}}'></textarea>
<button class="btn" onclick="submitCredentials()" style="margin-top:4px;padding:4px 12px;font-size:11px">Speichern</button>
<div id="creds-status" style="font-size:11px;margin-top:4px"></div>
</div>
</div>
<button class="btn secondary" id="btn-proxy-test" onclick="testProxyBtn()" style="margin-top:6px">Proxy testen</button>
<button class="btn secondary" onclick="checkProxyAuth()" style="margin-top:6px">Auth pruefen</button>
<button class="btn secondary" id="btn-proxy-login" onclick="startProxyLogin()" style="margin-top:6px">Login starten</button>
<button class="btn secondary" onclick="toggleCredsBox()" style="margin-top:6px">Credentials einfuegen</button>
</div>
</div>
<!-- Chat Test -->
<div class="grid">
<div class="card full">
<h2>Chat Test</h2>
<div class="chat-box" id="chat-box"></div>
<div class="input-row">
<input type="text" id="chat-input" value="aria lebst du noch?" placeholder="Nachricht...">
<button class="btn" id="btn-gw" onclick="testGateway()">Gateway senden</button>
<button class="btn" id="btn-rvs" onclick="testRVS()">Via RVS senden</button>
</div>
</div>
</div>
<!-- Logs mit Tabs -->
<div class="card" style="margin-top:12px; padding: 8px 0 0 0;">
<div style="padding: 0 12px;">
<div class="tab-bar">
<button class="tab-btn active" data-tab="all" onclick="switchTab('all')">Alle <span class="tab-count" id="count-all">0</span></button>
<button class="tab-btn" data-tab="gateway" onclick="switchTab('gateway')">Gateway <span class="tab-count" id="count-gateway">0</span></button>
<button class="tab-btn" data-tab="rvs" onclick="switchTab('rvs')">RVS <span class="tab-count" id="count-rvs">0</span></button>
<button class="tab-btn" data-tab="proxy" onclick="switchTab('proxy')">Proxy <span class="tab-count" id="count-proxy">0</span></button>
<button class="tab-btn" data-tab="bridge" onclick="switchTab('bridge')">Bridge <span class="tab-count" id="count-bridge">0</span></button>
<button class="tab-btn" data-tab="server" onclick="switchTab('server')">Server <span class="tab-count" id="count-server">0</span></button>
<button class="tab-btn" data-tab="pipeline" onclick="switchTab('pipeline')" style="margin-left:auto;border-color:#0096FF44;color:#0096FF">Pipeline <span class="tab-count" id="count-pipeline">0</span></button>
</div>
</div>
<div class="log-panel">
<div class="log-header">
<span>
<button class="btn secondary" id="btn-docker-logs" onclick="loadDockerLogs()" style="padding:4px 10px;font-size:11px;display:none">Docker Logs laden</button>
</span>
<span>
<span class="pause-hint" id="pause-hint">Autoscroll pausiert</span>
<button class="btn secondary" id="btn-scroll" onclick="resumeScroll()" style="display:none;padding:4px 10px;font-size:11px">Nach unten</button>
</span>
</div>
<div class="log-box" id="log-all"></div>
<div class="log-box hidden" id="log-gateway"></div>
<div class="log-box hidden" id="log-rvs"></div>
<div class="log-box hidden" id="log-proxy"></div>
<div class="log-box hidden" id="log-bridge"></div>
<div class="log-box hidden" id="log-server"></div>
<div class="log-box hidden" id="log-pipeline"></div>
</div>
</div>
<!-- Terminal Modal -->
<div class="modal-overlay" id="term-modal">
<div class="modal-box">
<div class="modal-header">
<h3>Claude Login Terminal</h3>
<button class="modal-close" onclick="closeTermModal()">&times;</button>
</div>
<div class="modal-body" id="terminal-container"></div>
<div class="modal-footer" id="term-status"></div>
</div>
</div>
<script>
const chatBox = document.getElementById('chat-box');
const pauseHint = document.getElementById('pause-hint');
const btnScroll = document.getElementById('btn-scroll');
let ws;
let activeTab = 'all';
const DOCKER_TABS = ['gateway', 'proxy', 'bridge'];
const autoScroll = { all: true, gateway: true, rvs: true, proxy: true, bridge: true, server: true, pipeline: true };
const logCounts = { all: 0, gateway: 0, rvs: 0, proxy: 0, bridge: 0, server: 0, pipeline: 0 };
const logBoxes = {
all: document.getElementById('log-all'),
gateway: document.getElementById('log-gateway'),
rvs: document.getElementById('log-rvs'),
proxy: document.getElementById('log-proxy'),
bridge: document.getElementById('log-bridge'),
server: document.getElementById('log-server'),
pipeline: document.getElementById('log-pipeline'),
};
// Scroll-Pause pro aktivem Tab
Object.entries(logBoxes).forEach(([tab, box]) => {
box.addEventListener('scroll', () => {
const atBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 30;
autoScroll[tab] = atBottom;
if (tab === activeTab) {
pauseHint.classList.toggle('visible', !atBottom);
btnScroll.style.display = atBottom ? 'none' : 'inline';
}
});
});
function resumeScroll() {
autoScroll[activeTab] = true;
const box = logBoxes[activeTab];
box.scrollTop = box.scrollHeight;
pauseHint.classList.remove('visible');
btnScroll.style.display = 'none';
}
function switchTab(tab) {
activeTab = tab;
document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === tab));
Object.entries(logBoxes).forEach(([t, box]) => box.classList.toggle('hidden', t !== tab));
// Docker Logs Button nur bei Tabs mit Containern
document.getElementById('btn-docker-logs').style.display = DOCKER_TABS.includes(tab) ? 'inline' : 'none';
// Pause-Hinweis aktualisieren
const box = logBoxes[tab];
const atBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 30;
pauseHint.classList.toggle('visible', !atBottom);
btnScroll.style.display = atBottom ? 'none' : 'inline';
if (autoScroll[tab]) box.scrollTop = box.scrollHeight;
}
function updateCount(source) {
logCounts.all++;
document.getElementById('count-all').textContent = logCounts.all;
// source → tab mapping
const tab = mapSourceToTab(source);
if (tab && logCounts[tab] !== undefined) {
logCounts[tab]++;
document.getElementById(`count-${tab}`).textContent = logCounts[tab];
}
}
function mapSourceToTab(source) {
if (source === 'gateway') return 'gateway';
if (source === 'rvs') return 'rvs';
if (source === 'proxy') return 'proxy';
if (source === 'bridge') return 'bridge';
if (source === 'server' || source === 'browser') return 'server';
if (source === 'pipeline') return 'pipeline';
return null;
}
const STATUS_LABELS = {
connected: 'Verbunden',
connecting: 'Verbinde...',
testing: 'Teste...',
disconnected: 'Getrennt',
error: 'Fehler',
not_configured: 'Nicht konfiguriert',
unknown: 'Nicht getestet',
};
function connectWS() {
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
ws = new WebSocket(`${proto}://${location.host}`);
ws.onopen = () => addLog('info', 'browser', 'Verbunden mit Diagnostic Server');
ws.onclose = () => {
addLog('warn', 'browser', 'Verbindung zum Diagnostic Server verloren — Reconnect in 2s');
setTimeout(connectWS, 2000);
};
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === 'init') {
updateState(msg.state);
msg.logs.forEach(entry => addLog(entry.level, entry.source, entry.message, entry.ts));
return;
}
if (msg.type === 'state') { updateState(msg.state); return; }
if (msg.type === 'log') { addLog(msg.entry.level, msg.entry.source, msg.entry.message, msg.entry.ts); return; }
if (msg.type === 'chat_final') {
addChat('received', msg.text, 'chat:final');
return;
}
if (msg.type === 'chat_delta') { return; }
if (msg.type === 'chat_error') {
addChat('error', msg.error, 'chat:error');
return;
}
if (msg.type === 'rvs_chat') {
const p = msg.msg.payload || {};
addChat('received', p.text || '?', `via RVS (${p.sender || '?'})`);
return;
}
if (msg.type === 'proxy_result') {
if (msg.ok) {
addChat('received', msg.reply, 'Claude Proxy direkt');
} else {
addChat('error', msg.error, 'Claude Proxy Fehler');
}
if (msg.models && msg.models.length) showProxyModels(msg.models);
return;
}
if (msg.type === 'proxy_auth') {
const el = document.getElementById('proxy-auth');
el.style.display = 'block';
el.textContent = msg.error ? `Fehler: ${msg.error}` : msg.info;
return;
}
if (msg.type === 'core_auth') {
const el = document.getElementById('core-auth');
el.style.display = 'block';
el.textContent = msg.error ? `Fehler: ${msg.error}` : msg.info;
return;
}
if (msg.type === 'term_ready') {
document.getElementById('term-status').textContent = 'Verbunden — interaktives Terminal';
if (term) term.writeln('\x1b[32mVerbunden!\x1b[0m\r\n');
return;
}
if (msg.type === 'term_data') {
if (term) {
// base64 → Uint8Array (UTF-8 safe)
const raw = atob(msg.data);
const bytes = new Uint8Array(raw.length);
for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
term.write(bytes);
}
return;
}
if (msg.type === 'term_exit') {
if (term) term.writeln('\r\n\x1b[33m--- Session beendet ---\x1b[0m');
document.getElementById('term-status').textContent = 'Session beendet — Fenster schliessen oder erneut einloggen';
return;
}
if (msg.type === 'term_error') {
if (term) term.writeln('\r\n\x1b[31mFehler: ' + msg.error + '\x1b[0m');
document.getElementById('btn-proxy-login').disabled = false;
return;
}
if (msg.type === 'login_status') {
if (msg.status === 'done') {
const cs = document.getElementById('creds-status');
if (cs) { cs.textContent = 'Erfolgreich!'; cs.style.color = '#34C759'; }
} else if (msg.status === 'error') {
const cs = document.getElementById('creds-status');
if (cs) { cs.textContent = msg.error; cs.style.color = '#FF6B6B'; }
}
return;
}
if (msg.type === 'docker_logs') {
showDockerLogs(msg);
return;
}
if (msg.type === 'response') { return; }
};
}
function send(obj) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(obj));
}
}
function testGateway() {
const text = document.getElementById('chat-input').value.trim();
if (!text) return;
addChat('sent', text, 'Gateway direkt');
send({ action: 'test_gateway', text });
}
function testRVS() {
const text = document.getElementById('chat-input').value.trim();
if (!text) return;
addChat('sent', text, 'via RVS');
send({ action: 'test_rvs', text });
}
function testProxyBtn() {
send({ action: 'test_proxy', text: 'Antworte mit genau einem Wort: Ping' });
}
function checkProxyAuth() {
send({ action: 'check_proxy_auth' });
}
let term = null;
let termFitAddon = null;
let termAction = null; // Welche Aktion beim Terminal-Start gesendet wird
function openTermModal(title, action) {
termAction = action;
document.querySelector('#term-modal .modal-header h3').textContent = title;
document.getElementById('term-modal').classList.add('open');
document.getElementById('term-status').textContent = 'Starte Terminal...';
if (typeof Terminal === 'undefined') {
const s = document.createElement('script');
s.src = 'https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js';
s.onload = () => {
const s2 = document.createElement('script');
s2.src = 'https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js';
s2.onload = () => initTerminal();
document.head.appendChild(s2);
};
document.head.appendChild(s);
} else {
initTerminal();
}
}
function startProxyLogin() {
document.getElementById('btn-proxy-login').disabled = true;
openTermModal('Claude Login Terminal (aria-proxy)', { action: 'proxy_login' });
}
function openCoreTerminal() {
document.getElementById('btn-core-term').disabled = true;
openTermModal('aria-core Shell', { action: 'core_terminal' });
}
function closeTermModal() {
document.getElementById('term-modal').classList.remove('open');
document.getElementById('btn-proxy-login').disabled = false;
document.getElementById('btn-core-term').disabled = false;
// Terminal aufraeumen
if (term) { term.dispose(); term = null; }
}
function initTerminal() {
const container = document.getElementById('terminal-container');
container.innerHTML = '';
term = new Terminal({
cursorBlink: true,
fontSize: 14,
fontFamily: "'DejaVu Sans Mono', 'Courier New', monospace",
theme: { background: '#000000', foreground: '#E0E0F0' },
});
if (window.FitAddon) {
termFitAddon = new FitAddon.FitAddon();
term.loadAddon(termFitAddon);
term.open(container);
termFitAddon.fit();
} else {
term.open(container);
}
// Tastatureingabe → Server (UTF-8 safe)
term.onData((data) => {
const encoder = new TextEncoder();
const bytes = encoder.encode(data);
let b64 = '';
for (let i = 0; i < bytes.length; i++) b64 += String.fromCharCode(bytes[i]);
send({ action: 'term_input', data: btoa(b64) });
});
const containerName = termAction?.action === 'core_terminal' ? 'aria-core' : 'aria-proxy';
term.writeln('\x1b[33mVerbinde mit ' + containerName + '...\x1b[0m');
send(termAction);
}
// Resize bei Fensteraenderung
window.addEventListener('resize', () => { if (termFitAddon && term) termFitAddon.fit(); });
function toggleCredsBox() {
const box = document.getElementById('proxy-creds-box');
box.style.display = box.style.display === 'none' ? 'block' : 'none';
}
function submitCredentials() {
const input = document.getElementById('creds-input').value.trim();
const status = document.getElementById('creds-status');
if (!input) { status.textContent = 'Bitte JSON einfuegen'; status.style.color = '#FF6B6B'; return; }
try {
JSON.parse(input);
} catch (e) {
status.textContent = 'Ungueltig: Kein gueltiges JSON'; status.style.color = '#FF6B6B'; return;
}
status.textContent = 'Speichere...'; status.style.color = '#FFD60A';
send({ action: 'write_credentials', credentials: input });
}
function loadDockerLogs() {
if (!DOCKER_TABS.includes(activeTab)) return;
send({ action: 'docker_logs', tab: activeTab, tail: 200 });
}
function updateState(state) {
// Gateway
const gw = state.gateway || {};
document.getElementById('gw-dot').className = `dot ${gw.status || 'disconnected'}`;
document.getElementById('gw-status').textContent =
(STATUS_LABELS[gw.status] || gw.status) + (gw.handshakeOk ? ' (Handshake OK)' : '');
document.getElementById('gw-error').textContent = gw.lastError || '';
// RVS
const rvs = state.rvs || {};
document.getElementById('rvs-dot').className = `dot ${rvs.status || 'disconnected'}`;
document.getElementById('rvs-status').textContent = STATUS_LABELS[rvs.status] || rvs.status;
document.getElementById('rvs-error').textContent = rvs.lastError || '';
// Proxy
const proxy = state.proxy || {};
document.getElementById('proxy-dot').className = `dot ${proxy.status || 'unknown'}`;
document.getElementById('proxy-status').textContent = STATUS_LABELS[proxy.status] || proxy.status;
document.getElementById('proxy-error').textContent = proxy.lastError || '';
document.getElementById('btn-proxy-test').disabled = proxy.status === 'testing';
if (proxy.models && proxy.models.length) showProxyModels(proxy.models);
// Buttons
document.getElementById('btn-gw').disabled = gw.status !== 'connected';
document.getElementById('btn-rvs').disabled = rvs.status !== 'connected';
}
function addLog(level, source, message, ts) {
const time = ts ? new Date(ts).toLocaleTimeString('de-DE') : new Date().toLocaleTimeString('de-DE');
const line = `${time} [${source}] ${message}`;
// Pipeline-Eintraege nur in Pipeline-Tab (nicht in Alle)
if (source === 'pipeline') {
const pipeLevel = level === 'error' ? 'pipeline-err' : level === 'info' && message.includes('>>>') ? 'pipeline-ok' : 'pipeline-step';
appendToLog('pipeline', pipeLevel, `${time} ${message}`);
logCounts.pipeline++;
document.getElementById('count-pipeline').textContent = logCounts.pipeline;
return;
}
// In "Alle" und in den passenden Tab schreiben
appendToLog('all', level, line);
const tab = mapSourceToTab(source);
if (tab) appendToLog(tab, level, line);
updateCount(source);
}
function appendToLog(tab, level, line) {
const box = logBoxes[tab];
const el = document.createElement('div');
el.className = `log-entry ${level}`;
el.textContent = line;
box.appendChild(el);
if (autoScroll[tab]) box.scrollTop = box.scrollHeight;
}
function addChat(type, text, meta) {
const el = document.createElement('div');
el.className = `chat-msg ${type}`;
el.innerHTML = `${escapeHtml(text)}<div class="meta">${escapeHtml(meta)}${new Date().toLocaleTimeString('de-DE')}</div>`;
chatBox.appendChild(el);
chatBox.scrollTop = chatBox.scrollHeight;
}
function showDockerLogs(msg) {
const tab = msg.tab;
const box = logBoxes[tab];
if (!box) return;
if (msg.error) {
appendToLog(tab, 'error', `Docker Logs Fehler: ${msg.error}`);
return;
}
// Bestehende Eintraege leeren und Docker-Logs einfuegen
box.innerHTML = '';
const header = document.createElement('div');
header.className = 'log-entry info';
header.textContent = `── Docker Logs: ${msg.container} (letzte ${msg.lines.length} Zeilen) ──`;
box.appendChild(header);
for (const line of msg.lines) {
const el = document.createElement('div');
el.className = 'log-entry ' + (line.match(/\b(error|Error|ERROR)\b/) ? 'error' :
line.match(/\b(warn|Warning|WARN)\b/) ? 'warn' : 'info');
el.textContent = line;
box.appendChild(el);
}
// Zähler aktualisieren
logCounts[tab] = msg.lines.length;
const countEl = document.getElementById(`count-${tab}`);
if (countEl) countEl.textContent = logCounts[tab];
box.scrollTop = box.scrollHeight;
autoScroll[tab] = true;
}
function showProxyModels(models) {
const container = document.getElementById('proxy-models');
const list = document.getElementById('proxy-models-list');
const hint = document.getElementById('proxy-models-hint');
container.style.display = 'block';
list.innerHTML = models.map(m => {
const clean = m.replace('openai/', '');
return `<div style="display:inline-block;background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:2px 8px;margin:2px;font-size:11px">${escapeHtml(m)}</div>`;
}).join('');
const cleanNames = models.map(m => m.replace('openai/', ''));
hint.textContent = `DEFAULT_MODEL fuer docker-compose.yml: ${cleanNames.join(' | ')}`;
}
function escapeHtml(str) {
return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
// Enter-Taste sendet via Gateway
document.getElementById('chat-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter') testGateway();
});
connectWS();
</script>
</body>
</html>