157 lines
6.8 KiB
HTML
157 lines
6.8 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>Pi-Orchestrator AI</title>
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/split.js/1.6.0/split.min.js"></script>
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.1.0/css/xterm.css" />
|
||
<script src="https://cdn.jsdelivr.net/npm/xterm@5.1.0/lib/xterm.js"></script>
|
||
|
||
<style>
|
||
/* Styles für die ziehbaren Trennbalken (Gutters) */
|
||
.gutter {
|
||
background-color: #2d3748;
|
||
background-repeat: no-repeat;
|
||
background-position: center;
|
||
}
|
||
.gutter.gutter-horizontal {
|
||
cursor: col-resize;
|
||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAG8aZGCAwMA6gBVHicPXM2nPRDAADy9GAVu4E9ngAAAABJRU5ErkJggg==');
|
||
}
|
||
.gutter.gutter-vertical {
|
||
cursor: row-resize;
|
||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABjU9S9AAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=');
|
||
}
|
||
.split { display: flex; flex-direction: row; }
|
||
#upper-area { display: flex; flex-direction: row; width: 100%; }
|
||
#lower-area { display: flex; flex-direction: row; width: 100%; }
|
||
</style>
|
||
</head>
|
||
<body class="bg-gray-900 text-white h-screen flex flex-col overflow-hidden">
|
||
|
||
<div id="vertical-split" class="flex flex-col flex-1">
|
||
|
||
<div id="upper-area" class="flex">
|
||
<div id="sidebar" class="bg-gray-800 p-4 overflow-y-auto">
|
||
<h2 class="text-xl font-bold mb-4">📍 Nodes</h2>
|
||
<div id="node-list" class="space-y-2">
|
||
{% for node in nodes %}
|
||
<div class="p-3 bg-gray-700 rounded border border-gray-600 relative group">
|
||
<form action="/remove_node/{{ node.id }}" method="post" class="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||
<button type="submit" class="text-red-500 font-bold px-1">×</button>
|
||
</form>
|
||
<div class="text-sm font-bold">{{ node.name }}</div>
|
||
<div class="text-[10px] text-gray-400">{{ node.ip }}</div>
|
||
<button onclick="openTerminal('{{ node.ip }}')" class="mt-2 text-[10px] bg-blue-600 hover:bg-blue-500 px-2 py-1 rounded w-full">Terminal</button>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
<button onclick="addNode()" class="mt-4 w-full bg-green-700 p-2 rounded text-sm">+ Add</button>
|
||
</div>
|
||
|
||
<div id="chat-area" class="flex flex-col bg-gray-900">
|
||
<div id="chat-window" class="flex-1 p-4 overflow-y-auto space-y-2">
|
||
<div class="text-gray-500 italic text-sm">System bereit...</div>
|
||
</div>
|
||
<div class="p-3 border-t border-gray-700 flex bg-gray-800">
|
||
<input id="user-input" type="text" class="flex-1 bg-gray-700 p-2 rounded-l outline-none text-sm" placeholder="KI Befehl...">
|
||
<button onclick="sendMessage()" class="bg-blue-600 px-4 rounded-r">Send</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="lower-area" class="flex bg-black">
|
||
<div id="log-area" class="p-2 font-mono text-xs text-green-400 overflow-y-auto border-r border-gray-800">
|
||
<div class="text-gray-600 border-b border-gray-800 mb-1 uppercase tracking-tighter">System Logs</div>
|
||
<div id="install-log"></div>
|
||
</div>
|
||
|
||
<div id="terminal-area" class="p-1">
|
||
<div id="terminal" class="h-full w-full"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// --- Split.js Initialisierung ---
|
||
|
||
// 1. Vertikaler Split (Oben vs Unten)
|
||
Split(['#upper-area', '#lower-area'], {
|
||
direction: 'vertical',
|
||
sizes: [65, 35], // 65% Oben, 35% Unten
|
||
minSize: 100,
|
||
gutterSize: 8,
|
||
});
|
||
|
||
// 2. Horizontaler Split Oben (Sidebar vs Chat)
|
||
Split(['#sidebar', '#chat-area'], {
|
||
sizes: [20, 80],
|
||
minSize: [150, 300],
|
||
gutterSize: 8,
|
||
});
|
||
|
||
// 3. Horizontaler Split Unten (Logs vs Terminal)
|
||
Split(['#log-area', '#terminal-area'], {
|
||
sizes: [30, 70],
|
||
minSize: [100, 300],
|
||
gutterSize: 8,
|
||
onDrag: () => {
|
||
// Terminal an neue Größe anpassen (falls Fit-Addon genutzt wird)
|
||
}
|
||
});
|
||
|
||
// --- Terminal & WebSockets ---
|
||
const term = new Terminal({ theme: { background: '#000' }, cursorBlink: true, fontSize: 13 });
|
||
term.open(document.getElementById('terminal'));
|
||
|
||
let termWs = null;
|
||
function openTerminal(ip) {
|
||
if(termWs) termWs.close();
|
||
term.clear();
|
||
term.write(`\r\n>>> Verbinde mit ${ip}...\r\n`);
|
||
termWs = new WebSocket(`ws://${location.host}/ws/terminal/${ip}`);
|
||
termWs.onmessage = (ev) => term.write(ev.data);
|
||
term.onData(data => termWs.send(data));
|
||
}
|
||
|
||
const logWs = new WebSocket(`ws://${location.host}/ws/install_logs`);
|
||
logWs.onmessage = (ev) => {
|
||
const l = document.getElementById('install-log');
|
||
l.innerHTML += `<div>${ev.data}</div>`;
|
||
l.parentElement.scrollTop = l.parentElement.scrollHeight;
|
||
};
|
||
|
||
const chatWs = new WebSocket(`ws://${location.host}/ws/chat`);
|
||
chatWs.onmessage = (ev) => appendChat("Bot", ev.data, "text-blue-400");
|
||
|
||
function sendMessage() {
|
||
const i = document.getElementById('user-input');
|
||
chatWs.send(i.value);
|
||
appendChat("Du", i.value, "text-white");
|
||
i.value = '';
|
||
}
|
||
|
||
function appendChat(user, msg, color) {
|
||
const div = document.createElement('div');
|
||
div.className = "text-sm border-l-2 border-gray-700 pl-2 py-1";
|
||
div.innerHTML = `<span class="${color} font-bold">${user}:</span> ${msg}`;
|
||
document.getElementById('chat-window').appendChild(div);
|
||
}
|
||
|
||
async function addNode() {
|
||
const name = prompt("Name:");
|
||
const ip = prompt("IP:");
|
||
const user = prompt("User:", "pi");
|
||
const pass = prompt("Passwort:");
|
||
if(name && ip && pass) {
|
||
const fd = new FormData();
|
||
fd.append('name', name); fd.append('ip', ip);
|
||
fd.append('user', user); fd.append('password', pass);
|
||
await fetch('/add_node', { method: 'POST', body: fd });
|
||
location.reload();
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |