// 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 = ` [${new Date(logEntry.timestamp).toLocaleTimeString()}] ${logEntry.message} `; } 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 = ` ${message} `; 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 = `
${text}
`; } }; window.hideLoading = function(element) { if (typeof element === 'string') { element = document.querySelector(element); } if (element) { element.innerHTML = ''; } };