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'), stopBtn: document.getElementById('stopBtn'), restartBtn: document.getElementById('restartBtn'), connectionStatus: document.getElementById('connectionStatus'), geyserStatus: document.getElementById('geyserStatus'), sftpStatus: document.getElementById('sftpStatus') }; 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) { //console.log('Charts already initialized, skipping'); 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], // Initialize with max 600 for 6 cores 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) { //console.log('WebSocket already connected, skipping'); return; } ws = new WebSocket(`wss://${window.location.host}/ws?apiKey=${encodeURIComponent(apiKey)}`); ws.onopen = () => { //console.log('WebSocket connected'); 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'] })); 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); //console.log('WebSocket message received:', message); 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 = () => { //console.log('WebSocket disconnected'); 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 { updateNonDockerUI(message); } } function updateDockerUI(message) { //console.log('Updating Docker UI:', 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) { //console.log(`Updating memory meter: ${memoryPercent}%`); 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) { //console.log(`Updating CPU meter: ${cpuPercent}%`); // Scale CPU percent to 0-100 for the chart (600% max becomes 100% on chart) const scaledCpuPercent = Math.min((cpuPercent / 600) * 100, 100); cpuMeter.data.datasets[0].data = [scaledCpuPercent, 100 - scaledCpuPercent]; cpuMeter.update(); // Display actual CPU percent (up to 600%) elements.cpuPercent.textContent = `${cpuPercent.toFixed(1)}%`; state.cpuPercent = cpuPercent; } const status = message.data?.status || 'Unknown'; if (state.serverStatus !== status && elements.serverStatus) { //console.log(`Updating status: ${status}`); 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 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 (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 (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) { //console.log('Updating Docker Logs UI:', 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) { //console.log('Updating Connection Status UI:', 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) { //console.log('Updating Geyser Status UI:', 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) { //console.log('Updating SFTP Status UI:', 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 updateNonDockerUI(message) { //console.log('Updating Non-Docker UI:', 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) { //console.log(`Updating user: ${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) { //console.log(`Updating key expiry: ${message.data.keyexpireString}`); 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 => `${mod.name} (${mod.version})
ID: ${mod.id}
${result.title}
${result.description}
Downloads: ${result.downloads}
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) { updateNotification(notification, `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'); } } 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 { await wsRequest('/start'); initializeTerminal(); if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'subscribe', endpoints: ['docker-logs'] })); //console.log('Re-subscribed to docker-logs after starting server'); } else { console.warn('WebSocket not connected, cannot subscribe to docker-logs'); } } 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); } }); 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(); } });