Initial commit: LuHost - Luanti Server Management Web Interface

A modern web interface for Luanti (Minetest) server management with ContentDB integration.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Nathan Schneider
2025-08-23 17:32:37 -06:00
commit 3aed09b60f
47 changed files with 12878 additions and 0 deletions

496
public/js/main.js Normal file
View File

@@ -0,0 +1,496 @@
// Main JavaScript for Luanti Web Server
class LuantiWebServer {
constructor() {
this.socket = null;
this.serverStatus = 'stopped';
this.init();
}
init() {
// Initialize Socket.IO
this.initSocket();
// Initialize UI components
this.initUI();
// Initialize forms
this.initForms();
// Initialize real-time updates
this.initRealTime();
}
initSocket() {
this.socket = io();
this.socket.on('connect', () => {
console.log('Connected to server');
this.updateConnectionStatus(true);
});
this.socket.on('disconnect', () => {
console.log('Disconnected from server');
this.updateConnectionStatus(false);
});
this.socket.on('serverStatus', (status) => {
this.updateServerStatus(status);
});
this.socket.on('serverLog', (logEntry) => {
this.appendLogEntry(logEntry);
});
}
initUI() {
// Modal functionality
this.initModals();
// Tooltips
this.initTooltips();
// Auto-refresh toggles
this.initAutoRefresh();
}
initModals() {
// Generic modal handling
document.addEventListener('click', (e) => {
if (e.target.matches('[data-modal-open]')) {
const modalId = e.target.getAttribute('data-modal-open');
this.openModal(modalId);
}
if (e.target.matches('[data-modal-close]') || e.target.closest('[data-modal-close]')) {
this.closeModal();
}
if (e.target.matches('.modal')) {
this.closeModal();
}
});
// Escape key to close modal
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.closeModal();
}
});
}
openModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
// Focus first input in modal
const firstInput = modal.querySelector('input, textarea, select');
if (firstInput) {
setTimeout(() => firstInput.focus(), 100);
}
}
}
closeModal() {
const modals = document.querySelectorAll('.modal');
modals.forEach(modal => {
modal.style.display = 'none';
});
document.body.style.overflow = '';
}
initTooltips() {
// Simple tooltip implementation
document.querySelectorAll('[data-tooltip]').forEach(element => {
element.addEventListener('mouseenter', (e) => {
this.showTooltip(e.target, e.target.getAttribute('data-tooltip'));
});
element.addEventListener('mouseleave', () => {
this.hideTooltip();
});
});
}
showTooltip(element, text) {
const tooltip = document.createElement('div');
tooltip.className = 'tooltip';
tooltip.textContent = text;
tooltip.style.cssText = `
position: absolute;
background: #1f2937;
color: white;
padding: 0.5rem;
border-radius: 0.375rem;
font-size: 0.875rem;
z-index: 1000;
white-space: nowrap;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
`;
document.body.appendChild(tooltip);
const rect = element.getBoundingClientRect();
tooltip.style.top = rect.top - tooltip.offsetHeight - 8 + 'px';
tooltip.style.left = rect.left + (rect.width - tooltip.offsetWidth) / 2 + 'px';
}
hideTooltip() {
const tooltip = document.querySelector('.tooltip');
if (tooltip) {
tooltip.remove();
}
}
initAutoRefresh() {
const autoRefreshElements = document.querySelectorAll('[data-auto-refresh]');
autoRefreshElements.forEach(element => {
const interval = parseInt(element.getAttribute('data-auto-refresh')) || 5000;
const url = element.getAttribute('data-refresh-url') || window.location.href;
setInterval(async () => {
if (element.checked || element.getAttribute('data-auto-refresh-active') === 'true') {
await this.refreshElement(element, url);
}
}, interval);
});
}
async refreshElement(element, url) {
try {
const response = await fetch(url);
if (response.ok) {
// This would need specific implementation per element type
console.log('Auto-refresh triggered for', element);
}
} catch (error) {
console.error('Auto-refresh failed:', error);
}
}
initForms() {
// AJAX form submission
document.addEventListener('submit', async (e) => {
if (e.target.matches('[data-ajax-form]')) {
e.preventDefault();
await this.submitAjaxForm(e.target);
}
});
// Real-time form validation
this.initFormValidation();
}
async submitAjaxForm(form) {
const submitBtn = form.querySelector('[type="submit"]');
const originalText = submitBtn.textContent;
try {
submitBtn.disabled = true;
submitBtn.textContent = 'Processing...';
const formData = new FormData(form);
const response = await fetch(form.action, {
method: form.method || 'POST',
body: formData
});
if (response.ok) {
if (response.headers.get('content-type')?.includes('application/json')) {
const result = await response.json();
this.showAlert(result.message || 'Success', 'success');
} else {
// Handle redirect or page reload
window.location.reload();
}
} else {
const error = await response.text();
this.showAlert(error || 'An error occurred', 'danger');
}
} catch (error) {
this.showAlert('Network error: ' + error.message, 'danger');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = originalText;
}
}
initFormValidation() {
// Real-time validation for world/mod names
document.addEventListener('input', (e) => {
if (e.target.matches('[data-validate-name]')) {
this.validateName(e.target);
}
});
}
validateName(input) {
const value = input.value;
const isValid = /^[a-zA-Z0-9_-]+$/.test(value) && value.length <= 50;
input.setCustomValidity(isValid ? '' : 'Only letters, numbers, underscore and hyphen allowed (max 50 chars)');
// Visual feedback
input.classList.toggle('is-invalid', !isValid && value.length > 0);
input.classList.toggle('is-valid', isValid && value.length > 0);
}
initRealTime() {
// Auto-scroll logs
this.initLogAutoScroll();
}
initLogAutoScroll() {
const logsContainer = document.querySelector('.logs');
if (logsContainer) {
// Auto-scroll to bottom when new logs arrive
const observer = new MutationObserver(() => {
if (logsContainer.scrollTop + logsContainer.clientHeight >= logsContainer.scrollHeight - 100) {
logsContainer.scrollTop = logsContainer.scrollHeight;
}
});
observer.observe(logsContainer, { childList: true, subtree: true });
}
}
updateConnectionStatus(connected) {
const statusElement = document.getElementById('connection-status');
if (statusElement) {
statusElement.className = connected ? 'status status-running' : 'status status-error';
statusElement.textContent = connected ? 'Connected' : 'Disconnected';
}
}
updateServerStatus(status) {
this.serverStatus = status.status;
// Update status badge
const statusElement = document.getElementById('server-status');
if (statusElement) {
statusElement.className = `status status-${status.status}`;
statusElement.textContent = status.status.charAt(0).toUpperCase() + status.status.slice(1);
}
// Update PID
const pidElement = document.getElementById('server-pid');
if (pidElement) {
pidElement.textContent = status.pid || 'N/A';
}
// Update uptime
const uptimeElement = document.getElementById('server-uptime');
if (uptimeElement) {
uptimeElement.textContent = this.formatUptime(status.uptime);
}
// Update control buttons
this.updateServerControls(status.status);
}
updateServerControls(status) {
const startBtn = document.getElementById('server-start');
const stopBtn = document.getElementById('server-stop');
const restartBtn = document.getElementById('server-restart');
if (startBtn) startBtn.disabled = status === 'running';
if (stopBtn) stopBtn.disabled = status === 'stopped';
if (restartBtn) restartBtn.disabled = status === 'stopped';
}
appendLogEntry(logEntry) {
const logsContainer = document.querySelector('.logs');
if (!logsContainer) return;
const logElement = document.createElement('div');
logElement.className = 'log-entry';
if (typeof logEntry === 'string') {
logElement.textContent = logEntry;
} else {
logElement.innerHTML = `
<span class="log-timestamp">[${new Date(logEntry.timestamp).toLocaleTimeString()}]</span>
<span class="log-level-${logEntry.level}">${logEntry.message}</span>
`;
}
logsContainer.appendChild(logElement);
// Limit log entries to prevent memory issues
const logEntries = logsContainer.children;
if (logEntries.length > 1000) {
logEntries[0].remove();
}
}
formatUptime(uptime) {
if (!uptime) return 'N/A';
const seconds = Math.floor(uptime / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
} else if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
} else {
return `${seconds}s`;
}
}
showAlert(message, type = 'info', duration = 5000) {
const alertsContainer = document.getElementById('alerts') || this.createAlertsContainer();
const alert = document.createElement('div');
alert.className = `alert alert-${type}`;
alert.innerHTML = `
<span>${message}</span>
<button type="button" class="modal-close" style="margin-left: auto;">&times;</button>
`;
alertsContainer.appendChild(alert);
// Auto-remove alert
setTimeout(() => {
if (alert.parentNode) {
alert.remove();
}
}, duration);
// Manual close
alert.querySelector('.modal-close').addEventListener('click', () => {
alert.remove();
});
}
createAlertsContainer() {
const container = document.createElement('div');
container.id = 'alerts';
container.style.cssText = `
position: fixed;
top: 1rem;
right: 1rem;
z-index: 1000;
max-width: 400px;
`;
document.body.appendChild(container);
return container;
}
// Utility methods
async api(endpoint, options = {}) {
try {
const response = await fetch(endpoint, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
} else {
return await response.text();
}
} catch (error) {
console.error('API Error:', error);
throw error;
}
}
formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
formatDate(date) {
return new Date(date).toLocaleString();
}
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
window.luantiWebServer = new LuantiWebServer();
});
// Global utility functions
window.confirmDelete = function(itemType, itemName) {
console.log(`confirmDelete called with: itemType="${itemType}", itemName="${itemName}"`);
if (itemType === 'world') {
// Extra confirmation for world deletion - require typing the world name
const message = `WARNING: You are about to permanently delete the world "${itemName}".\n\n` +
`This will remove ALL world data including:\n` +
`• All builds and constructions\n` +
`• Player inventories and progress\n` +
`• World settings and configuration\n` +
`• All world-specific mods and data\n\n` +
`This action cannot be undone!\n\n` +
`Type the world name exactly to confirm:`;
const confirmation = prompt(message);
console.log(`User entered: "${confirmation}", expected: "${itemName}"`);
if (confirmation === null) {
console.log('User cancelled the dialog');
return false;
}
const matches = confirmation === itemName;
console.log(`Confirmation ${matches ? 'matches' : 'does not match'}`);
if (!matches && confirmation !== null) {
alert('Deletion cancelled - world name did not match exactly.');
}
return matches;
} else {
// Standard confirmation for other items
return confirm(`Are you sure you want to delete the ${itemType} "${itemName}"? This action cannot be undone.`);
}
};
window.showLoading = function(element, text = 'Loading...') {
if (typeof element === 'string') {
element = document.querySelector(element);
}
if (element) {
element.innerHTML = `
<div class="loading">
<div class="spinner"></div>
<div>${text}</div>
</div>
`;
}
};
window.hideLoading = function(element) {
if (typeof element === 'string') {
element = document.querySelector(element);
}
if (element) {
element.innerHTML = '';
}
};

