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 allProperties = {}; 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') }; 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: '' }; function showNotification(message, type = 'loading') { 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(), 300); }, 3000); } return notification; } function updateNotification(notification, message, type) { notification.className = `notification ${type}`; notification.innerHTML = `${message}`; if (type !== 'loading') { setTimeout(() => { notification.style.opacity = '0'; setTimeout(() => notification.remove(), 300); }, 3000); } } function initializeCharts() { if (memoryMeter || cpuMeter) { return; } const memoryCanvas = document.getElementById('memoryMeter'); if (!memoryCanvas) { console.error('Memory Meter canvas not found'); showNotification('Failed to initialize memory chart: Canvas not found', 'error'); return; } const memoryCtx = memoryCanvas.getContext('2d'); if (!memoryCtx) { console.error('Failed to acquire 2D context for Memory Meter'); showNotification('Failed to initialize memory 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('Failed to initialize CPU chart: Canvas not found', 'error'); return; } const cpuCtx = cpuCanvas.getContext('2d'); if (!cpuCtx) { console.error('Failed to acquire 2D context for CPU Meter'); showNotification('Failed to initialize CPU 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' ] })); responseTimeout = setTimeout(() => { showNotification('No response from server. Please check connection or API key.', 'error'); 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 } = pendingRequests.get(message.requestId); pendingRequests.delete(message.requestId); if (message.error) { updateNotification(notification, `Error: ${message.error}`, 'error'); if (message.error.includes('Missing token') || message.error.includes('HTTP 403')) { showNotification('Invalid or missing API key. Please log in again.', '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', 'success'); resolve(message.data || message); } } else { updateUI(message); } } catch (error) { console.error('WebSocket message parsing error:', error); showNotification('Error processing server data', 'error'); } }; ws.onclose = () => { showLoginPage(); clearTimeout(responseTimeout); if (terminal) { terminal.clear(); } setTimeout(connectWebSocket, 3000); }; ws.onerror = (error) => { console.error('WebSocket error:', error); showNotification('WebSocket connection error', 'error'); }; } function wsRequest(endpoint, method = 'GET', body = null) { return new Promise((resolve, reject) => { if (!ws || ws.readyState !== WebSocket.OPEN) { showNotification('No connection to server', 'error'); reject(new Error('WebSocket not connected')); return; } const requestId = crypto.randomUUID(); const notification = showNotification(`${method} ${endpoint}...`); pendingRequests.set(requestId, { resolve, reject, notification }); 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 === 'update-mods') { updateModsUI(message); } else if (message.type === 'backup') { // Backup messages are primarily handled via wsRequest responses console.log('Received backup message:', message); } else { updateNonDockerUI(message); } } function updateDockerUI(message) { if (message.error) { if (elements.serverStatus) elements.serverStatus.textContent = 'Not Running'; toggleSections('error'); 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 (state.serverStatus !== status && 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 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 (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 not running. Please click the "Start" button to enable all features.', 'error'); 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 (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 ? '✅' : '❌'; if (state.sftpStatus !== statusIcon) { elements.sftpStatus.textContent = statusIcon; state.sftpStatus = statusIcon; } } } 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 missing API key. Please log in again.', '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 || []; const playerListHtml = players.length > 0 ? players.map(player => `
${player}
`).join('') : 'None'; if (state.playerList !== playerListHtml && elements.playerList) { elements.playerList.innerHTML = playerListHtml; state.playerList = playerListHtml; document.querySelectorAll('.kick-player').forEach(button => { button.addEventListener('click', () => { const player = button.getAttribute('data-player').trim(); if (!player) { showNotification('Invalid player name', 'error'); return; } try { const requestId = crypto.randomUUID(); const notification = showNotification(`Kicking ${player}...`); pendingRequests.set(requestId, { resolve: () => { updateNotification(notification, `${player} kicked`, 'success'); wsRequest('/list-players').then(response => { updateNonDockerUI({ type: 'list-players', data: response }); }); }, reject: (error) => { updateNotification(notification, `Failed to kick ${player}: ${error.message}`, 'error'); }, notification }); ws.send(JSON.stringify({ type: 'kick-player', requestId, player })); } catch (error) { console.error(`Kick player ${player} error:`, error); showNotification(`Failed to kick ${player}: ${error.message}`, 'error'); } }); }); document.querySelectorAll('.ban-player').forEach(button => { button.addEventListener('click', () => { const player = button.getAttribute('data-player').trim(); if (!player) { showNotification('Invalid player name', 'error'); return; } try { const requestId = crypto.randomUUID(); const notification = showNotification(`Banning ${player}...`); pendingRequests.set(requestId, { resolve: () => { updateNotification(notification, `${player} banned`, 'success'); wsRequest('/list-players').then(response => { updateNonDockerUI({ type: 'list-players', data: response }); }); }, reject: (error) => { updateNotification(notification, `Failed to ban ${player}: ${error.message}`, 'error'); }, notification }); ws.send(JSON.stringify({ type: 'ban-player', requestId, player })); } catch (error) { console.error(`Ban player ${player} error:`, error); showNotification(`Failed to ban ${player}: ${error.message}`, 'error'); } }); }); document.querySelectorAll('.op-player').forEach(button => { button.addEventListener('click', () => { const player = button.getAttribute('data-player').trim(); if (!player) { showNotification('Invalid player name', 'error'); return; } try { const requestId = crypto.randomUUID(); const notification = showNotification(`Opping ${player}...`); pendingRequests.set(requestId, { resolve: () => { updateNotification(notification, `${player} opped`, 'success'); wsRequest('/list-players').then(response => { updateNonDockerUI({ type: 'list-players', data: response }); }); }, reject: (error) => { updateNotification(notification, `Failed to op ${player}: ${error.message}`, 'error'); }, notification }); ws.send(JSON.stringify({ type: 'op-player', requestId, player })); } catch (error) { console.error(`Op player ${player} error:`, error); showNotification(`Failed to op ${player}: ${error.message}`, 'error'); } }); }); document.querySelectorAll('.deop-player').forEach(button => { button.addEventListener('click', () => { const player = button.getAttribute('data-player').trim(); if (!player) { showNotification('Invalid player name', 'error'); return; } try { const requestId = crypto.randomUUID(); const notification = showNotification(`Deopping ${player}...`); pendingRequests.set(requestId, { resolve: () => { updateNotification(notification, `${player} deopped`, 'success'); wsRequest('/list-players').then(response => { updateNonDockerUI({ type: 'list-players', data: response }); }); }, reject: (error) => { updateNotification(notification, `Failed to deop ${player}: ${error.message}`, 'error'); }, notification }); ws.send(JSON.stringify({ type: 'deop-player', requestId, player })); } catch (error) { console.error(`Deop player ${player} error:`, error); showNotification(`Failed to deop ${player}: ${error.message}`, 'error'); } }); }); document.querySelectorAll('.tell-player').forEach(button => { button.addEventListener('click', () => { const player = button.getAttribute('data-player').trim(); if (!player) { showNotification('Invalid player name', 'error'); return; } elements.tellPlayerName.textContent = player; elements.tellMessage.value = ''; elements.tellModal.classList.remove('hidden'); }); }); document.querySelectorAll('.give-player').forEach(button => { button.addEventListener('click', () => { const player = button.getAttribute('data-player').trim(); if (!player) { showNotification('Invalid player name', 'error'); return; } elements.givePlayerName.textContent = player; elements.loadoutSelect.value = 'custom'; elements.customGiveFields.classList.remove('hidden'); resetItemFields(); elements.giveModal.classList.remove('hidden'); }); }); } } if (message.type === 'mod-list' && message.data?.mods) { const modListHtml = message.data.mods.map(mod => `

${mod.name} (${mod.version})

ID: ${mod.id}

`).join(''); 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'); try { await wsRequest('/uninstall', 'POST', { mod: modId }); const response = await wsRequest('/mod-list'); updateNonDockerUI({ type: 'mod-list', data: response }); } catch (error) { console.error('Uninstall mod error:', error); } }); }); } } if (message.type === 'log' && message.data?.message) { if (state.logUrl !== message.data.message && elements.logUrl) { elements.logUrl.href = message.data.message; elements.logUrl.textContent = message.data.message; state.logUrl = message.data.message; } } if (message.type === 'website' && message.data?.message) { if (state.websiteUrl !== message.data.message && elements.websiteUrl) { elements.websiteUrl.href = message.data.message; elements.websiteUrl.textContent = message.data.message; state.websiteUrl = message.data.message; } } if (message.type === 'map' && message.data?.message) { if (state.mapUrl !== message.data.message && elements.mapUrl) { elements.mapUrl.href = message.data.message; elements.mapUrl.textContent = message.data.message; state.mapUrl = message.data.message; } } if (message.type === 'my-link-cache' && message.data?.hostname && message.data?.port) { const myLinkText = `${message.data.hostname}:${message.data.port}`; if (state.myLink !== myLinkText && elements.myLink) { elements.myLink.textContent = myLinkText; state.myLink = myLinkText; } } if (message.type === 'my-geyser-cache' && message.data?.hostname && message.data?.port) { const geyserLinkText = `${message.data.hostname}:${message.data.port}`; if (state.geyserLink !== geyserLinkText && elements.geyserLink) { elements.geyserLink.textContent = geyserLinkText; state.geyserLink = geyserLinkText; } } if (message.type === 'my-sftp-cache' && message.data?.hostname && message.data?.port && message.data?.user && 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; } } if (message.type === 'my-link' && message.data?.hostname && message.data?.port) { const myLinkText = `${message.data.hostname}:${message.data.port}`; if (state.myLink !== myLinkText && elements.myLink) { elements.myLink.textContent = myLinkText; state.myLink = myLinkText; } } if (message.type === 'my-geyser-link' && message.data?.hostname && message.data?.port) { const geyserLinkText = `${message.data.hostname}:${message.data.port}`; if (state.geyserLink !== geyserLinkText && elements.geyserLink) { elements.geyserLink.textContent = geyserLinkText; state.geyserLink = geyserLinkText; } } if (message.type === 'my-sftp' && message.data?.hostname && message.data?.port && message.data?.user) { const sftpLinkText = `${message.data.hostname}:${message.data.port} (User: ${message.data.user})`; if (state.sftpLink !== sftpLinkText && elements.sftpLink) { elements.sftpLink.textContent = sftpLinkText; state.sftpLink = sftpLinkText; } } } function showLoginPage() { if (!elements.loginPage || !elements.mainContent) { console.error('Required elements not found:', { loginPage: elements.loginPage, mainContent: elements.mainContent }); showNotification('Error: Page elements not found. Please refresh the page.', 'error'); return; } elements.loginPage.classList.remove('hidden'); elements.mainContent.classList.add('hidden'); elements.authControls.innerHTML = ''; elements.apiKeyInput = document.getElementById('apiKey'); elements.apiKeyInput.addEventListener('change', handleApiKeyChange); if (ws) { ws.close(); ws = null; } if (memoryMeter) { memoryMeter.destroy(); memoryMeter = null; } if (cpuMeter) { cpuMeter.destroy(); cpuMeter = null; } if (terminal) { terminal.clear(); } } function showMainContent() { if (!elements.loginPage || !elements.mainContent) { console.error('Required elements not found:', { loginPage: elements.loginPage, mainContent: elements.mainContent }); showNotification('Error: Page elements not found. Please refresh the page.', 'error'); return; } elements.loginPage.classList.add('hidden'); elements.mainContent.classList.remove('hidden'); elements.authControls.innerHTML = ''; const logoutBtn = document.getElementById('logoutBtn'); if (logoutBtn) { logoutBtn.addEventListener('click', handleLogout); } else { console.error('Logout button not found after insertion'); } initializeCharts(); initializeTerminal(); } function handleApiKeyChange(e) { apiKey = e.target.value.trim(); if (apiKey) { localStorage.setItem('apiKey', apiKey); elements.loginError.classList.add('hidden'); connectWebSocket(); } } function handleLogout() { apiKey = ''; localStorage.removeItem('apiKey'); showLoginPage(); } function updatePagination() { const totalPages = Math.max(1, Math.ceil(totalResults / resultsPerPage)); elements.pagination.innerHTML = ''; const createPageButton = (page, text, disabled = false) => { const button = document.createElement('button'); button.textContent = text; button.className = `px-3 py-1 rounded ${disabled || page === currentPage ? 'bg-gray-600 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-700' }`; if (!disabled && page !== currentPage) { button.addEventListener('click', () => { currentPage = page; searchMods(); }); } elements.pagination.appendChild(button); }; if (totalResults > 0) { createPageButton(currentPage - 1, 'Previous', currentPage === 1); const maxButtons = 5; const startPage = Math.max(1, currentPage - Math.floor(maxButtons / 2)); const endPage = Math.min(totalPages, startPage + maxButtons - 1); for (let i = startPage; i <= endPage; i++) { createPageButton(i, i.toString()); } createPageButton(currentPage + 1, 'Next', currentPage === totalPages); } } function closeSearch() { elements.modSearch.value = ''; elements.modResults.innerHTML = ''; elements.pagination.innerHTML = ''; elements.closeSearchBtn.classList.add('hidden'); currentPage = 1; totalResults = 0; } async function searchMods(page = currentPage) { currentPage = page; const mod = elements.modSearch.value.trim(); if (mod) { try { const offset = (currentPage - 1) * resultsPerPage; const response = await wsRequest('/search', 'POST', { mod, offset }); if (elements.modResults) { totalResults = response.totalHits || response.results?.length || 0; elements.modResults.innerHTML = response.results?.length > 0 ? response.results.map(result => `

${result.title}

${result.description}

Downloads: ${result.downloads}

`).join('') : '

No results found.

'; updatePagination(); elements.closeSearchBtn.classList.remove('hidden'); document.querySelectorAll('.install-mod').forEach(button => { button.addEventListener('click', async () => { const modId = button.getAttribute('data-mod-id'); try { await wsRequest('/install', 'POST', { mod: modId }); const modResponse = await wsRequest('/mod-list'); updateNonDockerUI({ type: 'mod-list', data: modResponse }); } catch (error) { console.error('Install mod error:', error); showNotification('Failed to install mod', 'error'); } }); }); } } catch (error) { console.error('Search mods error:', error); showNotification('Failed to search mods', 'error'); } } else { showNotification('Please enter a search term', 'error'); } } async function sendConsoleCommand() { const command = elements.consoleInput.value.trim(); if (command) { try { const response = await wsRequest('/console', 'POST', { command }); if (elements.consoleOutput) { elements.consoleOutput.textContent += `> ${command}\n${response.message}\n`; elements.consoleInput.value = ''; } } catch (error) { if (elements.consoleOutput) { elements.consoleOutput.textContent += `> ${command}\nError: ${error.message}\n`; } console.error('Console command error:', 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'); return; } try { const requestId = crypto.randomUUID(); const notification = showNotification(`Sending message to ${player}...`); pendingRequests.set(requestId, { resolve: () => { updateNotification(notification, `Message sent to ${player}`, 'success'); elements.tellModal.classList.add('hidden'); }, reject: (error) => { updateNotification(notification, `Failed to send message: ${error.message}`, 'error'); }, notification }); 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: ${error.message}`, '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 amount are required for custom give', 'error'); return; } } else { items = loadouts[loadout] || []; } try { const notification = showNotification(`Giving items to ${player}...`); 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'); }, reject: (error) => { updateNotification(notification, `Failed to give ${item}: ${error.message}`, 'error'); }, notification }); 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: ${error.message}`, '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 response = await wsRequest('/server-properties', 'GET'); if (response.error) { showNotification(`Failed to load server.properties: ${response.error}`, 'error'); return; } if (response.content && response.content.length > 4000) { showNotification(`File too large to edit (${response.content.length} characters, max 4000)`, 'error'); return; } allProperties = parseServerProperties(response.content || ''); const displayProperties = Object.fromEntries( Object.entries(allProperties).filter(([key]) => !filteredSettings.includes(key)) ); renderPropertiesFields(displayProperties); elements.editPropertiesModal.classList.remove('hidden'); } catch (error) { console.error('Fetch server properties error:', error); showNotification(`Failed to load server.properties: ${error.message}`, 'error'); } } async function saveServerProperties() { try { 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) { showNotification(`Failed to save server.properties: ${response.error}`, 'error'); return; } elements.editPropertiesModal.classList.add('hidden'); } catch (error) { console.error('Save server properties error:', error); showNotification(`Failed to save server.properties: ${error.message}`, 'error'); } } async function updateMods() { try { const response = await wsRequest('/update-mods', 'POST'); if (response.error) { showNotification(`Failed to update mods: ${response.error}`, 'error'); elements.updateModsOutput.textContent = `Error: ${response.error}`; } else { const output = response.output || 'No output from mod update.'; elements.updateModsOutput.textContent = output; } elements.updateModsModal.classList.remove('hidden'); } catch (error) { console.error('Update mods error:', error); showNotification(`Failed to update mods: ${error.message}`, 'error'); elements.updateModsOutput.textContent = `Error: ${error.message}`; elements.updateModsModal.classList.remove('hidden'); } } async function createBackup() { try { showNotification('Your backup is being created, a download will begin once ready!', 'success'); const response = await wsRequest('/backup', 'POST'); if (response.error) { showNotification(`Failed to create backup: ${response.error}`, 'error'); return; } const downloadURL = response.downloadURL; if (downloadURL) { const link = document.createElement('a'); link.href = downloadURL; link.download = ''; // Let the browser infer the filename from the URL document.body.appendChild(link); link.click(); document.body.removeChild(link); showNotification('Backup download initiated', 'success'); } else { showNotification('Backup created but no download URL provided', 'error'); } } catch (error) { console.error('Create backup error:', error); showNotification(`Failed to create backup: ${error.message}`, '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 { await wsRequest('/my-link'); } catch (error) { console.error('Generate connection link error:', error); } }); elements.generateGeyserLinkBtn.addEventListener('click', async () => { try { await wsRequest('/my-geyser-link'); } catch (error) { console.error('Generate geyser link error:', error); } }); elements.generateSftpLinkBtn.addEventListener('click', async () => { try { await wsRequest('/my-sftp'); } catch (error) { console.error('Generate SFTP link error:', error); } }); document.getElementById('refresh').addEventListener('click', async () => { if (ws && ws.readyState === WebSocket.OPEN) { const notification = showNotification('Refreshing data...'); ws.send(JSON.stringify({ type: 'refresh' })); initializeTerminal(); setTimeout(() => updateNotification(notification, 'Data refresh requested', 'success'), 1000); } else { showNotification('WebSocket not connected', 'error'); } }); document.getElementById('startBtn').addEventListener('click', async () => { try { const notification = showNotification('Starting server...'); 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' && message.data?.status === 'running') { updateNotification(notification, 'Server started successfully', 'success'); toggleSections('running'); 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, 'Server failed to start', 'error'); } } }, 30000); } else { updateNotification(notification, 'WebSocket not connected', 'error'); } } catch (error) { console.error('Start server error:', error); showNotification(`Failed to start server: ${error.message}`, 'error'); } }); document.getElementById('stopBtn').addEventListener('click', async () => { try { await wsRequest('/stop'); } catch (error) { console.error('Stop server error:', error); } }); document.getElementById('restartBtn').addEventListener('click', async () => { try { await wsRequest('/restart'); initializeTerminal(); if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'subscribe', endpoints: ['docker-logs'] })); } } catch (error) { console.error('Restart server error:', 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.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.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(); }); if (apiKey) { connectWebSocket(); } else { showLoginPage(); } });