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