| | |
| | <!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> |