document.addEventListener('DOMContentLoaded', () => { let apiKey = localStorage.getItem('apiKey') || ''; let ws = null; const pendingRequests = new Map(); let responseTimeout = null; let terminal = null; let fitAddon = null; let memoryMeter = null; let cpuMeter = null; let currentPage = 1; let totalResults = 0; const resultsPerPage = 10; let modListCurrentPage = 1; let modListSearchQuery = ''; let allProperties = {}; let sftpCredentials = null; let isSftpOnline = false; const filteredSettings = [ 'bug-report-link', 'query.port', 'rcon.password', 'region-file-compression', 'rcon.port', 'server-port', 'level-type', 'enable-rcon', 'enable-status' ]; const elements = { loginPage: document.getElementById('loginPage'), loginApiKey: document.getElementById('loginApiKey'), loginBtn: document.getElementById('loginBtn'), loginError: document.getElementById('loginError'), authControls: document.getElementById('authControls'), apiKeyInput: document.getElementById('apiKey'), user: document.getElementById('user'), serverStatus: document.getElementById('serverStatus'), memoryPercent: document.getElementById('memoryPercent'), cpuPercent: document.getElementById('cpuPercent'), keyExpiry: document.getElementById('keyExpiry'), playerList: document.getElementById('playerList'), modList: document.getElementById('modList'), logUrl: document.getElementById('logUrl'), websiteUrl: document.getElementById('websiteUrl'), mapUrl: document.getElementById('mapUrl'), myLink: document.getElementById('myLink'), geyserLink: document.getElementById('geyserLink'), sftpLink: document.getElementById('sftpLink'), modResults: document.getElementById('modResults'), consoleOutput: document.getElementById('consoleOutput'), consoleInput: document.getElementById('consoleInput'), dockerLogsTerminal: document.getElementById('dockerLogsTerminal'), mainContent: document.getElementById('mainContent'), notificationContainer: document.getElementById('notificationContainer'), generateMyLinkBtn: document.getElementById('generateMyLinkBtn'), generateGeyserLinkBtn: document.getElementById('generateGeyserLinkBtn'), generateSftpLinkBtn: document.getElementById('generateSftpLinkBtn'), pagination: document.getElementById('pagination'), modSearch: document.getElementById('modSearch'), closeSearchBtn: document.getElementById('closeSearchBtn'), tellModal: document.getElementById('tellModal'), tellPlayerName: document.getElementById('tellPlayerName'), tellMessage: document.getElementById('tellMessage'), tellForm: document.getElementById('tellForm'), giveModal: document.getElementById('giveModal'), givePlayerName: document.getElementById('givePlayerName'), loadoutSelect: document.getElementById('loadoutSelect'), customGiveFields: document.getElementById('customGiveFields'), giveForm: document.getElementById('giveForm'), addItemBtn: document.getElementById('addItemBtn'), itemList: document.getElementById('itemList'), editPropertiesBtn: document.getElementById('editPropertiesBtn'), editPropertiesModal: document.getElementById('editPropertiesModal'), propertiesFields: document.getElementById('propertiesFields'), editPropertiesForm: document.getElementById('editPropertiesForm'), updateModsBtn: document.getElementById('updateModsBtn'), updateModsModal: document.getElementById('updateModsModal'), updateModsOutput: document.getElementById('updateModsOutput'), closeUpdateModsBtn: document.getElementById('closeUpdateModsBtn'), stopBtn: document.getElementById('stopBtn'), restartBtn: document.getElementById('restartBtn'), connectionStatus: document.getElementById('connectionStatus'), geyserStatus: document.getElementById('geyserStatus'), sftpStatus: document.getElementById('sftpStatus'), backupBtn: document.getElementById('backupBtn'), modListSearch: document.getElementById('modListSearch'), clearModListSearch: document.getElementById('clearModListSearch'), modListPagination: document.getElementById('modListPagination'), teleportModal: document.getElementById('teleportModal'), teleportPlayerName: document.getElementById('teleportPlayerName'), teleportDestination: document.getElementById('teleportDestination'), teleportForm: document.getElementById('teleportForm'), effectModal: document.getElementById('effectModal'), effectPlayerName: document.getElementById('effectPlayerName'), effectSelect: document.getElementById('effectSelect'), effectForm: document.getElementById('effectForm'), sftpBtn: document.getElementById('sftpBtn'), sftpBrowserSection: document.getElementById('sftpBrowserSection'), sftpIframe: document.getElementById('sftpIframe') }; const loadouts = { starter: [ { item: 'torch', amount: 16 }, { item: 'cooked_beef', amount: 8 }, { item: 'stone_pickaxe', amount: 1 } ], builder: [ { item: 'stone', amount: 64 }, { item: 'oak_planks', amount: 64 }, { item: 'glass', amount: 32 }, { item: 'ladder', amount: 16 } ], combat: [ { item: 'iron_sword', amount: 1 }, { item: 'leather_chestplate', amount: 1 }, { item: 'shield', amount: 1 }, { item: 'arrow', amount: 32 } ], miner: [ { item: 'iron_pickaxe', amount: 1, enchantments: [{ name: 'efficiency', level: 2 }] }, { item: 'torch', amount: 64 }, { item: 'iron_shovel', amount: 1 }, { item: 'bucket', amount: 1 } ], adventurer: [ { item: 'bow', amount: 1, enchantments: [{ name: 'power', level: 1 }] }, { item: 'arrow', amount: 64 }, { item: 'compass', amount: 1 }, { item: 'map', amount: 1 } ], alchemist: [ { item: 'brewing_stand', amount: 1 }, { item: 'potion', amount: 3, type: 'healing' }, { item: 'potion', amount: 2, type: 'swiftness' }, { item: 'nether_wart', amount: 16 } ], enchanter: [ { item: 'enchanting_table', amount: 1 }, { item: 'book', amount: 10 }, { item: 'lapis_lazuli', amount: 32 }, { item: 'experience_bottle', amount: 16 } ], farmer: [ { item: 'wheat_seeds', amount: 32 }, { item: 'iron_hoe', amount: 1 }, { item: 'bone_meal', amount: 16 }, { item: 'carrot', amount: 8 } ], nether: [ { item: 'potion', amount: 2, type: 'fire_resistance' }, { item: 'obsidian', amount: 10 }, { item: 'flint_and_steel', amount: 1 }, { item: 'golden_apple', amount: 1 } ], end: [ { item: 'ender_pearl', amount: 16 }, { item: 'blaze_rod', amount: 8 }, { item: 'diamond_sword', amount: 1, enchantments: [{ name: 'sharpness', level: 2 }] }, { item: 'pumpkin', amount: 1 } ] }; let state = { user: '', serverStatus: '', memoryPercent: 0, cpuPercent: 0, keyExpiry: '', playerList: '', modListHtml: '', logUrl: '', websiteUrl: '', mapUrl: '', myLink: '', geyserLink: '', sftpLink: '', hasShownStartNotification: false, connectionStatus: '', geyserStatus: '', sftpStatus: '', activeNotifications: new Map(), allMods: [], currentPlayers: [] }; function showNotification(message, type = 'loading', key = null) { if (key && state.activeNotifications.has(key)) { const existing = state.activeNotifications.get(key); existing.notification.style.opacity = '0'; setTimeout(() => existing.notification.remove(), 300); state.activeNotifications.delete(key); } const notification = document.createElement('div'); notification.className = `notification ${type}`; notification.innerHTML = ` ${type === 'loading' ? '
' : ''} ${message} `; elements.notificationContainer.appendChild(notification); if (type !== 'loading') { setTimeout(() => { notification.style.opacity = '0'; setTimeout(() => { notification.remove(); if (key) state.activeNotifications.delete(key); }, 300); }, 3000); } if (key) { state.activeNotifications.set(key, { notification, message, type }); } return notification; } function updateNotification(notification, message, type, key = null) { if (key && state.activeNotifications.has(key) && state.activeNotifications.get(key).notification !== notification) { const existing = state.activeNotifications.get(key); existing.notification.style.opacity = '0'; setTimeout(() => existing.notification.remove(), 300); } notification.className = `notification ${type}`; notification.innerHTML = `${message}`; if (type !== 'loading') { setTimeout(() => { notification.style.opacity = '0'; setTimeout(() => { notification.remove(); if (key) state.activeNotifications.delete(key); }, 300); }, 3000); } if (key) { state.activeNotifications.set(key, { notification, message, type }); } } function initializeCharts() { if (memoryMeter || cpuMeter) { return; } const memoryCanvas = document.getElementById('memoryMeter'); if (!memoryCanvas) { console.error('Memory Meter canvas not found'); showNotification('Unable to display memory usage chart: Canvas element missing', 'error'); return; } const memoryCtx = memoryCanvas.getContext('2d'); if (!memoryCtx) { console.error('Failed to acquire 2D context for Memory Meter'); showNotification('Unable to display memory usage chart: Invalid canvas context', 'error'); return; } memoryMeter = new Chart(memoryCtx, { type: 'doughnut', data: { datasets: [{ data: [0, 100], backgroundColor: ['#EF4444', '#4B5563'], borderWidth: 0, circumference: 180, rotation: 270 }] }, options: { cutout: '80%', responsive: false, plugins: { title: { display: false }, tooltip: { enabled: false } } } }); const cpuCanvas = document.getElementById('cpuMeter'); if (!cpuCanvas) { console.error('CPU Meter canvas not found'); showNotification('Unable to display CPU usage chart: Canvas element missing', 'error'); return; } const cpuCtx = cpuCanvas.getContext('2d'); if (!cpuCtx) { console.error('Failed to acquire 2D context for CPU Meter'); showNotification('Unable to display CPU usage chart: Invalid canvas context', 'error'); return; } cpuMeter = new Chart(cpuCtx, { type: 'doughnut', data: { datasets: [{ data: [0, 600], backgroundColor: ['#3B82F6', '#4B5563'], borderWidth: 0, circumference: 180, rotation: 270 }] }, options: { cutout: '80%', responsive: false, plugins: { title: { display: false }, tooltip: { enabled: false } } } }); } function initializeTerminal() { if (terminal) { terminal.clear(); } else { terminal = new Terminal({ rows: 8, fontSize: 14, fontFamily: 'monospace', theme: { background: '#1f2937', foreground: '#ffffff', cursor: '#ffffff' }, scrollback: 1000, rendererType: 'canvas' }); fitAddon = new FitAddon.FitAddon(); terminal.loadAddon(fitAddon); terminal.open(elements.dockerLogsTerminal); terminal.element.style.border = 'none'; } fitAddon.fit(); window.addEventListener('resize', () => { if (fitAddon && terminal) { fitAddon.fit(); } }); } function connectWebSocket() { if (ws && ws.readyState === WebSocket.OPEN) { return; } ws = new WebSocket(`wss://${window.location.host}/ws?apiKey=${encodeURIComponent(apiKey)}`); ws.onopen = () => { showMainContent(); ws.send(JSON.stringify({ type: 'subscribe', endpoints: [ 'docker', 'docker-logs', 'hello', 'time', 'list-players', 'mod-list', 'log', 'website', 'map', 'my-link-cache', 'my-geyser-cache', 'my-sftp-cache', 'backup', 'sftp-status' ] })); responseTimeout = setTimeout(() => { showNotification('No response from server. Please check your connection or API key.', 'error', 'ws-timeout'); handleLogout(); }, 5000); }; ws.onmessage = (event) => { try { clearTimeout(responseTimeout); const message = JSON.parse(event.data); if (message.requestId && pendingRequests.has(message.requestId)) { const { resolve, reject, notification, key } = pendingRequests.get(message.requestId); pendingRequests.delete(message.requestId); if (message.error) { updateNotification(notification, `Error: ${message.error}`, 'error', key); if (message.error.includes('Missing token') || message.error.includes('HTTP 403')) { showNotification('Invalid or expired API key. Please log in again.', 'error', 'auth-error'); elements.loginError.classList.remove('hidden'); elements.loginError.textContent = 'Invalid API key. Please try again.'; handleLogout(); } reject(new Error(message.error)); } else { updateNotification(notification, message.message || 'Action completed successfully', 'success', key); resolve(message.data || message); } } else { updateUI(message); } } catch (error) { console.error('WebSocket message parsing error:', error); showNotification('Error processing server data. Please try again.', 'error', 'ws-parse-error'); } }; ws.onclose = () => { showLoginPage(); clearTimeout(responseTimeout); if (terminal) { terminal.clear(); } setTimeout(connectWebSocket, 3000); }; ws.onerror = (error) => { console.error('WebSocket error:', error); showNotification('Failed to connect to server. Please check your network.', 'error', 'ws-error'); }; } function wsRequest(endpoint, method = 'GET', body = null) { return new Promise((resolve, reject) => { if (!ws || ws.readyState !== WebSocket.OPEN) { showNotification('Not connected to server. Please log in.', 'error', 'ws-disconnected'); reject(new Error('WebSocket not connected')); return; } const requestId = crypto.randomUUID(); const action = endpoint.replace('/', '').replace('-', ' '); const key = `action-${action}`; const notification = showNotification(`Processing ${action}...`, 'loading', key); pendingRequests.set(requestId, { resolve, reject, notification, key }); ws.send(JSON.stringify({ type: 'request', requestId, endpoint, method, body })); }); } function updateUI(message) { if (message.type === 'docker') { updateDockerUI(message); } else if (message.type === 'docker-logs') { updateDockerLogsUI(message); } else if (message.type === 'connection-status') { updateConnectionStatusUI(message); } else if (message.type === 'geyser-status') { updateGeyserStatusUI(message); } else if (message.type === 'sftp-status') { updateSftpStatusUI(message); } else if (message.type === 'my-sftp-cache') { updateSftpCacheUI(message); } else if (message.type === 'update-mods') { updateModsUI(message); } else if (message.type === 'backup') { console.log('Received backup message:', message); } else { updateNonDockerUI(message); } } function updateDockerUI(message) { if (message.error) { if (elements.serverStatus) elements.serverStatus.textContent = 'Not Running'; toggleSections('Not Running'); return; } const memoryPercent = parseFloat(message.data?.memory?.percent) || 0; if (state.memoryPercent !== memoryPercent && elements.memoryPercent && memoryMeter) { memoryMeter.data.datasets[0].data = [memoryPercent, 100 - memoryPercent]; memoryMeter.update(); elements.memoryPercent.textContent = `${memoryPercent.toFixed(1)}%`; state.memoryPercent = memoryPercent; } const cpuPercent = parseFloat(message.data?.cpu) || 0; if (state.cpuPercent !== cpuPercent && elements.cpuPercent && cpuMeter) { const scaledCpuPercent = Math.min((cpuPercent / 600) * 100, 100); cpuMeter.data.datasets[0].data = [scaledCpuPercent, 100 - scaledCpuPercent]; cpuMeter.update(); elements.cpuPercent.textContent = `${cpuPercent.toFixed(1)}%`; state.cpuPercent = cpuPercent; } const status = message.data?.status || 'Unknown'; if (elements.serverStatus) { elements.serverStatus.textContent = status; state.serverStatus = status; toggleSections(status); } } function toggleSections(status) { const sections = document.querySelectorAll('.bg-gray-800.p-6.rounded-lg.shadow-lg.mb-6'); const serverStatusSection = document.querySelector('.bg-gray-800.p-6.rounded-lg.shadow-lg.mb-6[data-section="server-status"]'); const editPropertiesBtn = elements.editPropertiesBtn; const updateModsBtn = elements.updateModsBtn; const backupBtn = elements.backupBtn; const sftpBtn = elements.sftpBtn; const startBtn = document.getElementById('startBtn'); const stopBtn = elements.stopBtn; const restartBtn = elements.restartBtn; if (startBtn) { if (status.toLowerCase() === 'running') { startBtn.disabled = true; startBtn.classList.add('disabled-btn'); } else { startBtn.disabled = false; startBtn.classList.remove('disabled-btn'); } } if (status.toLowerCase() !== 'running') { sections.forEach(section => { if (section !== serverStatusSection) { section.classList.add('hidden'); } }); if (editPropertiesBtn) { editPropertiesBtn.classList.add('hidden'); } if (updateModsBtn) { updateModsBtn.classList.add('hidden'); } if (backupBtn) { backupBtn.classList.add('hidden'); } if (sftpBtn) { sftpBtn.classList.add('hidden'); } if (stopBtn) { stopBtn.disabled = true; stopBtn.classList.add('disabled-btn'); } if (restartBtn) { restartBtn.disabled = true; restartBtn.classList.add('disabled-btn'); } if (!state.hasShownStartNotification) { showNotification('Server is stopped. Click "Start" to enable all features.', 'error', 'server-stopped'); state.hasShownStartNotification = true; } } else { sections.forEach(section => { section.classList.remove('hidden'); }); if (editPropertiesBtn) { editPropertiesBtn.classList.remove('hidden'); } if (updateModsBtn) { updateModsBtn.classList.remove('hidden'); } if (backupBtn) { backupBtn.classList.remove('hidden'); } if (sftpBtn) { sftpBtn.classList.remove('hidden'); } if (stopBtn) { stopBtn.disabled = false; stopBtn.classList.remove('disabled-btn'); } if (restartBtn) { restartBtn.disabled = false; restartBtn.classList.remove('disabled-btn'); } state.hasShownStartNotification = false; } } function updateDockerLogsUI(message) { if (message.error) { return; } if (message.data?.log && terminal) { const logLines = message.data.log.split('\n'); const cleanedLog = logLines .map(line => line .replace(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z\s*/, '') .replace(/\u001b\[32m0\|MineCraft Server\s*\|\s*\u001b\[39m/, '') ) .join('\n'); terminal.write(cleanedLog); const lines = terminal.buffer.active.length; if (lines > 8) { terminal.scrollToLine(lines - 8); } if (fitAddon) { fitAddon.fit(); } } } function updateConnectionStatusUI(message) { if (message.data?.isOnline !== undefined && elements.connectionStatus) { const statusIcon = message.data.isOnline ? '✅' : '❌'; if (state.connectionStatus !== statusIcon) { elements.connectionStatus.textContent = statusIcon; state.connectionStatus = statusIcon; } } } function updateGeyserStatusUI(message) { if (message.data?.isOnline !== undefined && elements.geyserStatus) { const statusIcon = message.data.isOnline ? '✅' : '❌'; if (state.geyserStatus !== statusIcon) { elements.geyserStatus.textContent = statusIcon; state.geyserStatus = statusIcon; } } } function updateSftpStatusUI(message) { if (message.data?.isOnline !== undefined && elements.sftpStatus) { const statusIcon = message.data.isOnline ? '✅' : '❌'; isSftpOnline = message.data.isOnline; if (state.sftpStatus !== statusIcon) { elements.sftpStatus.textContent = statusIcon; state.sftpStatus = statusIcon; } } } function updateSftpCacheUI(message) { if (message.data?.hostname && message.data?.port && message.data?.user && message.data?.password) { sftpCredentials = { hostname: message.data.hostname, port: message.data.port, user: message.data.user, password: message.data.password }; const sftpLinkText = `${message.data.hostname}:${message.data.port} (Auth: User: ${message.data.user} | Pass: ${message.data.password})`; if (state.sftpLink !== sftpLinkText && elements.sftpLink) { elements.sftpLink.textContent = sftpLinkText; state.sftpLink = sftpLinkText; } } } function updateModsUI(message) { if (message.error) { elements.updateModsOutput.textContent = `Error: ${message.error}`; } else if (message.output) { elements.updateModsOutput.textContent = message.output; } elements.updateModsModal.classList.remove('hidden'); } function updateNonDockerUI(message) { if (message.error) { if (message.error.includes('Missing token') || message.error.includes('HTTP 403')) { showNotification('Invalid or expired API key. Please log in again.', 'error', 'auth-error'); elements.loginError.classList.remove('hidden'); elements.loginError.textContent = 'Invalid API key. Please try again.'; handleLogout(); } return; } if (message.type === 'hello' && message.data?.message) { const user = message.data.message.split(', ')[1]?.replace('!', '').trim() || 'Unknown'; if (state.user !== user && elements.user) { elements.user.textContent = user; state.user = user; if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'updateUser', user })); } } } if (message.type === 'time' && message.data?.keyexpireString) { if (state.keyExpiry !== message.data.keyexpireString && elements.keyExpiry) { const expiryDate = new Date(message.data.keyexpireString); const formattedDate = expiryDate.toLocaleString('en-US', { month: 'long', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: true, timeZoneName: 'short' }); elements.keyExpiry.textContent = formattedDate; state.keyExpiry = message.data.keyexpireString; } } if (message.type === 'list-players' && message.data?.players) { const players = message.data.players || []; state.currentPlayers = players; const isSinglePlayer = players.length <= 1; const playerListHtml = players.length > 0 ? players.map(player => `${result.title}
${result.description}
Downloads: ${result.downloads}
No results found.
'; updatePagination(); elements.closeSearchBtn.classList.remove('hidden'); updateNotification(notification, `Found ${totalResults} mod${totalResults === 1 ? '' : 's'} for "${mod}"`, 'success', key); document.querySelectorAll('.install-mod').forEach(button => { button.addEventListener('click', async () => { const modId = button.getAttribute('data-mod-id'); const installKey = `action-install-mod-${modId}`; try { await wsRequest('/install', 'POST', { mod: modId }); const modResponse = await wsRequest('/mod-list'); updateNonDockerUI({ type: 'mod-list', data: modResponse }); showNotification(`Mod ${modId} installed successfully`, 'success', installKey); } catch (error) { console.error('Install mod error:', error); showNotification(`Failed to install mod ${modId}: ${error.message}`, 'error', installKey); } }); }); } } catch (error) { console.error('Search mods error:', error); showNotification(`Failed to search mods: ${error.message}`, 'error', key); } } else { showNotification('Please enter a mod name to search.', 'error', 'search-mods-error'); } } function renderModList() { const filteredMods = state.allMods.filter(mod => mod.name.toLowerCase().includes(modListSearchQuery.toLowerCase()) ); const startIndex = (modListCurrentPage - 1) * resultsPerPage; const endIndex = startIndex + resultsPerPage; const paginatedMods = filteredMods.slice(startIndex, endIndex); const modListHtml = paginatedMods.length > 0 ? paginatedMods.map(mod => `${mod.name} (${mod.version})
ID: ${mod.id}
No mods found.
'; if (state.modListHtml !== modListHtml && elements.modList) { elements.modList.innerHTML = modListHtml; state.modListHtml = modListHtml; document.querySelectorAll('.uninstall-mod').forEach(button => { button.addEventListener('click', async () => { const modId = button.getAttribute('data-mod-id'); const key = `action-uninstall-mod-${modId}`; try { await wsRequest('/uninstall', 'POST', { mod: modId }); const response = await wsRequest('/mod-list'); updateNonDockerUI({ type: 'mod-list', data: response }); showNotification(`Mod ${modId} uninstalled successfully`, 'success', key); } catch (error) { console.error('Uninstall mod error:', error); showNotification(`Failed to uninstall mod ${modId}: ${error.message}`, 'error', key); } }); }); } updateModListPagination(); } async function sendConsoleCommand() { const command = elements.consoleInput.value.trim(); if (command) { try { const key = `action-console-command`; const notification = showNotification(`Executing command "${command}"...`, 'loading', key); const response = await wsRequest('/console', 'POST', { command }); if (elements.consoleOutput) { elements.consoleOutput.textContent += `> ${command}\n${response.message}\n`; elements.consoleInput.value = ''; updateNotification(notification, `Command "${command}" executed successfully`, 'success', key); } } catch (error) { if (elements.consoleOutput) { elements.consoleOutput.textContent += `> ${command}\nError: ${error.message}\n`; } console.error('Console command error:', error); showNotification(`Failed to execute command "${command}": ${error.message}`, 'error', 'console-error'); } } } async function sendTellMessage() { const player = elements.tellPlayerName.textContent.trim(); const message = elements.tellMessage.value.trim(); if (!player || !message) { showNotification('Player name and message are required.', 'error', 'tell-error'); return; } try { const requestId = crypto.randomUUID(); const key = `action-tell-player-${player}`; const notification = showNotification(`Sending message to ${player}...`, 'loading', key); pendingRequests.set(requestId, { resolve: () => { updateNotification(notification, `Message sent to ${player} successfully`, 'success', key); elements.tellModal.classList.add('hidden'); }, reject: (error) => { updateNotification(notification, `Failed to send message to ${player}: ${error.message}`, 'error', key); }, notification, key }); ws.send(JSON.stringify({ type: 'tell-player', requestId, player, message })); } catch (error) { console.error(`Send message to ${player} error:`, error); showNotification(`Failed to send message to ${player}: ${error.message}`, 'error', 'tell-error'); } } async function sendTeleportCommand() { const player = elements.teleportPlayerName.textContent.trim(); const destination = elements.teleportDestination.value.trim(); if (!player || !destination) { showNotification('Source and destination players are required.', 'error', 'teleport-error'); return; } try { const command = `tp ${player} ${destination}`; const key = `action-teleport-player-${player}`; const notification = showNotification(`Teleporting ${player} to ${destination}...`, 'loading', key); const response = await wsRequest('/console', 'POST', { command }); updateNotification(notification, `Teleported ${player} to ${destination} successfully`, 'success', key); elements.teleportModal.classList.add('hidden'); } catch (error) { console.error(`Teleport ${player} error:`, error); showNotification(`Failed to teleport ${player}: ${error.message}`, 'error', 'teleport-error'); } } async function sendEffectCommand() { const player = elements.effectPlayerName.textContent.trim(); const effectData = elements.effectSelect.value.split(':'); const effect = effectData[0]; const duration = parseInt(effectData[1], 10); const amplifier = parseInt(effectData[2], 10); if (!player || !effect) { showNotification('Player name and effect are required.', 'error', 'effect-error'); return; } try { const command = `effect give ${player} minecraft:${effect} ${duration} ${amplifier}`; const key = `action-effect-player-${player}`; const notification = showNotification(`Applying ${effect} to ${player}...`, 'loading', key); const response = await wsRequest('/console', 'POST', { command }); updateNotification(notification, `Applied ${effect} to ${player} successfully`, 'success', key); elements.effectModal.classList.add('hidden'); } catch (error) { console.error(`Apply effect to ${player} error:`, error); showNotification(`Failed to apply effect to ${player}: ${error.message}`, 'error', 'effect-error'); } } function addItemField() { const itemList = elements.itemList; const itemEntry = document.createElement('div'); itemEntry.className = 'item-entry flex space-x-2 items-center'; itemEntry.innerHTML = ` `; itemList.appendChild(itemEntry); itemEntry.querySelector('.remove-item').addEventListener('click', () => { itemEntry.remove(); }); } function resetItemFields() { elements.itemList.innerHTML = ''; addItemField(); } async function sendGiveCommand() { const player = elements.givePlayerName.textContent.trim(); const loadout = elements.loadoutSelect.value; let items = []; if (loadout === 'custom') { const itemEntries = document.querySelectorAll('.item-entry'); items = Array.from(itemEntries).map(entry => { const item = entry.querySelector('.item-name').value.trim(); const amount = parseInt(entry.querySelector('.item-amount').value, 10); return { item, amount }; }).filter(itemData => itemData.item && itemData.amount > 0); if (items.length === 0) { showNotification('At least one valid item and quantity are required.', 'error', 'give-error'); return; } } else { items = loadouts[loadout] || []; } try { const key = `action-give-player-${player}`; const notification = showNotification(`Giving items to ${player}...`, 'loading', key); for (const itemData of items) { const { item, amount, enchantments, type } = itemData; const requestId = crypto.randomUUID(); pendingRequests.set(requestId, { resolve: () => { updateNotification(notification, `Gave ${amount} ${item}${type ? ` (${type})` : ''} to ${player}`, 'success', key); }, reject: (error) => { updateNotification(notification, `Failed to give ${item} to ${player}: ${error.message}`, 'error', key); }, notification, key }); ws.send(JSON.stringify({ type: 'give-player', requestId, player, item, amount, enchantments: enchantments || undefined, potionType: type || undefined })); } elements.giveModal.classList.add('hidden'); } catch (error) { console.error(`Give items to ${player} error:`, error); showNotification(`Failed to give items to ${player}: ${error.message}`, 'error', 'give-error'); } } function parseServerProperties(content) { const properties = {}; const lines = content.split('\n'); lines.forEach(line => { if (line.trim() && !line.trim().startsWith('#')) { const [key, value] = line.split('=', 2).map(part => part.trim()); if (key && value !== undefined) { properties[key] = value; } } }); return properties; } function renderPropertiesFields(properties) { const fieldsContainer = elements.propertiesFields; fieldsContainer.innerHTML = ''; Object.entries(properties).forEach(([key, value]) => { if (filteredSettings.includes(key)) { return; } const fieldDiv = document.createElement('div'); fieldDiv.className = 'flex items-center space-x-2'; let inputType = 'text'; let isBoolean = value.toLowerCase() === 'true' || value.toLowerCase() === 'false'; if (isBoolean) { inputType = 'checkbox'; } else if (/^\d+$/.test(value) && !isNaN(parseInt(value))) { inputType = 'number'; } const label = document.createElement('label'); label.textContent = key; label.className = 'w-1/3 text-sm font-medium'; label.setAttribute('for', `prop-${key}`); const input = document.createElement('input'); input.id = `prop-${key}`; input.name = key; input.className = 'bg-gray-700 px-4 py-2 rounded text-white w-2/3'; if (inputType === 'checkbox') { input.type = 'checkbox'; input.checked = value.toLowerCase() === 'true'; } else { input.type = inputType; input.value = value; if (inputType === 'number') { input.min = '0'; } } fieldDiv.appendChild(label); fieldDiv.appendChild(input); fieldsContainer.appendChild(fieldDiv); }); } function propertiesToString(properties) { let header = `#Minecraft server properties\n#${new Date().toUTCString()}\n`; return header + Object.entries(properties) .map(([key, value]) => `${key}=${value}`) .join('\n'); } async function fetchServerProperties() { try { const key = `action-fetch-properties`; const notification = showNotification('Loading server properties...', 'loading', key); const response = await wsRequest('/server-properties', 'GET'); if (response.error) { updateNotification(notification, `Failed to load server properties: ${response.error}`, 'error', key); return; } if (response.content && response.content.length > 4000) { updateNotification(notification, `Server properties file is too large to edit (${response.content.length} characters, max 4000)`, 'error', key); return; } allProperties = parseServerProperties(response.content || ''); const displayProperties = Object.fromEntries( Object.entries(allProperties).filter(([key]) => !filteredSettings.includes(key)) ); renderPropertiesFields(displayProperties); elements.editPropertiesModal.classList.remove('hidden'); updateNotification(notification, 'Server properties loaded successfully', 'success', key); } catch (error) { console.error('Fetch server properties error:', error); showNotification(`Failed to load server properties: ${error.message}`, 'error', 'fetch-properties-error'); } } async function saveServerProperties() { try { const key = `action-save-properties`; const notification = showNotification('Saving server properties...', 'loading', key); const properties = {}; const inputs = elements.propertiesFields.querySelectorAll('input'); inputs.forEach(input => { const key = input.name; let value = input.type === 'checkbox' ? input.checked.toString() : input.value.trim(); if (value !== '') { properties[key] = value; } }); const fullProperties = { ...allProperties, ...properties }; const content = propertiesToString(fullProperties); const response = await wsRequest('/server-properties', 'POST', { content }); if (response.error) { updateNotification(notification, `Failed to save server properties: ${response.error}`, 'error', key); return; } elements.editPropertiesModal.classList.add('hidden'); updateNotification(notification, 'Server properties saved successfully', 'success', key); } catch (error) { console.error('Save server properties error:', error); showNotification(`Failed to save server properties: ${error.message}`, 'error', 'save-properties-error'); } } async function updateMods() { try { const key = `action-update-mods`; const response = await wsRequest('/update-mods', 'POST'); if (response.error) { elements.updateModsOutput.textContent = `Error: ${response.error}`; showNotification(`Failed to update mods: ${response.error}`, 'error', key); } else { const output = response.output || 'No output from mod update.'; elements.updateModsOutput.textContent = output; showNotification('Mods updated successfully', 'success', key); } elements.updateModsModal.classList.remove('hidden'); } catch (error) { console.error('Update mods error:', error); elements.updateModsOutput.textContent = `Error: ${error.message}`; elements.updateModsModal.classList.remove('hidden'); showNotification(`Failed to update mods: ${error.message}`, 'error', 'update-mods-error'); } } async function createBackup() { try { const key = `action-backup`; const notification = showNotification('Creating backup... Download will start when ready.', 'loading', key); const response = await wsRequest('/backup', 'POST'); if (response.error) { updateNotification(notification, `Failed to create backup: ${response.error}`, 'error', key); return; } const downloadURL = response.downloadURL; if (downloadURL) { const link = document.createElement('a'); link.href = downloadURL; link.download = ''; document.body.appendChild(link); link.click(); document.body.removeChild(link); updateNotification(notification, 'Backup created and download started', 'success', key); } else { updateNotification(notification, 'Backup created but download URL unavailable', 'error', key); } } catch (error) { console.error('Create backup error:', error); showNotification(`Failed to create backup: ${error.message}`, 'error', 'backup-error'); } } async function connectSftp() { if (!isSftpOnline) { showNotification('SFTP is offline. Please try again later.', 'error', 'sftp-offline'); return; } if (!sftpCredentials) { showNotification('SFTP credentials not available.', 'error', 'sftp-credentials'); return; } try { const key = `action-sftp-connect`; const notification = showNotification('Connecting to SFTP...', 'loading', key); const response = await fetch('https://sftp.my-mc.link/auto-connection', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ host: sftpCredentials.hostname.replace('sftp://', ''), port: sftpCredentials.port, username: sftpCredentials.user, password: sftpCredentials.password, }), }); const result = await response.json(); if (result.success && result.connectionUrl) { elements.sftpIframe.src = result.connectionUrl; elements.sftpBrowserSection.style.display = 'block'; updateNotification(notification, 'SFTP connected successfully', 'success', key); } else { updateNotification(notification, 'Failed to establish SFTP connection.', 'error', key); } } catch (error) { console.error('SFTP connection error:', error); showNotification(`Failed to connect to SFTP: ${error.message}`, 'error', 'sftp-connect-error'); } } elements.loginBtn.addEventListener('click', () => { apiKey = elements.loginApiKey.value.trim(); if (apiKey) { localStorage.setItem('apiKey', apiKey); elements.loginError.classList.add('hidden'); connectWebSocket(); } else { elements.loginError.classList.remove('hidden'); elements.loginError.textContent = 'Please enter an API key.'; } }); elements.apiKeyInput.addEventListener('change', handleApiKeyChange); elements.generateMyLinkBtn.addEventListener('click', async () => { try { const key = `action-my-link`; await wsRequest('/my-link'); showNotification('Connection link generated successfully', 'success', key); } catch (error) { console.error('Generate connection link error:', error); showNotification(`Failed to generate connection link: ${error.message}`, 'error', 'my-link-error'); } }); elements.generateGeyserLinkBtn.addEventListener('click', async () => { try { const key = `action-my-geyser-link`; await wsRequest('/my-geyser-link'); showNotification('Geyser link generated successfully', 'success', key); } catch (error) { console.error('Generate geyser link error:', error); showNotification(`Failed to generate Geyser link: ${error.message}`, 'error', 'geyser-link-error'); } }); elements.generateSftpLinkBtn.addEventListener('click', async () => { try { const key = `action-my-sftp`; await wsRequest('/my-sftp'); showNotification('SFTP link generated successfully', 'success', key); } catch (error) { console.error('Generate SFTP link error:', error); showNotification(`Failed to generate SFTP link: ${error.message}`, 'error', 'sftp-link-error'); } }); elements.sftpBtn.addEventListener('click', connectSftp); document.getElementById('refresh').addEventListener('click', async () => { if (ws && ws.readyState === WebSocket.OPEN) { const key = `action-refresh`; const notification = showNotification('Refreshing server data...', 'loading', key); ws.send(JSON.stringify({ type: 'refresh' })); initializeTerminal(); setTimeout(() => updateNotification(notification, 'Server data refreshed successfully', 'success', key), 1000); } else { showNotification('Not connected to server. Please log in.', 'error', 'ws-disconnected'); } }); document.getElementById('startBtn').addEventListener('click', async () => { try { const key = `action-start`; const notification = showNotification('Starting server...', 'loading', key); await wsRequest('/start'); initializeTerminal(); if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'subscribe', endpoints: ['docker', 'docker-logs'] })); const messageHandler = (event) => { try { const message = JSON.parse(event.data); if (message.type === 'docker') { state.serverStatus = message.data?.status || 'Unknown'; elements.serverStatus.textContent = state.serverStatus; toggleSections(state.serverStatus); if (message.data?.status === 'running') { updateNotification(notification, 'Server started successfully', 'success', key); ws.removeEventListener('message', messageHandler); } } } catch (error) { console.error('Error parsing WebSocket message:', error); } }; ws.addEventListener('message', messageHandler); setTimeout(() => { if (ws && ws.readyState === WebSocket.OPEN) { ws.removeEventListener('message', messageHandler); if (state.serverStatus !== 'running') { updateNotification(notification, 'Failed to start server. Please try again.', 'error', key); toggleSections(state.serverStatus); } } }, 30000); } else { updateNotification(notification, 'Not connected to server. Please log in.', 'error', key); } } catch (error) { console.error('Start server error:', error); showNotification(`Failed to start server: ${error.message}`, 'error', 'start-error'); } }); document.getElementById('stopBtn').addEventListener('click', async () => { try { const key = `action-stop`; const notification = showNotification('Stopping server...', 'loading', key); await wsRequest('/stop'); updateNotification(notification, 'Server stopped successfully', 'success', key); } catch (error) { console.error('Stop server error:', error); showNotification(`Failed to stop server: ${error.message}`, 'error', 'stop-error'); } }); document.getElementById('restartBtn').addEventListener('click', async () => { try { const key = `action-restart`; const notification = showNotification('Restarting server...', 'loading', key); await wsRequest('/restart'); initializeTerminal(); if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'subscribe', endpoints: ['docker', 'docker-logs'] })); const messageHandler = (event) => { try { const message = JSON.parse(event.data); if (message.type === 'docker') { state.serverStatus = message.data?.status || 'Unknown'; elements.serverStatus.textContent = state.serverStatus; toggleSections(state.serverStatus); if (message.data?.status === 'running') { updateNotification(notification, 'Server restarted successfully', 'success', key); ws.removeEventListener('message', messageHandler); } } } catch (error) { console.error('Error parsing WebSocket message:', error); } }; ws.addEventListener('message', messageHandler); setTimeout(() => { if (ws && ws.readyState === WebSocket.OPEN) { ws.removeEventListener('message', messageHandler); if (state.serverStatus !== 'running') { updateNotification(notification, 'Failed to restart server. Please try again.', 'error', key); toggleSections(state.serverStatus); } } }, 60000); } else { updateNotification(notification, 'Not connected to server. Please log in.', 'error', key); } } catch (error) { console.error('Restart server error:', error); showNotification(`Failed to restart server: ${error.message}`, 'error', 'restart-error'); } }); elements.updateModsBtn.addEventListener('click', updateMods); elements.closeUpdateModsBtn.addEventListener('click', () => { elements.updateModsModal.classList.add('hidden'); elements.updateModsOutput.textContent = ''; }); elements.updateModsModal.querySelector('.modal-close').addEventListener('click', () => { elements.updateModsModal.classList.add('hidden'); elements.updateModsOutput.textContent = ''; }); elements.backupBtn.addEventListener('click', createBackup); document.getElementById('searchBtn').addEventListener('click', () => { searchMods(1); }); document.getElementById('sendConsole').addEventListener('click', sendConsoleCommand); elements.closeSearchBtn.addEventListener('click', closeSearch); elements.tellModal.querySelector('.modal-close').addEventListener('click', () => { elements.tellModal.classList.add('hidden'); }); elements.giveModal.querySelector('.modal-close').addEventListener('click', () => { elements.giveModal.classList.add('hidden'); }); elements.teleportModal.querySelector('.modal-close').addEventListener('click', () => { elements.teleportModal.classList.add('hidden'); }); elements.effectModal.querySelector('.modal-close').addEventListener('click', () => { elements.effectModal.classList.add('hidden'); }); elements.loadoutSelect.addEventListener('change', () => { const isCustom = elements.loadoutSelect.value === 'custom'; elements.customGiveFields.classList.toggle('hidden', !isCustom); if (isCustom) { resetItemFields(); } }); elements.tellMessage.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendTellMessage(); } }); elements.itemList.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.target.classList.contains('item-name') || e.target.classList.contains('item-amount'))) { e.preventDefault(); sendGiveCommand(); } }); elements.teleportForm.addEventListener('submit', (e) => { e.preventDefault(); sendTeleportCommand(); }); elements.effectForm.addEventListener('submit', (e) => { e.preventDefault(); sendEffectCommand(); }); elements.tellForm.addEventListener('submit', (e) => { e.preventDefault(); sendTellMessage(); }); elements.giveForm.addEventListener('submit', (e) => { e.preventDefault(); sendGiveCommand(); }); elements.addItemBtn.addEventListener('click', addItemField); elements.editPropertiesBtn.addEventListener('click', fetchServerProperties); elements.editPropertiesModal.querySelector('.modal-close').addEventListener('click', () => { elements.editPropertiesModal.classList.add('hidden'); }); elements.editPropertiesForm.addEventListener('submit', (e) => { e.preventDefault(); saveServerProperties(); }); elements.clearModListSearch.addEventListener('click', clearModListSearch); const debouncedModListSearch = debounce((query) => { modListSearchQuery = query.trim(); modListCurrentPage = 1; elements.clearModListSearch.classList.toggle('hidden', !modListSearchQuery); renderModList(); }, 300); elements.modListSearch.addEventListener('input', (e) => { debouncedModListSearch(e.target.value); }); if (apiKey) { connectWebSocket(); } else { showLoginPage(); } });