let socket; let autoScroll = true; let serverRunning = false; let isExternalServer = false; document.addEventListener('DOMContentLoaded', function() { // Initialize WebSocket connection for real-time updates initializeWebSocket(); // Load initial data loadWorlds(); updateServerStatus(); // Set up periodic status updates (every 3 seconds for better responsiveness) setInterval(updateServerStatus, 3000); // Add event listeners for buttons document.getElementById('startBtn').addEventListener('click', startServer); document.getElementById('stopBtn').addEventListener('click', stopServer); document.getElementById('restartBtn').addEventListener('click', restartServer); document.getElementById('downloadBtn').addEventListener('click', downloadLogs); document.getElementById('clearBtn').addEventListener('click', clearLogs); document.getElementById('autoScrollBtn').addEventListener('click', toggleAutoScroll); document.getElementById('sendBtn').addEventListener('click', sendCommand); // Add enter key handler for console input document.getElementById('consoleInput').addEventListener('keypress', function(event) { if (event.key === 'Enter') { sendCommand(); } }); }); function initializeWebSocket() { socket = io(); socket.on('server:log', function(logEntry) { addLogEntry(logEntry.type, logEntry.content, logEntry.timestamp); }); socket.on('server:status', function(status) { isExternalServer = status.isExternal || false; updateStatusDisplay(status); }); socket.on('server:players', function(players) { updatePlayersList(players, isExternalServer); }); } async function updateServerStatus() { try { const response = await fetch('/api/server/status'); // Check for authentication redirect const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('text/html')) { console.warn('Authentication required for server status'); // Silently fail for status updates, don't redirect automatically return; } if (!response.ok) { throw new Error('HTTP error! status: ' + response.status); } const status = await response.json(); isExternalServer = status.isExternal || false; updateStatusDisplay(status); } catch (error) { console.error('Failed to update server status:', error); } } async function checkServerStatus() { try { const response = await fetch('/api/server/status'); // Check for authentication redirect const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('text/html')) { return null; } if (!response.ok) { return null; } return await response.json(); } catch (error) { console.error('Failed to check server status:', error); return null; } } function updateStatusDisplay(status) { const statusLight = document.getElementById('statusLight'); const statusText = document.getElementById('statusText'); const startBtn = document.getElementById('startBtn'); const stopBtn = document.getElementById('stopBtn'); const restartBtn = document.getElementById('restartBtn'); const consoleInputGroup = document.getElementById('consoleInputGroup'); const wasRunning = serverRunning; serverRunning = status.isRunning; if (status.isRunning) { if (status.isReady) { // Server is running and ready to accept connections statusLight.className = 'status-light online'; statusText.textContent = status.isExternal ? 'Running (External - Monitor Only)' : 'Running'; } else { // Server process is running but not ready yet statusLight.className = 'status-light starting'; statusText.textContent = 'Starting...'; } // For external servers, disable control buttons if (status.isExternal) { startBtn.disabled = true; stopBtn.disabled = true; restartBtn.disabled = true; consoleInputGroup.style.display = 'none'; } else { startBtn.disabled = true; stopBtn.disabled = false; restartBtn.disabled = false; consoleInputGroup.style.display = 'block'; } } else { statusLight.className = 'status-light offline'; statusText.textContent = 'Offline'; startBtn.disabled = false; stopBtn.disabled = true; restartBtn.disabled = true; consoleInputGroup.style.display = 'none'; // Reset button states if server stopped unexpectedly if (startBtn.textContent === 'âŗ Starting...') { startBtn.textContent = 'â–ļī¸ Start Server'; } if (restartBtn.textContent === 'âŗ Restarting...') { restartBtn.textContent = '🔄 Restart Server'; } // Log if server stopped unexpectedly if (wasRunning && !status.isRunning) { addLogEntry('warning', 'Server has stopped. Check logs for details.'); } } // Update stats document.getElementById('uptime').textContent = formatUptime(status.uptime); document.getElementById('playerCount').textContent = status.players || 0; document.getElementById('memoryUsage').textContent = status.memoryUsage ? Math.round(status.memoryUsage) + ' MB' : '--'; // Debug: Log the status to see what we're getting console.log('Server status update:', { isRunning: status.isRunning, players: status.players, uptime: status.uptime }); } function formatUptime(milliseconds) { if (!milliseconds) return '--'; const seconds = Math.floor(milliseconds / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (days > 0) return days + 'd ' + (hours % 24) + 'h'; if (hours > 0) return hours + 'h ' + (minutes % 60) + 'm'; if (minutes > 0) return minutes + 'm ' + (seconds % 60) + 's'; return seconds + 's'; } async function loadWorlds() { console.log('loadWorlds() called'); try { const response = await fetch('/api/worlds'); // Check for authentication redirect const contentType = response.headers.get('content-type'); console.log('Response status:', response.status, 'Content-Type:', contentType); if (contentType && contentType.includes('text/html')) { console.warn('Authentication required for loading worlds'); document.getElementById('worldSelect').innerHTML = '' + '' + ''; return; } if (!response.ok) { throw new Error('HTTP error! status: ' + response.status); } const worlds = await response.json(); console.log('Worlds received:', worlds); const worldSelect = document.getElementById('worldSelect'); if (worlds.length === 0) { worldSelect.innerHTML = '' + '' + ''; } else { worldSelect.innerHTML = ''; worlds.forEach(world => { const option = document.createElement('option'); option.value = world.name; option.textContent = '🌍 ' + (world.displayName || world.name); worldSelect.appendChild(option); }); } } catch (error) { console.error('Failed to load worlds:', error); document.getElementById('worldSelect').innerHTML = ''; } } async function startServer() { const worldName = document.getElementById('worldSelect').value; console.log('Starting server with world:', worldName); const startBtn = document.getElementById('startBtn'); // Validate that a world is selected if (!worldName) { addLogEntry('error', 'Please select a world before starting the server'); return; } try { startBtn.disabled = true; startBtn.textContent = 'âŗ Starting...'; const response = await fetch('/api/server/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ worldName: worldName }) }); // Check if response is HTML (redirect to login) instead of JSON const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('text/html')) { addLogEntry('error', 'Authentication required. Please refresh the page and log in.'); startBtn.disabled = false; startBtn.textContent = 'â–ļī¸ Start Server'; // Optionally redirect to login setTimeout(() => { window.location.href = '/login?redirect=' + encodeURIComponent(window.location.pathname); }, 2000); return; } if (!response.ok) { const errorText = await response.text(); startBtn.disabled = false; startBtn.textContent = 'â–ļī¸ Start Server'; throw new Error('HTTP error! status: ' + response.status + ' - ' + errorText); } const result = await response.json(); if (result.success) { addLogEntry('info', result.message || 'Server started successfully'); await updateServerStatus(); // Monitor for early server crash setTimeout(async () => { const status = await checkServerStatus(); if (status && !status.isRunning) { addLogEntry('warning', 'Server appears to have stopped unexpectedly. Check logs for errors.'); startBtn.disabled = false; startBtn.textContent = 'â–ļī¸ Start Server'; } }, 3000); // Check after 3 seconds } else { addLogEntry('error', 'Failed to start server: ' + (result.error || 'Unknown error')); startBtn.disabled = false; startBtn.textContent = 'â–ļī¸ Start Server'; } } catch (error) { console.error('Server start error:', error); addLogEntry('error', 'Failed to start server: ' + error.message); startBtn.disabled = false; startBtn.textContent = 'â–ļī¸ Start Server'; } } async function stopServer() { const stopBtn = document.getElementById('stopBtn'); try { stopBtn.disabled = true; stopBtn.textContent = 'âŗ Stopping...'; const response = await fetch('/api/server/stop', { method: 'POST' }); // Check for authentication redirect const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('text/html')) { addLogEntry('error', 'Authentication required. Please refresh the page and log in.'); stopBtn.disabled = false; stopBtn.textContent = 'âšī¸ Stop Server'; setTimeout(() => { window.location.href = '/login?redirect=' + encodeURIComponent(window.location.pathname); }, 2000); return; } if (!response.ok) { throw new Error('HTTP error! status: ' + response.status); } const result = await response.json(); if (result.success) { addLogEntry('info', result.message || 'Server stopped successfully'); await updateServerStatus(); } else { addLogEntry('error', 'Failed to stop server: ' + (result.error || 'Unknown error')); } } catch (error) { console.error('Server stop error:', error); addLogEntry('error', 'Failed to stop server: ' + error.message); } finally { stopBtn.disabled = false; stopBtn.textContent = 'âšī¸ Stop Server'; } } async function restartServer() { const worldName = document.getElementById('worldSelect').value; const restartBtn = document.getElementById('restartBtn'); // Validate that a world is selected if (!worldName) { addLogEntry('error', 'Please select a world before restarting the server'); return; } try { restartBtn.disabled = true; restartBtn.textContent = 'âŗ Restarting...'; const response = await fetch('/api/server/restart', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ worldName: worldName || null }) }); // Check for authentication redirect const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('text/html')) { addLogEntry('error', 'Authentication required. Please refresh the page and log in.'); restartBtn.disabled = false; restartBtn.textContent = '🔄 Restart Server'; setTimeout(() => { window.location.href = '/login?redirect=' + encodeURIComponent(window.location.pathname); }, 2000); return; } if (!response.ok) { const errorText = await response.text(); restartBtn.disabled = false; restartBtn.textContent = '🔄 Restart Server'; throw new Error('HTTP error! status: ' + response.status + ' - ' + errorText); } const result = await response.json(); if (result.success) { addLogEntry('info', result.message || 'Server restarted successfully'); await updateServerStatus(); // Monitor for early server crash setTimeout(async () => { const status = await checkServerStatus(); if (status && !status.isRunning) { addLogEntry('warning', 'Server appears to have stopped unexpectedly after restart. Check logs for errors.'); restartBtn.disabled = false; restartBtn.textContent = '🔄 Restart Server'; } }, 3000); // Check after 3 seconds } else { addLogEntry('error', 'Failed to restart server: ' + (result.error || 'Unknown error')); } } catch (error) { console.error('Server restart error:', error); addLogEntry('error', 'Failed to restart server: ' + error.message); } finally { restartBtn.disabled = false; restartBtn.textContent = '🔄 Restart Server'; } } function addLogEntry(type, message, timestamp) { const consoleContent = document.getElementById('consoleContent'); const logEntry = document.createElement('div'); timestamp = timestamp || new Date().toLocaleTimeString(); logEntry.className = 'log-entry ' + type; logEntry.innerHTML = '' + timestamp + '' + '' + escapeHtml(message) + ''; consoleContent.appendChild(logEntry); // Auto-scroll to bottom if enabled if (autoScroll) { consoleContent.scrollTop = consoleContent.scrollHeight; } // Limit log entries to prevent memory issues const maxEntries = 1000; while (consoleContent.children.length > maxEntries) { consoleContent.removeChild(consoleContent.firstChild); } } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function clearLogs() { document.getElementById('consoleContent').innerHTML = ''; addLogEntry('info', 'Console cleared'); } function toggleAutoScroll() { autoScroll = !autoScroll; document.getElementById('autoScrollText').textContent = 'Auto-scroll: ' + (autoScroll ? 'ON' : 'OFF'); } async function sendCommand() { const input = document.getElementById('consoleInput'); const command = input.value.trim(); if (!command) return; try { const response = await fetch('/api/server/command', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command }) }); // Check for authentication redirect const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('text/html')) { addLogEntry('error', 'Authentication required. Please refresh the page and log in.'); setTimeout(() => { window.location.href = '/login?redirect=' + encodeURIComponent(window.location.pathname); }, 2000); return; } if (!response.ok) { throw new Error('HTTP error! status: ' + response.status); } const result = await response.json(); if (result.success) { addLogEntry('info', 'Command sent: ' + command); input.value = ''; } else { addLogEntry('error', 'Failed to send command: ' + (result.error || 'Unknown error')); } } catch (error) { console.error('Send command error:', error); addLogEntry('error', 'Failed to send command: ' + error.message); } } function updatePlayersList(players, isExternal) { const playersList = document.getElementById('playersList'); if (!players || players.length === 0) { playersList.innerHTML = '

No players online

'; return; } // Create a table for better formatting with kick functionality const playersHtml = '' + '' + '' + '' + '' + '' + '' + '' + '' + players.map((player, index) => { // Format the last seen time let lastActivity = '--'; if (player.lastSeen) { const now = new Date(); const lastSeenTime = new Date(player.lastSeen); const diffMinutes = Math.floor((now - lastSeenTime) / (1000 * 60)); if (diffMinutes < 1) { lastActivity = 'Just now'; } else if (diffMinutes < 60) { lastActivity = diffMinutes + 'm ago'; } else { lastActivity = Math.floor(diffMinutes / 60) + 'h ago'; } } return '' + '' + '' + '' + ''; }).join('') + '' + '
PlayerLast ActivityActions
' + escapeHtml(player.name) + '' + '' + lastActivity + '
' + '' + (player.lastAction || 'Active') + '' + '
' + '' + '
'; playersList.innerHTML = playersHtml; // Add event listeners for kick buttons const kickButtons = playersList.querySelectorAll('.kick-player-btn'); kickButtons.forEach(button => { button.addEventListener('click', function(e) { e.preventDefault(); const playerName = this.getAttribute('data-player-name'); kickPlayer(playerName); }); }); } async function kickPlayer(playerName) { console.log('kickPlayer() called for player:', playerName); addLogEntry('info', 'Attempting to kick player: ' + playerName); if (!confirm('Are you sure you want to kick ' + playerName + '?')) { console.log('Kick cancelled by user'); return; } console.log('Sending kick request...'); try { const response = await fetch('/api/server/command', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, credentials: 'same-origin', body: JSON.stringify({ command: '/kick ' + playerName }) }); if (!response.ok) { if (response.status === 401) { addLogEntry('error', 'Authentication required to kick players. Please refresh the page.'); setTimeout(() => { window.location.reload(); }, 2000); return; } throw new Error('HTTP error! status: ' + response.status); } const result = await response.json(); if (result.success) { addLogEntry('success', 'Kicked player: ' + playerName); // Refresh player list after a short delay setTimeout(updateServerStatus, 1000); } else { addLogEntry('error', 'Failed to kick player: ' + (result.error || 'Unknown error')); } } catch (error) { console.error('Error kicking player:', error); addLogEntry('error', 'Error kicking player: ' + error.message); } } async function downloadLogs() { try { const response = await fetch('/api/server/logs'); // Check for authentication redirect const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('text/html')) { addLogEntry('error', 'Authentication required to download logs'); setTimeout(() => { window.location.href = '/login?redirect=' + encodeURIComponent(window.location.pathname); }, 2000); return; } if (response.ok) { const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'server-logs-' + new Date().toISOString().split('T')[0] + '.txt'; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); } else { addLogEntry('error', 'Failed to download logs: HTTP ' + response.status); } } catch (error) { console.error('Download logs error:', error); addLogEntry('error', 'Failed to download logs: ' + error.message); } }