Spaces:
Sleeping
Sleeping
| <html lang="de"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Mineflayer Bot Manager - Hubging Face Spaces</title> | |
| <!-- Tailwind CSS --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- FontAwesome Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <!-- Google Fonts Inter --> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet"> | |
| <style> | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| } | |
| .terminal-font { | |
| font-family: 'Fira Code', monospace; | |
| } | |
| /* Custom Scrollbar for Terminal */ | |
| .custom-scrollbar::-webkit-scrollbar { | |
| width: 6px; | |
| height: 6px; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar-track { | |
| background: #18181b; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar-thumb { | |
| background: #3f3f46; | |
| border-radius: 3px; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar-thumb:hover { | |
| background: #52525b; | |
| } | |
| @keyframes pulse-slow { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.4; } | |
| } | |
| .pulse-animation { | |
| animation: pulse-slow 2s infinite; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-slate-950 text-slate-100 min-h-screen flex flex-col"> | |
| <!-- Header --> | |
| <header class="border-b border-slate-800 bg-slate-900/50 backdrop-blur sticky top-0 z-50"> | |
| <div class="max-w-7xl mx-auto px-4 py-4 sm:px-6 lg:px-8 flex items-center justify-between"> | |
| <div class="flex items-center space-x-3"> | |
| <div class="bg-emerald-500 text-slate-950 p-2 rounded-lg font-bold shadow-lg shadow-emerald-500/20"> | |
| <i class="fa-solid fa-robot text-xl"></i> | |
| </div> | |
| <div> | |
| <h1 class="text-xl font-bold tracking-tight text-white flex items-center gap-2"> | |
| Mineflayer Bot Manager | |
| <span class="text-xs bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-2 py-0.5 rounded-full">v1.0.0</span> | |
| </h1> | |
| <p class="text-xs text-slate-400">Robustes Web-Dashboard für Minecraft AFK-Bots</p> | |
| </div> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <!-- HF Status badge --> | |
| <span class="flex items-center gap-1.5 text-xs bg-slate-800 border border-slate-700 px-3 py-1.5 rounded-lg text-slate-300"> | |
| <span class="h-2 w-2 rounded-full bg-green-500"></span> | |
| HF Spaces Ready (Port 7860) | |
| </span> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main class="flex-grow max-w-7xl w-full mx-auto px-4 py-6 sm:px-6 lg:px-8 grid grid-cols-1 lg:grid-cols-12 gap-6"> | |
| <!-- Left Column: Bot Spawner & Bot List (5 cols) --> | |
| <section class="lg:col-span-5 flex flex-col space-y-6"> | |
| <!-- Bot Spawner Card --> | |
| <div class="bg-slate-900 border border-slate-800 rounded-xl shadow-xl overflow-hidden"> | |
| <div class="border-b border-slate-800 bg-slate-900/80 px-5 py-4 flex items-center space-x-2"> | |
| <i class="fa-solid fa-plus-circle text-emerald-400"></i> | |
| <h2 class="font-semibold text-white">Neuen Bot erstellen</h2> | |
| </div> | |
| <form id="bot-form" class="p-5 space-y-4"> | |
| <div class="grid grid-cols-3 gap-3"> | |
| <div class="col-span-2"> | |
| <label class="block text-xs font-medium text-slate-400 mb-1">Server IP *</label> | |
| <input type="text" id="host" required placeholder="z.B. play.server.de" | |
| class="w-full bg-slate-950 border border-slate-700 rounded-lg px-3 py-2 text-sm text-white placeholder-slate-500 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition"> | |
| </div> | |
| <div> | |
| <label class="block text-xs font-medium text-slate-400 mb-1">Port</label> | |
| <input type="number" id="port" value="25565" placeholder="25565" | |
| class="w-full bg-slate-950 border border-slate-700 rounded-lg px-3 py-2 text-sm text-white placeholder-slate-500 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition"> | |
| </div> | |
| </div> | |
| <div class="flex items-center justify-between"> | |
| <button type="button" id="ping-btn" | |
| class="text-xs bg-slate-800 hover:bg-slate-700 border border-slate-700 text-slate-300 px-3 py-1.5 rounded-lg transition flex items-center gap-1.5"> | |
| <i class="fa-solid fa-wifi"></i> | |
| <span>Server testen</span> | |
| </button> | |
| <div id="ping-result" class="text-xs text-slate-500"></div> | |
| </div> | |
| <div> | |
| <label class="block text-xs font-medium text-slate-400 mb-1">Bot-Name * <span id="name-hint" class="text-blue-400 hidden">(Microsoft-Email)</span></label> | |
| <input type="text" id="username" required placeholder="z.B. AFK_Bot_1" | |
| class="w-full bg-slate-950 border border-slate-700 rounded-lg px-3 py-2 text-sm text-white placeholder-slate-500 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition"> | |
| </div> | |
| <div> | |
| <label class="block text-xs font-medium text-slate-400 mb-1">Startup-Command (Befehl nach Login)</label> | |
| <div class="relative"> | |
| <span class="absolute inset-y-0 left-0 pl-3 flex items-center text-slate-500 font-mono text-xs">/</span> | |
| <input type="text" id="startupCommand" placeholder="register passwort123 passwort123" | |
| class="w-full bg-slate-950 border border-slate-700 rounded-lg pl-6 pr-3 py-2 text-sm text-white placeholder-slate-500 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition"> | |
| </div> | |
| <p class="text-[10px] text-slate-500 mt-1">Wird ca. 2.5 Sekunden nach dem Spawn ausgeführt. Schrägstrich "/" weglassen.</p> | |
| </div> | |
| <div> | |
| <label class="block text-xs font-medium text-slate-400 mb-1">Minecraft-Version</label> | |
| <select id="version" | |
| class="w-full bg-slate-950 border border-slate-700 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition"> | |
| <option value="">Automatisch erkennen (Empfohlen)</option> | |
| <option value="1.21.4">1.21.4 (Latest)</option> | |
| <option value="1.21.3">1.21.3</option> | |
| <option value="1.21.1">1.21.1</option> | |
| <option value="1.20.4">1.20.4</option> | |
| <option value="1.20.1">1.20.1</option> | |
| <option value="1.19.4">1.19.4</option> | |
| <option value="1.19.2">1.19.2</option> | |
| <option value="1.18.2">1.18.2</option> | |
| <option value="1.17.1">1.17.1</option> | |
| <option value="1.16.5">1.16.5</option> | |
| <option value="1.12.2">1.12.2</option> | |
| <option value="1.8.9">1.8.9</option> | |
| </select> | |
| </div> | |
| <!-- Microsoft Account Toggle --> | |
| <div class="flex items-center justify-between bg-slate-950 border border-slate-700 rounded-lg px-4 py-3"> | |
| <div class="flex items-center space-x-2"> | |
| <i class="fa-brands fa-microsoft text-blue-500"></i> | |
| <span class="text-sm text-white font-medium">Microsoft-Konto</span> | |
| <span class="text-[10px] bg-blue-500/10 text-blue-400 border border-blue-500/20 px-1.5 py-0.5 rounded">Auth</span> | |
| </div> | |
| <label class="relative inline-flex items-center cursor-pointer"> | |
| <input type="checkbox" id="use-microsoft" class="sr-only peer"> | |
| <div class="w-9 h-5 bg-slate-700 rounded-full peer peer-checked:bg-blue-600 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all"></div> | |
| </label> | |
| </div> | |
| <p id="msa-info" class="text-[10px] text-slate-500 -mt-2 hidden">Der Bot verwendet Device-Code-Flow. Einmaliger Login nötig, Token wird gecached.</p> | |
| <button type="submit" | |
| class="w-full bg-emerald-500 hover:bg-emerald-600 active:bg-emerald-700 text-slate-950 font-semibold py-2 px-4 rounded-lg shadow-lg hover:shadow-emerald-500/10 transition flex items-center justify-center space-x-2"> | |
| <i class="fa-solid fa-play"></i> | |
| <span>Bot starten & verbinden</span> | |
| </button> | |
| </form> | |
| </div> | |
| <!-- Bot List Card --> | |
| <div class="bg-slate-900 border border-slate-800 rounded-xl shadow-xl overflow-hidden flex-grow flex flex-col min-h-[300px]"> | |
| <div class="border-b border-slate-800 bg-slate-900/80 px-5 py-4 flex items-center justify-between"> | |
| <div class="flex items-center space-x-2"> | |
| <i class="fa-solid fa-list text-indigo-400"></i> | |
| <h2 class="font-semibold text-white">Aktive Bots</h2> | |
| </div> | |
| <span id="active-bots-count" class="text-xs bg-slate-800 text-slate-400 px-2 py-0.5 rounded-full border border-slate-700"> | |
| 0 Bots | |
| </span> | |
| </div> | |
| <div id="bot-list-container" class="p-4 space-y-3 overflow-y-auto max-h-[350px] flex-grow custom-scrollbar"> | |
| <!-- Fallback empty state --> | |
| <div id="no-bots" class="text-center py-10 text-slate-500"> | |
| <i class="fa-solid fa-robot text-4xl mb-3 opacity-30"></i> | |
| <p class="text-sm">Keine Bots aktiv.</p> | |
| <p class="text-xs mt-1">Erstelle oben einen Bot, um zu starten!</p> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Right Column: Logs, Console & Control Panel (7 cols) --> | |
| <section class="lg:col-span-7 flex flex-col space-y-6"> | |
| <!-- Terminal & Console Card --> | |
| <div class="bg-slate-900 border border-slate-800 rounded-xl shadow-xl overflow-hidden flex flex-col flex-grow h-[650px]"> | |
| <!-- Terminal Header --> | |
| <div class="border-b border-slate-800 bg-slate-900/80 px-5 py-3 flex items-center justify-between"> | |
| <div class="flex items-center space-x-3"> | |
| <div class="flex space-x-1.5"> | |
| <span class="w-3 h-3 rounded-full bg-red-500/70 inline-block"></span> | |
| <span class="w-3 h-3 rounded-full bg-yellow-500/70 inline-block"></span> | |
| <span class="w-3 h-3 rounded-full bg-green-500/70 inline-block"></span> | |
| </div> | |
| <span class="text-xs text-slate-400 font-mono">|</span> | |
| <div class="flex items-center space-x-2"> | |
| <i class="fa-solid fa-terminal text-emerald-400 text-sm"></i> | |
| <span id="terminal-title" class="text-sm font-semibold text-white font-mono">Wähle einen Bot aus</span> | |
| </div> | |
| </div> | |
| <div class="flex items-center space-x-2"> | |
| <!-- Clear Logs Button --> | |
| <button id="clear-logs-btn" class="text-slate-400 hover:text-white bg-slate-800 hover:bg-slate-700 p-1.5 rounded-md transition text-xs flex items-center gap-1.5"> | |
| <i class="fa-solid fa-trash-can"></i> | |
| <span>Leeren</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Terminal Output --> | |
| <div id="terminal" class="flex-grow bg-slate-950 p-5 overflow-y-auto custom-scrollbar terminal-font text-xs space-y-2 select-text"> | |
| <div class="text-slate-500 italic">Willkommen im Mineflayer Bot Manager Terminal. Bitte wähle links einen Bot aus oder erstelle einen neuen, um Echtzeit-Logs und Chat-Nachrichten anzuzeigen.</div> | |
| </div> | |
| <!-- Terminal Chat Input / Direct Control --> | |
| <div class="border-t border-slate-800 p-4 bg-slate-900/50"> | |
| <form id="chat-form" class="flex items-center space-x-2"> | |
| <span class="text-slate-500 font-mono text-sm pl-2">➔</span> | |
| <input type="text" id="chat-input" disabled placeholder="Wähle einen Bot aus, um Befehle oder Chat zu senden..." | |
| class="flex-grow bg-slate-950 border border-slate-700 rounded-lg px-4 py-2.5 text-sm text-white placeholder-slate-500 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition disabled:opacity-50 disabled:cursor-not-allowed"> | |
| <button type="submit" id="chat-send-btn" disabled | |
| class="bg-emerald-500 hover:bg-emerald-600 active:bg-emerald-700 text-slate-950 px-4 py-2.5 rounded-lg font-semibold transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1"> | |
| <i class="fa-solid fa-paper-plane"></i> | |
| <span class="hidden sm:inline">Senden</span> | |
| </button> | |
| </form> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| <!-- Footer --> | |
| <footer class="mt-auto border-t border-slate-800 py-4 bg-slate-950/80"> | |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center text-xs text-slate-500 flex flex-col sm:flex-row justify-between items-center gap-2"> | |
| <p>Entwickelt für absolute Fehlerresistenz mit automatischer 5-Sekunden-Wiederverbindung.</p> | |
| <p>© 2026 AFK Bot Dashboard • Mineflayer & Socket.io</p> | |
| </div> | |
| </footer> | |
| <!-- Microsoft Device Code Modal --> | |
| <div id="msa-modal" class="hidden fixed inset-0 z-[100] flex items-center justify-center bg-slate-950/80 backdrop-blur-sm"> | |
| <div class="bg-slate-900 border border-slate-700/50 rounded-xl p-6 max-w-md w-full mx-4 shadow-2xl shadow-blue-500/5"> | |
| <div class="text-center"> | |
| <div class="w-16 h-16 mx-auto mb-4 bg-blue-500/10 rounded-full flex items-center justify-center"> | |
| <i class="fa-brands fa-microsoft text-3xl text-blue-400"></i> | |
| </div> | |
| <h3 class="text-lg font-semibold text-white mb-2">Microsoft Authentifizierung</h3> | |
| <p class="text-sm text-slate-400 mb-4">Öffne den Link in deinem Browser und gib den Code ein, um dich mit deinem Microsoft-Konto anzumelden.</p> | |
| <div class="bg-slate-950 rounded-lg p-4 mb-4 border border-slate-700/50"> | |
| <p class="text-xs text-slate-500 mb-2">Link zum Öffnen:</p> | |
| <a id="msa-url" href="#" target="_blank" class="text-blue-400 hover:text-blue-300 underline break-all text-sm font-medium"></a> | |
| <p class="text-xs text-slate-500 mt-4 mb-2">Anmeldecode:</p> | |
| <p id="msa-code" class="text-3xl font-bold text-white tracking-[0.3em] select-all"></p> | |
| </div> | |
| <p class="text-xs text-slate-500">Nach erfolgreicher Anmeldung verbindet sich der Bot automatisch. Die eingegebenen Anmeldedaten werden im Auth-Cache gespeichert.</p> | |
| <button id="msa-modal-close" class="mt-4 text-sm text-slate-400 hover:text-white bg-slate-800 hover:bg-slate-700 px-4 py-2 rounded-lg transition"> | |
| Modal schließen | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Socket.io library --> | |
| <script src="/socket.io/socket.io.js"></script> | |
| <script> | |
| // Global State | |
| const socket = io(); | |
| const bots = new Map(); // username -> { status, config, logs: [] } | |
| let selectedBot = null; // username or null | |
| // DOM Elements | |
| const botForm = document.getElementById('bot-form'); | |
| const botListContainer = document.getElementById('bot-list-container'); | |
| const noBotsEl = document.getElementById('no-bots'); | |
| const activeBotsCountEl = document.getElementById('active-bots-count'); | |
| const terminal = document.getElementById('terminal'); | |
| const terminalTitle = document.getElementById('terminal-title'); | |
| const clearLogsBtn = document.getElementById('clear-logs-btn'); | |
| const chatForm = document.getElementById('chat-form'); | |
| const chatInput = document.getElementById('chat-input'); | |
| const chatSendBtn = document.getElementById('chat-send-btn'); | |
| // UI Toast Notification helper | |
| function showToast(message, type = 'error') { | |
| const toast = document.createElement('div'); | |
| toast.className = `fixed bottom-5 right-5 z-50 flex items-center gap-2.5 px-4 py-3 rounded-lg border shadow-2xl transition-all duration-300 transform translate-y-10 opacity-0`; | |
| if (type === 'error') { | |
| toast.className += ' bg-red-950 border-red-500/30 text-red-200'; | |
| toast.innerHTML = `<i class="fa-solid fa-circle-exclamation text-red-500"></i><span>${message}</span>`; | |
| } else { | |
| toast.className += ' bg-emerald-950 border-emerald-500/30 text-emerald-200'; | |
| toast.innerHTML = `<i class="fa-solid fa-circle-check text-emerald-500"></i><span>${message}</span>`; | |
| } | |
| document.body.appendChild(toast); | |
| // Animate in | |
| setTimeout(() => { | |
| toast.classList.remove('translate-y-10', 'opacity-0'); | |
| }, 50); | |
| // Remove | |
| setTimeout(() => { | |
| toast.classList.add('translate-y-10', 'opacity-0'); | |
| setTimeout(() => toast.remove(), 300); | |
| }, 4000); | |
| } | |
| // Connect form submit | |
| botForm.addEventListener('submit', (e) => { | |
| e.preventDefault(); | |
| const host = document.getElementById('host').value; | |
| const port = document.getElementById('port').value; | |
| const username = document.getElementById('username').value; | |
| const startupCommand = document.getElementById('startupCommand').value; | |
| const version = document.getElementById('version').value; | |
| const useMicrosoft = document.getElementById('use-microsoft').checked; | |
| const auth = useMicrosoft ? 'microsoft' : 'offline'; | |
| socket.emit('start-bot', { host, port, username, startupCommand, version, auth }); | |
| // Auto-select this bot so they see connecting logs right away | |
| selectedBot = username; | |
| updateTerminalUI(); | |
| showToast(`Verbindungsprozess für ${username} gestartet...${auth === 'microsoft' ? ' (Microsoft-Auth)' : ''}`, 'success'); | |
| }); | |
| // Server Ping | |
| document.getElementById('ping-btn').addEventListener('click', () => { | |
| const host = document.getElementById('host').value; | |
| const port = document.getElementById('port').value; | |
| const resultEl = document.getElementById('ping-result'); | |
| if (!host.trim()) { resultEl.innerHTML = '<span class="text-rose-400">IP eingeben!</span>'; return; } | |
| resultEl.innerHTML = '<span class="text-yellow-400"><i class="fa-solid fa-spinner fa-spin"></i> Pinge...</span>'; | |
| socket.emit('ping-server', { host, port }); | |
| }); | |
| socket.on('ping-result', (data) => { | |
| const el = document.getElementById('ping-result'); | |
| if (data.error) { | |
| el.innerHTML = `<span class="text-rose-400"><i class="fa-solid fa-circle-exclamation"></i> ${data.error}</span>`; | |
| } else { | |
| el.innerHTML = `<span class="text-emerald-400"><i class="fa-solid fa-check-circle"></i> ${data.version} · ${data.players}/${data.maxPlayers} Spieler · ${data.latency}ms</span>`; | |
| } | |
| }); | |
| document.getElementById('use-microsoft').addEventListener('change', function() { | |
| document.getElementById('msa-info').classList.toggle('hidden', !this.checked); | |
| document.getElementById('name-hint').classList.toggle('hidden', !this.checked); | |
| const input = document.getElementById('username'); | |
| input.placeholder = this.checked ? 'deine@email.com (Microsoft-Konto)' : 'z.B. AFK_Bot_1'; | |
| }); | |
| // Send chat command | |
| chatForm.addEventListener('submit', (e) => { | |
| e.preventDefault(); | |
| const msg = chatInput.value.trim(); | |
| if (!msg || !selectedBot) return; | |
| socket.emit('send-chat', { username: selectedBot, message: msg }); | |
| chatInput.value = ''; | |
| }); | |
| // Clear logs button | |
| clearLogsBtn.addEventListener('click', () => { | |
| if (selectedBot && bots.has(selectedBot)) { | |
| bots.get(selectedBot).logs = []; | |
| updateTerminalUI(); | |
| } else { | |
| terminal.innerHTML = '<div class="text-slate-500 italic">Logs geleert.</div>'; | |
| } | |
| }); | |
| // Socket: Handle Initial State | |
| socket.on('init-state', (initialBots) => { | |
| bots.clear(); | |
| initialBots.forEach(bot => { | |
| bots.set(bot.username, { | |
| status: bot.status, | |
| config: bot.config, | |
| logs: bot.logs || [] | |
| }); | |
| }); | |
| // If there's bots and no selectedBot, select the first one | |
| if (bots.size > 0 && !selectedBot) { | |
| selectedBot = bots.keys().next().value; | |
| } | |
| renderBotList(); | |
| updateTerminalUI(); | |
| }); | |
| // Socket: Handle bot status changes | |
| socket.on('bot-status-change', ({ username, status, config }) => { | |
| if (bots.has(username)) { | |
| const bot = bots.get(username); | |
| bot.status = status; | |
| if (config) bot.config = config; | |
| } else { | |
| bots.set(username, { | |
| status, | |
| config, | |
| logs: [] | |
| }); | |
| } | |
| // If we deleted/stopped a bot and it was selected, select another one if possible | |
| if (status === 'offline' && !bots.get(username)?.config) { | |
| // Means it's fully stopped and deleted | |
| bots.delete(username); | |
| if (selectedBot === username) { | |
| selectedBot = bots.size > 0 ? bots.keys().next().value : null; | |
| } | |
| } | |
| renderBotList(); | |
| updateTerminalUI(); | |
| }); | |
| // Socket: Handle incoming real-time logs | |
| socket.on('bot-log', (logEntry) => { | |
| const { username, type, message, timestamp } = logEntry; | |
| if (!bots.has(username)) { | |
| bots.set(username, { | |
| status: 'connecting', | |
| config: null, | |
| logs: [] | |
| }); | |
| } | |
| const bot = bots.get(username); | |
| bot.logs.push(logEntry); | |
| if (bot.logs.length > 200) { | |
| bot.logs.shift(); | |
| } | |
| // If this is the currently selected bot, render it on screen | |
| if (selectedBot === username) { | |
| appendLogToTerminal(logEntry); | |
| } | |
| }); | |
| // Socket: General Error Message from Server | |
| socket.on('error-msg', (msg) => { | |
| showToast(msg, 'error'); | |
| }); | |
| // Socket: Microsoft Device Code received | |
| socket.on('microsoft-device-code', (data) => { | |
| const modal = document.getElementById('msa-modal'); | |
| document.getElementById('msa-url').href = data.url; | |
| document.getElementById('msa-url').textContent = data.url; | |
| document.getElementById('msa-code').textContent = data.code; | |
| modal.classList.remove('hidden'); | |
| if (data.username && selectedBot !== data.username) { | |
| showToast(`Microsoft-Auth erforderlich für ${data.username}!`, 'error'); | |
| } | |
| }); | |
| // Microsoft Auth Modal Close | |
| document.getElementById('msa-modal-close').addEventListener('click', () => { | |
| document.getElementById('msa-modal').classList.add('hidden'); | |
| }); | |
| document.getElementById('msa-modal').addEventListener('click', (e) => { | |
| if (e.target === e.currentTarget) { | |
| document.getElementById('msa-modal').classList.add('hidden'); | |
| } | |
| }); | |
| // Render left column bot list | |
| function renderBotList() { | |
| // Update count | |
| activeBotsCountEl.innerText = `${bots.size} Bot${bots.size === 1 ? '' : 's'}`; | |
| if (bots.size === 0) { | |
| noBotsEl.classList.remove('hidden'); | |
| botListContainer.innerHTML = ''; | |
| botListContainer.appendChild(noBotsEl); | |
| return; | |
| } | |
| noBotsEl.classList.add('hidden'); | |
| botListContainer.innerHTML = ''; | |
| bots.forEach((bot, username) => { | |
| const isSelected = selectedBot === username; | |
| const card = document.createElement('div'); | |
| // Dynamic border and background based on selection | |
| card.className = `p-4 rounded-xl border transition duration-200 cursor-pointer flex flex-col space-y-3 relative group ${ | |
| isSelected | |
| ? 'bg-slate-800/80 border-emerald-500/50 shadow-md shadow-emerald-500/5' | |
| : 'bg-slate-900 border-slate-800 hover:border-slate-700' | |
| }`; | |
| // Status indicator classes | |
| let statusColorClass = 'bg-slate-500 text-slate-300 border-slate-600'; | |
| let statusLabel = 'Offline'; | |
| let isPulsing = false; | |
| switch (bot.status) { | |
| case 'online': | |
| statusColorClass = 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20'; | |
| statusLabel = 'Online'; | |
| break; | |
| case 'connecting': | |
| statusColorClass = 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20'; | |
| statusLabel = 'Verbinde...'; | |
| isPulsing = true; | |
| break; | |
| case 'reconnecting': | |
| statusColorClass = 'bg-amber-500/10 text-amber-400 border-amber-500/20'; | |
| statusLabel = 'Auto-Reconnect'; | |
| isPulsing = true; | |
| break; | |
| case 'offline': | |
| statusColorClass = 'bg-rose-500/10 text-rose-400 border-rose-500/20'; | |
| statusLabel = 'Inaktiv'; | |
| break; | |
| } | |
| const serverString = bot.config ? `${bot.config.host}:${bot.config.port}` : 'Unbekannter Server'; | |
| card.innerHTML = ` | |
| <div class="flex items-start justify-between"> | |
| <div class="flex items-center space-x-3"> | |
| <div class="p-2 rounded-lg ${isSelected ? 'bg-emerald-500 text-slate-950' : 'bg-slate-800 text-slate-400'}"> | |
| <i class="fa-solid fa-user-robot"></i> | |
| <i class="fa-solid fa-gamepad text-xs"></i> | |
| </div> | |
| <div> | |
| <h3 class="text-sm font-semibold text-white">${username}</h3> | |
| <p class="text-xs text-slate-400 flex items-center gap-1"> | |
| <i class="fa-solid fa-server text-[10px]"></i> ${serverString} | |
| </p> | |
| </div> | |
| </div> | |
| <span class="text-xs px-2 py-0.5 rounded-full border ${statusColorClass} ${isPulsing ? 'pulse-animation' : ''}"> | |
| ${statusLabel} | |
| </span> | |
| </div> | |
| <div class="flex items-center justify-between pt-1 border-t border-slate-800/60 text-xs"> | |
| <span class="text-slate-500"> | |
| ${bot.config?.version ? `MC ${bot.config.version}` : 'MC Auto-detect'} | |
| </span> | |
| <button class="stop-btn text-rose-400 hover:text-rose-300 hover:bg-rose-500/10 px-2 py-1 rounded transition flex items-center gap-1.5" | |
| data-username="${username}"> | |
| <i class="fa-solid fa-stop text-[10px]"></i> | |
| <span>Stoppen</span> | |
| </button> | |
| </div> | |
| `; | |
| // Selection trigger | |
| card.addEventListener('click', (e) => { | |
| // If stopped button was clicked, don't trigger selection | |
| if (e.target.closest('.stop-btn')) return; | |
| selectedBot = username; | |
| renderBotList(); | |
| updateTerminalUI(); | |
| }); | |
| // Stop button trigger | |
| const stopBtn = card.querySelector('.stop-btn'); | |
| stopBtn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| socket.emit('stop-bot', username); | |
| showToast(`Bot ${username} wird gestoppt...`, 'success'); | |
| }); | |
| botListContainer.appendChild(card); | |
| }); | |
| } | |
| // Update Terminal when changing selected bot | |
| function updateTerminalUI() { | |
| if (!selectedBot || !bots.has(selectedBot)) { | |
| terminalTitle.innerHTML = '<span class="text-slate-500">Wähle einen Bot aus</span>'; | |
| terminal.innerHTML = '<div class="text-slate-500 italic">Kein Bot ausgewählt. Erstelle einen Bot oder wähle einen aktiven Bot links aus.</div>'; | |
| chatInput.disabled = true; | |
| chatInput.placeholder = "Wähle einen Bot aus, um Befehle oder Chat zu senden..."; | |
| chatSendBtn.disabled = true; | |
| return; | |
| } | |
| const bot = bots.get(selectedBot); | |
| // Update terminal title with bot credentials | |
| terminalTitle.innerHTML = ` | |
| <span class="text-emerald-400">${selectedBot}</span> | |
| <span class="text-slate-600 font-sans">@</span> | |
| <span class="text-indigo-400 text-xs">${bot.config ? bot.config.host : ''}</span> | |
| `; | |
| // Enable input based on whether bot is online | |
| const isOnline = bot.status === 'online'; | |
| chatInput.disabled = !isOnline; | |
| chatSendBtn.disabled = !isOnline; | |
| if (isOnline) { | |
| chatInput.placeholder = `Sende Chat-Nachricht als ${selectedBot}...`; | |
| } else { | |
| chatInput.placeholder = `Bot ${selectedBot} ist offline. Nachrichten senden nur im Online-Zustand möglich.`; | |
| } | |
| // Re-populate logs in terminal | |
| terminal.innerHTML = ''; | |
| if (!bot.logs || bot.logs.length === 0) { | |
| terminal.innerHTML = `<div class="text-slate-600 italic">[Logs leer für ${selectedBot}]</div>`; | |
| } else { | |
| bot.logs.forEach(log => appendLogToTerminal(log)); | |
| } | |
| } | |
| // Append single log entry to terminal & auto scroll | |
| function appendLogToTerminal(logEntry) { | |
| const { type, message, timestamp } = logEntry; | |
| // Remove placeholder empty state | |
| if (terminal.innerHTML.includes('Logs leer') || terminal.innerHTML.includes('Kein Bot ausgewählt')) { | |
| terminal.innerHTML = ''; | |
| } | |
| let colorClass = 'text-slate-300'; | |
| let icon = ''; | |
| switch (type) { | |
| case 'info': | |
| colorClass = 'text-sky-400'; | |
| icon = '<i class="fa-solid fa-circle-info text-[10px]"></i> '; | |
| break; | |
| case 'success': | |
| colorClass = 'text-emerald-400 font-semibold'; | |
| icon = '<i class="fa-solid fa-circle-check text-[10px]"></i> '; | |
| break; | |
| case 'warn': | |
| colorClass = 'text-amber-500'; | |
| icon = '<i class="fa-solid fa-triangle-exclamation text-[10px]"></i> '; | |
| break; | |
| case 'error': | |
| colorClass = 'text-rose-500 font-semibold'; | |
| icon = '<i class="fa-solid fa-bug text-[10px]"></i> '; | |
| break; | |
| case 'chat': | |
| colorClass = 'text-slate-200 bg-slate-900/50 border-l border-emerald-500/30 pl-2'; | |
| icon = '<i class="fa-solid fa-message text-[10px] text-emerald-500/60 mr-1"></i> '; | |
| break; | |
| } | |
| const logRow = document.createElement('div'); | |
| logRow.className = `py-0.5 leading-relaxed flex items-start gap-1.5 hover:bg-slate-900/30 px-1 rounded transition-colors duration-100 ${colorClass}`; | |
| logRow.innerHTML = ` | |
| <span class="text-slate-600 select-none flex-shrink-0">[${timestamp}]</span> | |
| <span class="flex-grow whitespace-pre-wrap break-all">${icon}${escapeHtml(message)}</span> | |
| `; | |
| terminal.appendChild(logRow); | |
| // Keep logs to limit | |
| if (terminal.childNodes.length > 500) { | |
| terminal.removeChild(terminal.firstChild); | |
| } | |
| // Auto scroll to bottom | |
| terminal.scrollTop = terminal.scrollHeight; | |
| } | |
| // Helper: Escape HTML to avoid rendering problems with user messages containing HTML characters | |
| function escapeHtml(text) { | |
| const map = { | |
| '&': '&', | |
| '<': '<', | |
| '>': '>', | |
| '"': '"', | |
| "'": ''' | |
| }; | |
| return text.replace(/[&<>"']/g, function(m) { return map[m]; }); | |
| } | |
| </script> | |
| </body> | |
| </html> | |