| | <!DOCTYPE html> |
| | <html lang="en"> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | <title>Mineflayer Bot Manager</title> |
| | <script src="/socket.io/socket.io.js"></script> |
| | <style> |
| | * { |
| | margin: 0; |
| | padding: 0; |
| | box-sizing: border-box; |
| | } |
| | |
| | body { |
| | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | min-height: 100vh; |
| | padding: 20px; |
| | } |
| | |
| | .container { |
| | max-width: 1400px; |
| | margin: 0 auto; |
| | } |
| | |
| | .header { |
| | background: white; |
| | border-radius: 10px; |
| | padding: 20px; |
| | margin-bottom: 20px; |
| | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
| | } |
| | |
| | .header h1 { |
| | color: #333; |
| | margin-bottom: 10px; |
| | } |
| | |
| | .stats { |
| | display: flex; |
| | gap: 20px; |
| | flex-wrap: wrap; |
| | } |
| | |
| | .stat-card { |
| | background: #f8f9fa; |
| | padding: 10px 20px; |
| | border-radius: 5px; |
| | border-left: 4px solid #667eea; |
| | } |
| | |
| | .stat-card .label { |
| | font-size: 12px; |
| | color: #666; |
| | text-transform: uppercase; |
| | } |
| | |
| | .stat-card .value { |
| | font-size: 24px; |
| | font-weight: bold; |
| | color: #333; |
| | } |
| | |
| | .controls { |
| | background: white; |
| | border-radius: 10px; |
| | padding: 15px; |
| | margin-bottom: 20px; |
| | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
| | } |
| | |
| | .btn { |
| | background: #667eea; |
| | color: white; |
| | border: none; |
| | padding: 10px 20px; |
| | border-radius: 5px; |
| | cursor: pointer; |
| | font-size: 14px; |
| | transition: background 0.3s; |
| | } |
| | |
| | .btn:hover { |
| | background: #5a67d8; |
| | } |
| | |
| | .btn:disabled { |
| | background: #ccc; |
| | cursor: not-allowed; |
| | } |
| | |
| | .bot-grid { |
| | display: grid; |
| | grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); |
| | gap: 15px; |
| | } |
| | |
| | .bot-card { |
| | background: white; |
| | border-radius: 10px; |
| | padding: 15px; |
| | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
| | transition: transform 0.3s; |
| | } |
| | |
| | .bot-card:hover { |
| | transform: translateY(-2px); |
| | box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); |
| | } |
| | |
| | .bot-header { |
| | display: flex; |
| | justify-content: space-between; |
| | align-items: center; |
| | margin-bottom: 10px; |
| | } |
| | |
| | .bot-name { |
| | font-weight: bold; |
| | font-size: 16px; |
| | color: #333; |
| | } |
| | |
| | .bot-status { |
| | padding: 4px 8px; |
| | border-radius: 12px; |
| | font-size: 12px; |
| | font-weight: bold; |
| | } |
| | |
| | .status-connected { |
| | background: #d4edda; |
| | color: #155724; |
| | } |
| | |
| | .status-disconnected { |
| | background: #f8d7da; |
| | color: #721c24; |
| | } |
| | |
| | .status-connecting { |
| | background: #fff3cd; |
| | color: #856404; |
| | } |
| | |
| | .bot-info { |
| | display: grid; |
| | grid-template-columns: repeat(2, 1fr); |
| | gap: 10px; |
| | margin-bottom: 10px; |
| | } |
| | |
| | .info-item { |
| | display: flex; |
| | flex-direction: column; |
| | } |
| | |
| | .info-label { |
| | font-size: 11px; |
| | color: #666; |
| | text-transform: uppercase; |
| | } |
| | |
| | .info-value { |
| | font-size: 14px; |
| | color: #333; |
| | font-weight: 500; |
| | } |
| | |
| | .bot-actions { |
| | display: flex; |
| | gap: 10px; |
| | } |
| | |
| | .btn-small { |
| | padding: 6px 12px; |
| | font-size: 12px; |
| | } |
| | |
| | .btn-reconnect { |
| | background: #28a745; |
| | } |
| | |
| | .btn-reconnect:hover { |
| | background: #218838; |
| | } |
| | |
| | .notification { |
| | position: fixed; |
| | top: 20px; |
| | right: 20px; |
| | padding: 15px 20px; |
| | border-radius: 5px; |
| | background: white; |
| | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
| | display: none; |
| | animation: slideIn 0.3s; |
| | } |
| | |
| | .notification.success { |
| | border-left: 4px solid #28a745; |
| | } |
| | |
| | .notification.error { |
| | border-left: 4px solid #dc3545; |
| | } |
| | |
| | @keyframes slideIn { |
| | from { |
| | transform: translateX(100%); |
| | opacity: 0; |
| | } |
| | to { |
| | transform: translateX(0); |
| | opacity: 1; |
| | } |
| | } |
| | |
| | .loading { |
| | text-align: center; |
| | padding: 40px; |
| | color: white; |
| | } |
| | </style> |
| | </head> |
| | <body> |
| | <div class="container"> |
| | <div class="header"> |
| | <h1>🤖 Mineflayer Bot Manager</h1> |
| | <div class="stats"> |
| | <div class="stat-card"> |
| | <div class="label">Total Bots</div> |
| | <div class="value" id="totalBots">0</div> |
| | </div> |
| | <div class="stat-card"> |
| | <div class="label">Connected</div> |
| | <div class="value" id="connectedBots">0</div> |
| | </div> |
| | <div class="stat-card"> |
| | <div class="label">Disconnected</div> |
| | <div class="value" id="disconnectedBots">0</div> |
| | </div> |
| | <div class="stat-card"> |
| | <div class="label">Total Deaths</div> |
| | <div class="value" id="totalDeaths">0</div> |
| | </div> |
| | </div> |
| | </div> |
| | |
| | <div class="controls"> |
| | <button class="btn" onclick="refreshSheet()">🔄 Refresh from Sheet</button> |
| | <span style="margin-left: 10px; color: #666; font-size: 14px;"> |
| | Auto-refresh every 30 seconds |
| | </span> |
| | </div> |
| | |
| | <div id="botContainer" class="bot-grid"> |
| | <div class="loading">Loading bots...</div> |
| | </div> |
| | </div> |
| | |
| | <div id="notification" class="notification"></div> |
| | |
| | <script> |
| | const socket = io(); |
| | let botsData = []; |
| | |
| | socket.on('connect', () => { |
| | console.log('Connected to server'); |
| | showNotification('Connected to server', 'success'); |
| | }); |
| | |
| | socket.on('disconnect', () => { |
| | console.log('Disconnected from server'); |
| | showNotification('Disconnected from server', 'error'); |
| | }); |
| | |
| | socket.on('botUpdate', (data) => { |
| | botsData = data; |
| | updateUI(); |
| | }); |
| | |
| | socket.on('reconnectResult', (result) => { |
| | if (result.success) { |
| | showNotification(`Bot ${result.botName} is reconnecting...`, 'success'); |
| | } else { |
| | showNotification(`Cannot reconnect ${result.botName} yet. Wait 1 hour between reconnects.`, 'error'); |
| | } |
| | }); |
| | |
| | function updateUI() { |
| | const container = document.getElementById('botContainer'); |
| | |
| | if (botsData.length === 0) { |
| | container.innerHTML = '<div class="loading">No bots configured. Add bots to the Google Sheet.</div>'; |
| | updateStats(0, 0, 0, 0); |
| | return; |
| | } |
| | |
| | let html = ''; |
| | let totalBots = 0; |
| | let connectedBots = 0; |
| | let disconnectedBots = 0; |
| | let totalDeaths = 0; |
| | |
| | botsData.forEach(bot => { |
| | totalBots++; |
| | if (bot.status === 'Connected') { |
| | connectedBots++; |
| | } else { |
| | disconnectedBots++; |
| | } |
| | totalDeaths += bot.deathCount; |
| | |
| | const statusClass = bot.status === 'Connected' ? 'status-connected' : |
| | bot.status === 'Connecting...' ? 'status-connecting' : |
| | 'status-disconnected'; |
| | |
| | const uptime = formatUptime(bot.uptime); |
| | const disconnectTime = bot.disconnectTime ? |
| | formatTimeSince(bot.disconnectTime) : 'N/A'; |
| | |
| | html += ` |
| | <div class="bot-card"> |
| | <div class="bot-header"> |
| | <div class="bot-name">${bot.botName}</div> |
| | <div class="bot-status ${statusClass}">${bot.status}</div> |
| | </div> |
| | <div class="bot-info"> |
| | <div class="info-item"> |
| | <span class="info-label">Uptime</span> |
| | <span class="info-value">${uptime}</span> |
| | </div> |
| | <div class="info-item"> |
| | <span class="info-label">Deaths</span> |
| | <span class="info-value">${bot.deathCount}</span> |
| | </div> |
| | ${bot.status === 'Disconnected' ? ` |
| | <div class="info-item"> |
| | <span class="info-label">Disconnected</span> |
| | <span class="info-value">${disconnectTime}</span> |
| | </div> |
| | ` : ''} |
| | </div> |
| | <div class="bot-actions"> |
| | ${bot.canReconnect && bot.status === 'Disconnected' ? ` |
| | <button class="btn btn-small btn-reconnect" onclick="reconnectBot('${bot.botName}')"> |
| | 🔌 Reconnect |
| | </button> |
| | ` : ''} |
| | ${!bot.canReconnect && bot.status === 'Disconnected' ? ` |
| | <button class="btn btn-small" disabled> |
| | ⏰ Wait to reconnect |
| | </button> |
| | ` : ''} |
| | </div> |
| | </div> |
| | `; |
| | }); |
| | |
| | container.innerHTML = html; |
| | updateStats(totalBots, connectedBots, disconnectedBots, totalDeaths); |
| | } |
| | |
| | function updateStats(total, connected, disconnected, deaths) { |
| | document.getElementById('totalBots').textContent = total; |
| | document.getElementById('connectedBots').textContent = connected; |
| | document.getElementById('disconnectedBots').textContent = disconnected; |
| | document.getElementById('totalDeaths').textContent = deaths; |
| | } |
| | |
| | function formatUptime(seconds) { |
| | if (seconds === 0) return '0s'; |
| | |
| | const hours = Math.floor(seconds / 3600); |
| | const minutes = Math.floor((seconds % 3600) / 60); |
| | const secs = seconds % 60; |
| | |
| | if (hours > 0) { |
| | return `${hours}h ${minutes}m`; |
| | } else if (minutes > 0) { |
| | return `${minutes}m ${secs}s`; |
| | } else { |
| | return `${secs}s`; |
| | } |
| | } |
| | |
| | function formatTimeSince(timestamp) { |
| | const seconds = Math.floor((Date.now() - timestamp) / 1000); |
| | return formatUptime(seconds) + ' ago'; |
| | } |
| | |
| | function reconnectBot(botName) { |
| | socket.emit('reconnectBot', botName); |
| | } |
| | |
| | function refreshSheet() { |
| | socket.emit('refreshSheet'); |
| | showNotification('Refreshing from Google Sheet...', 'success'); |
| | } |
| | |
| | function showNotification(message, type) { |
| | const notification = document.getElementById('notification'); |
| | notification.textContent = message; |
| | notification.className = `notification ${type}`; |
| | notification.style.display = 'block'; |
| | |
| | setTimeout(() => { |
| | notification.style.display = 'none'; |
| | }, 3000); |
| | } |
| | </script> |
| | </body> |
| | </html> |