| |
| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Bot Manager</title> |
| <script src="/socket.io/socket.io.js"></script> |
| <style> |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| body { |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; |
| background: #000; |
| color: #fff; |
| padding: 20px; |
| line-height: 1.5; |
| } |
| |
| .container { |
| max-width: 1200px; |
| margin: 0 auto; |
| } |
| |
| .header { |
| text-align: center; |
| margin-bottom: 30px; |
| padding-bottom: 20px; |
| border-bottom: 1px solid #333; |
| } |
| |
| .header h1 { |
| font-size: 28px; |
| font-weight: 600; |
| margin-bottom: 5px; |
| } |
| |
| .header p { |
| color: #888; |
| font-size: 14px; |
| } |
| |
| .card { |
| background: #111; |
| border: 1px solid #222; |
| border-radius: 8px; |
| padding: 20px; |
| margin-bottom: 20px; |
| } |
| |
| .card-title { |
| font-size: 14px; |
| font-weight: 600; |
| margin-bottom: 15px; |
| color: #888; |
| text-transform: uppercase; |
| letter-spacing: 1px; |
| } |
| |
| .server-info { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); |
| gap: 15px; |
| } |
| |
| .info-item { |
| background: #0a0a0a; |
| padding: 12px; |
| border-radius: 6px; |
| border: 1px solid #1a1a1a; |
| } |
| |
| .info-label { |
| font-size: 11px; |
| color: #666; |
| margin-bottom: 5px; |
| } |
| |
| .info-value { |
| font-size: 16px; |
| font-weight: 600; |
| } |
| |
| .status-online { |
| color: #0f0; |
| } |
| |
| .status-offline { |
| color: #f00; |
| } |
| |
| .rotation-card { |
| background: linear-gradient(135deg, #1a1a1a 0%, #0a0a0a 100%); |
| border: 1px solid #333; |
| text-align: center; |
| padding: 30px 20px; |
| } |
| |
| .current-bot { |
| font-size: 36px; |
| font-weight: 700; |
| margin: 15px 0; |
| letter-spacing: 2px; |
| } |
| |
| .timer { |
| font-size: 32px; |
| font-family: 'Courier New', monospace; |
| margin: 20px 0; |
| color: #888; |
| } |
| |
| .progress-bar { |
| background: #1a1a1a; |
| height: 4px; |
| border-radius: 2px; |
| overflow: hidden; |
| margin: 20px 0; |
| } |
| |
| .progress-fill { |
| background: #fff; |
| height: 100%; |
| transition: width 1s linear; |
| } |
| |
| .next-info { |
| font-size: 14px; |
| color: #666; |
| margin-top: 15px; |
| } |
| |
| button { |
| background: #fff; |
| color: #000; |
| border: none; |
| padding: 10px 24px; |
| border-radius: 6px; |
| font-size: 14px; |
| font-weight: 600; |
| cursor: pointer; |
| margin: 10px 5px 0; |
| transition: all 0.2s; |
| } |
| |
| button:hover { |
| background: #ddd; |
| transform: translateY(-1px); |
| } |
| |
| button:active { |
| transform: translateY(0); |
| } |
| |
| .queue { |
| display: flex; |
| justify-content: center; |
| gap: 10px; |
| flex-wrap: wrap; |
| margin: 20px 0; |
| } |
| |
| .queue-item { |
| background: #0a0a0a; |
| padding: 10px 20px; |
| border-radius: 6px; |
| border: 1px solid #1a1a1a; |
| font-size: 13px; |
| font-weight: 600; |
| } |
| |
| .queue-item.active { |
| background: #fff; |
| color: #000; |
| } |
| |
| .queue-item.next { |
| border-color: #666; |
| } |
| |
| .bot-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); |
| gap: 15px; |
| } |
| |
| .bot-card { |
| background: #0a0a0a; |
| border: 1px solid #1a1a1a; |
| border-radius: 6px; |
| padding: 15px; |
| transition: all 0.3s; |
| } |
| |
| .bot-card.active { |
| border-color: #fff; |
| background: #1a1a1a; |
| } |
| |
| .bot-card.online { |
| border-left: 3px solid #0f0; |
| } |
| |
| .bot-card.offline { |
| border-left: 3px solid #333; |
| } |
| |
| .bot-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 12px; |
| } |
| |
| .bot-name { |
| font-size: 16px; |
| font-weight: 600; |
| } |
| |
| .bot-badge { |
| background: #fff; |
| color: #000; |
| padding: 3px 8px; |
| border-radius: 4px; |
| font-size: 10px; |
| font-weight: 700; |
| } |
| |
| .bot-stats { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 8px; |
| margin: 12px 0; |
| } |
| |
| .stat { |
| background: #000; |
| padding: 8px; |
| border-radius: 4px; |
| text-align: center; |
| } |
| |
| .stat-label { |
| font-size: 10px; |
| color: #666; |
| } |
| |
| .stat-value { |
| font-size: 14px; |
| font-weight: 600; |
| margin-top: 3px; |
| } |
| |
| .bot-status { |
| text-align: center; |
| padding: 8px; |
| border-radius: 4px; |
| font-size: 12px; |
| font-weight: 600; |
| background: #000; |
| margin-top: 10px; |
| } |
| |
| .toast { |
| position: fixed; |
| top: 20px; |
| right: 20px; |
| background: #fff; |
| color: #000; |
| padding: 12px 20px; |
| border-radius: 6px; |
| font-size: 14px; |
| font-weight: 600; |
| z-index: 1000; |
| animation: slideIn 0.3s; |
| } |
| |
| @keyframes slideIn { |
| from { |
| transform: translateX(400px); |
| opacity: 0; |
| } |
| to { |
| transform: translateX(0); |
| opacity: 1; |
| } |
| } |
| |
| @media (max-width: 768px) { |
| body { |
| padding: 10px; |
| } |
| .header h1 { |
| font-size: 22px; |
| } |
| .current-bot { |
| font-size: 24px; |
| } |
| .timer { |
| font-size: 20px; |
| } |
| .bot-grid { |
| grid-template-columns: 1fr; |
| } |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <div class="header"> |
| <h1>🎮 BOT MANAGER</h1> |
| <p>OrbitMC Server Monitor</p> |
| </div> |
|
|
| |
| <div class="card"> |
| <div class="card-title">Server Status</div> |
| <div class="server-info"> |
| <div class="info-item"> |
| <div class="info-label">Address</div> |
| <div class="info-value" id="server-addr">-</div> |
| </div> |
| <div class="info-item"> |
| <div class="info-label">Status</div> |
| <div class="info-value" id="server-status">-</div> |
| </div> |
| <div class="info-item"> |
| <div class="info-label">Latency</div> |
| <div class="info-value" id="server-latency">-</div> |
| </div> |
| <div class="info-item"> |
| <div class="info-label">Version</div> |
| <div class="info-value" id="server-version">-</div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="card rotation-card"> |
| <div class="card-title">Current Active Bot</div> |
| <div class="current-bot" id="current-bot">-</div> |
| <div class="timer" id="timer">00:00:00</div> |
| <div class="progress-bar"> |
| <div class="progress-fill" id="progress"></div> |
| </div> |
| <div class="next-info"> |
| Next: <strong id="next-bot">-</strong> in <strong id="next-time">-</strong> |
| </div> |
| <button onclick="forceRotation()">⏭ Next Bot</button> |
| </div> |
|
|
| |
| <div class="card"> |
| <div class="card-title">Rotation Queue</div> |
| <div class="queue" id="queue"></div> |
| </div> |
|
|
| |
| <div class="bot-grid" id="bot-grid"></div> |
| </div> |
|
|
| <div id="toast-container"></div> |
|
|
| <script> |
| const socket = io(); |
| let data = {}; |
| |
| socket.on('update', (update) => { |
| data = update; |
| render(); |
| }); |
| |
| function render() { |
| |
| const srv = data.server; |
| if (srv) { |
| document.getElementById('server-addr').textContent = `${srv.host}:${srv.port}`; |
| const status = document.getElementById('server-status'); |
| status.textContent = srv.status.online ? 'Online' : 'Offline'; |
| status.className = 'info-value ' + (srv.status.online ? 'status-online' : 'status-offline'); |
| document.getElementById('server-latency').textContent = srv.status.online ? `${srv.status.latency}ms` : 'N/A'; |
| document.getElementById('server-version').textContent = srv.version; |
| } |
| |
| |
| const rot = data.rotation; |
| if (rot) { |
| document.getElementById('current-bot').textContent = rot.current || '-'; |
| document.getElementById('timer').textContent = formatTime(rot.elapsed); |
| document.getElementById('next-bot').textContent = rot.next || '-'; |
| document.getElementById('next-time').textContent = formatTime(rot.remaining); |
| |
| const progress = (rot.elapsed / 3600) * 100; |
| document.getElementById('progress').style.width = progress + '%'; |
| |
| |
| const queueEl = document.getElementById('queue'); |
| queueEl.innerHTML = ''; |
| rot.queue.forEach((bot, i) => { |
| const item = document.createElement('div'); |
| item.className = 'queue-item'; |
| if (bot === rot.current) item.className += ' active'; |
| else if (bot === rot.next) item.className += ' next'; |
| item.textContent = bot; |
| queueEl.appendChild(item); |
| }); |
| } |
| |
| |
| const bots = data.bots; |
| if (bots) { |
| const grid = document.getElementById('bot-grid'); |
| grid.innerHTML = ''; |
| |
| bots.forEach(bot => { |
| const card = document.createElement('div'); |
| card.className = `bot-card ${bot.status}`; |
| if (bot.isActive) card.className += ' active'; |
| |
| const badge = bot.isActive ? '<div class="bot-badge">ACTIVE</div>' : ''; |
| |
| card.innerHTML = ` |
| <div class="bot-header"> |
| <div class="bot-name">${bot.name}</div> |
| ${badge} |
| </div> |
| <div class="bot-stats"> |
| <div class="stat"> |
| <div class="stat-label">Uptime</div> |
| <div class="stat-value">${formatTimeShort(bot.uptimeSeconds || 0)}</div> |
| </div> |
| <div class="stat"> |
| <div class="stat-label">Deaths</div> |
| <div class="stat-value">${bot.deaths}</div> |
| </div> |
| <div class="stat"> |
| <div class="stat-label">Position</div> |
| <div class="stat-value">${bot.position.x}, ${bot.position.y}, ${bot.position.z}</div> |
| </div> |
| <div class="stat"> |
| <div class="stat-label">Health</div> |
| <div class="stat-value">❤️ ${bot.health}</div> |
| </div> |
| </div> |
| <div class="bot-status">${bot.status.toUpperCase()}</div> |
| `; |
| |
| grid.appendChild(card); |
| }); |
| } |
| } |
| |
| function formatTime(sec) { |
| if (!sec) return '00:00:00'; |
| const h = Math.floor(sec / 3600); |
| const m = Math.floor((sec % 3600) / 60); |
| const s = sec % 60; |
| return `${pad(h)}:${pad(m)}:${pad(s)}`; |
| } |
| |
| function formatTimeShort(sec) { |
| if (!sec) return '0m'; |
| const m = Math.floor(sec / 60); |
| if (m < 60) return `${m}m`; |
| const h = Math.floor(m / 60); |
| return `${h}h ${m % 60}m`; |
| } |
| |
| function pad(n) { |
| return n.toString().padStart(2, '0'); |
| } |
| |
| function forceRotation() { |
| if (confirm('Switch to next bot?')) { |
| socket.emit('forceRotation'); |
| toast('Switching to next bot...'); |
| } |
| } |
| |
| function toast(msg) { |
| const container = document.getElementById('toast-container'); |
| const el = document.createElement('div'); |
| el.className = 'toast'; |
| el.textContent = msg; |
| container.appendChild(el); |
| setTimeout(() => el.remove(), 3000); |
| } |
| |
| socket.on('rotationForced', () => { |
| toast('✅ Rotation forced'); |
| }); |
| </script> |
| </body> |
| </html> |