Hugosmpbots / public /index.html
TheTapTap800
Clearer Microsoft auth UI + fix spawn timeout race
eca30f9
Raw
History Blame Contribute Delete
31.5 kB
<!DOCTYPE html>
<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>&copy; 2026 AFK Bot Dashboard &bull; 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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}
</script>
</body>
</html>