629
public/js/server.js Normal file
View File

@@ -0,0 +1,629 @@
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 =
'<option value="">Please log in to load worlds</option>' +
'<option value="" disabled>───────────────────</option>' +
'<option value="" disabled>🔒 Authentication required</option>';
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 =
'<option value="">No worlds found - server will create default world</option>' +
'<option value="" disabled>───────────────────</option>' +
'<option value="" disabled>💡 Create worlds in the Worlds section</option>';
} else {
worldSelect.innerHTML = '<option value="" disabled selected>Choose a world to run</option>';
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 =
'<option value="">Error loading worlds - will use defaults</option>';
}
}
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 = '<span class="timestamp">' + timestamp + '</span>' +
'<span class="message">' + escapeHtml(message) + '</span>';
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 = '<p class="text-muted">No players online</p>';
return;
}
// Create a table for better formatting with kick functionality
const playersHtml = '<table class="table table-sm">' +
'<thead>' +
'<tr>' +
'<th>Player</th>' +
'<th>Last Activity</th>' +
'<th>Actions</th>' +
'</tr>' +
'</thead>' +
'<tbody>' +
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 '<tr>' +
'<td><strong>' + escapeHtml(player.name) + '</strong></td>' +
'<td>' +
'<small class="text-muted">' + lastActivity + '</small><br>' +
'<span class="badge badge-secondary">' + (player.lastAction || 'Active') + '</span>' +
'</td>' +
'<td>' +
'<button class="btn btn-sm btn-outline-danger kick-player-btn" data-player-name="' + escapeHtml(player.name) + '"' +
(isExternal ? ' disabled title="Cannot kick players on external servers"' : '') + '>' +
'<i class="fas fa-user-slash"></i> Kick' +
'</button>' +
'</td>' +
'</tr>';
}).join('') +
'</tbody>' +
'</table>';
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);
}
}

View File

@@ -0,0 +1,39 @@
// Shared server status functionality for all pages
async function updateServerStatus(statusElementId) {
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();
updateStatusElement(statusElementId, status);
} catch (error) {
console.error('Failed to update server status:', error);
// Show error state
const statusElement = document.getElementById(statusElementId);
if (statusElement) {
statusElement.textContent = 'Error';
statusElement.className = 'status status-stopped';
}
}
}
function updateStatusElement(elementId, status) {
const statusElement = document.getElementById(elementId);
if (statusElement) {
const statusText = status.statusText || (status.isRunning ? 'running' : 'stopped');
statusElement.textContent = statusText.charAt(0).toUpperCase() + statusText.slice(1);
statusElement.className = `status status-${statusText}`;
}
}