diff --git a/public/app.js b/public/app.js index 6a87fc6..cdf83d6 100644 --- a/public/app.js +++ b/public/app.js @@ -161,10 +161,19 @@ document.addEventListener('DOMContentLoaded', () => { hasShownStartNotification: false, connectionStatus: '', geyserStatus: '', - sftpStatus: '' + sftpStatus: '', + activeNotifications: new Map() // Track active notifications to prevent duplicates }; - function showNotification(message, type = 'loading') { + function showNotification(message, type = 'loading', key = null) { + // If a notification for this key exists, remove it + 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 = ` @@ -172,24 +181,48 @@ document.addEventListener('DOMContentLoaded', () => { ${message} `; elements.notificationContainer.appendChild(notification); + if (type !== 'loading') { setTimeout(() => { notification.style.opacity = '0'; - setTimeout(() => notification.remove(), 300); + 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) { + function updateNotification(notification, message, type, key = null) { + // Ensure only one notification per key + 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(), 300); + setTimeout(() => { + notification.remove(); + if (key) state.activeNotifications.delete(key); + }, 300); }, 3000); } + + if (key) { + state.activeNotifications.set(key, { notification, message, type }); + } } function initializeCharts() { @@ -200,13 +233,13 @@ document.addEventListener('DOMContentLoaded', () => { const memoryCanvas = document.getElementById('memoryMeter'); if (!memoryCanvas) { console.error('Memory Meter canvas not found'); - showNotification('Failed to initialize memory chart: Canvas not found', 'error'); + 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('Failed to initialize memory chart: Invalid canvas context', 'error'); + showNotification('Unable to display memory usage chart: Invalid canvas context', 'error'); return; } memoryMeter = new Chart(memoryCtx, { @@ -230,13 +263,13 @@ document.addEventListener('DOMContentLoaded', () => { const cpuCanvas = document.getElementById('cpuMeter'); if (!cpuCanvas) { console.error('CPU Meter canvas not found'); - showNotification('Failed to initialize CPU chart: Canvas not found', 'error'); + 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('Failed to initialize CPU chart: Invalid canvas context', 'error'); + showNotification('Unable to display CPU usage chart: Invalid canvas context', 'error'); return; } cpuMeter = new Chart(cpuCtx, { @@ -316,7 +349,7 @@ document.addEventListener('DOMContentLoaded', () => { ] })); responseTimeout = setTimeout(() => { - showNotification('No response from server. Please check connection or API key.', 'error'); + showNotification('No response from server. Please check your connection or API key.', 'error', 'ws-timeout'); handleLogout(); }, 5000); }; @@ -326,19 +359,19 @@ document.addEventListener('DOMContentLoaded', () => { clearTimeout(responseTimeout); const message = JSON.parse(event.data); if (message.requestId && pendingRequests.has(message.requestId)) { - const { resolve, reject, notification } = pendingRequests.get(message.requestId); + const { resolve, reject, notification, key } = pendingRequests.get(message.requestId); pendingRequests.delete(message.requestId); if (message.error) { - updateNotification(notification, `Error: ${message.error}`, 'error'); + updateNotification(notification, `Error: ${message.error}`, 'error', key); if (message.error.includes('Missing token') || message.error.includes('HTTP 403')) { - showNotification('Invalid or missing API key. Please log in again.', 'error'); + 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', 'success'); + updateNotification(notification, message.message || 'Action completed successfully', 'success', key); resolve(message.data || message); } } else { @@ -346,7 +379,7 @@ document.addEventListener('DOMContentLoaded', () => { } } catch (error) { console.error('WebSocket message parsing error:', error); - showNotification('Error processing server data', 'error'); + showNotification('Error processing server data. Please try again.', 'error', 'ws-parse-error'); } }; @@ -361,20 +394,22 @@ document.addEventListener('DOMContentLoaded', () => { ws.onerror = (error) => { console.error('WebSocket error:', error); - showNotification('WebSocket connection 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('No connection to server', 'error'); + showNotification('Not connected to server. Please log in.', 'error', 'ws-disconnected'); reject(new Error('WebSocket not connected')); return; } const requestId = crypto.randomUUID(); - const notification = showNotification(`${method} ${endpoint}...`); - pendingRequests.set(requestId, { resolve, reject, notification }); + const action = endpoint.replace('/', '').replace('-', ' '); + const key = `action-${action}`; // Unique key per action type + const notification = showNotification(`Processing ${action}...`, 'loading', key); + pendingRequests.set(requestId, { resolve, reject, notification, key }); ws.send(JSON.stringify({ type: 'request', requestId, endpoint, method, body })); }); } @@ -393,7 +428,6 @@ document.addEventListener('DOMContentLoaded', () => { } 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); @@ -406,7 +440,7 @@ document.addEventListener('DOMContentLoaded', () => { 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]; @@ -414,7 +448,7 @@ document.addEventListener('DOMContentLoaded', () => { 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); @@ -423,12 +457,12 @@ document.addEventListener('DOMContentLoaded', () => { 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); // Always call toggleSections + toggleSections(status); } } @@ -476,7 +510,7 @@ document.addEventListener('DOMContentLoaded', () => { restartBtn.classList.add('disabled-btn'); } if (!state.hasShownStartNotification) { - showNotification('Server is not running. Please click the "Start" button to enable all features.', 'error'); + showNotification('Server is stopped. Click "Start" to enable all features.', 'error', 'server-stopped'); state.hasShownStartNotification = true; } } else { @@ -570,7 +604,7 @@ document.addEventListener('DOMContentLoaded', () => { 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'); + 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(); @@ -630,28 +664,30 @@ document.addEventListener('DOMContentLoaded', () => { button.addEventListener('click', () => { const player = button.getAttribute('data-player').trim(); if (!player) { - showNotification('Invalid player name', 'error'); + showNotification('Invalid player name.', 'error'); return; } try { const requestId = crypto.randomUUID(); - const notification = showNotification(`Kicking ${player}...`); + const key = `action-kick-player-${player}`; + const notification = showNotification(`Kicking player ${player}...`, 'loading', key); pendingRequests.set(requestId, { resolve: () => { - updateNotification(notification, `${player} kicked`, 'success'); + updateNotification(notification, `Player ${player} kicked successfully`, 'success', key); wsRequest('/list-players').then(response => { updateNonDockerUI({ type: 'list-players', data: response }); }); }, reject: (error) => { - updateNotification(notification, `Failed to kick ${player}: ${error.message}`, 'error'); + updateNotification(notification, `Failed to kick player ${player}: ${error.message}`, 'error', key); }, - notification + notification, + key }); 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'); + showNotification(`Failed to kick player ${player}: ${error.message}`, 'error'); } }); }); @@ -660,28 +696,30 @@ document.addEventListener('DOMContentLoaded', () => { button.addEventListener('click', () => { const player = button.getAttribute('data-player').trim(); if (!player) { - showNotification('Invalid player name', 'error'); + showNotification('Invalid player name.', 'error'); return; } try { const requestId = crypto.randomUUID(); - const notification = showNotification(`Banning ${player}...`); + const key = `action-ban-player-${player}`; + const notification = showNotification(`Banning player ${player}...`, 'loading', key); pendingRequests.set(requestId, { resolve: () => { - updateNotification(notification, `${player} banned`, 'success'); + updateNotification(notification, `Player ${player} banned successfully`, 'success', key); wsRequest('/list-players').then(response => { updateNonDockerUI({ type: 'list-players', data: response }); }); }, reject: (error) => { - updateNotification(notification, `Failed to ban ${player}: ${error.message}`, 'error'); + updateNotification(notification, `Failed to ban player ${player}: ${error.message}`, 'error', key); }, - notification + notification, + key }); 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'); + showNotification(`Failed to ban player ${player}: ${error.message}`, 'error'); } }); }); @@ -690,28 +728,30 @@ document.addEventListener('DOMContentLoaded', () => { button.addEventListener('click', () => { const player = button.getAttribute('data-player').trim(); if (!player) { - showNotification('Invalid player name', 'error'); + showNotification('Invalid player name.', 'error'); return; } try { const requestId = crypto.randomUUID(); - const notification = showNotification(`Opping ${player}...`); + const key = `action-op-player-${player}`; + const notification = showNotification(`Granting operator status to ${player}...`, 'loading', key); pendingRequests.set(requestId, { resolve: () => { - updateNotification(notification, `${player} opped`, 'success'); + updateNotification(notification, `Operator status granted to ${player}`, 'success', key); wsRequest('/list-players').then(response => { updateNonDockerUI({ type: 'list-players', data: response }); }); }, reject: (error) => { - updateNotification(notification, `Failed to op ${player}: ${error.message}`, 'error'); + updateNotification(notification, `Failed to grant operator status to ${player}: ${error.message}`, 'error', key); }, - notification + notification, + key }); 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'); + showNotification(`Failed to grant operator status to ${player}: ${error.message}`, 'error'); } }); }); @@ -720,28 +760,30 @@ document.addEventListener('DOMContentLoaded', () => { button.addEventListener('click', () => { const player = button.getAttribute('data-player').trim(); if (!player) { - showNotification('Invalid player name', 'error'); + showNotification('Invalid player name.', 'error'); return; } try { const requestId = crypto.randomUUID(); - const notification = showNotification(`Deopping ${player}...`); + const key = `action-deop-player-${player}`; + const notification = showNotification(`Removing operator status from ${player}...`, 'loading', key); pendingRequests.set(requestId, { resolve: () => { - updateNotification(notification, `${player} deopped`, 'success'); + updateNotification(notification, `Operator status removed from ${player}`, 'success', key); wsRequest('/list-players').then(response => { updateNonDockerUI({ type: 'list-players', data: response }); }); }, reject: (error) => { - updateNotification(notification, `Failed to deop ${player}: ${error.message}`, 'error'); + updateNotification(notification, `Failed to remove operator status from ${player}: ${error.message}`, 'error', key); }, - notification + notification, + key }); 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'); + showNotification(`Failed to remove operator status from ${player}: ${error.message}`, 'error'); } }); }); @@ -750,7 +792,7 @@ document.addEventListener('DOMContentLoaded', () => { button.addEventListener('click', () => { const player = button.getAttribute('data-player').trim(); if (!player) { - showNotification('Invalid player name', 'error'); + showNotification('Invalid player name.', 'error'); return; } elements.tellPlayerName.textContent = player; @@ -763,7 +805,7 @@ document.addEventListener('DOMContentLoaded', () => { button.addEventListener('click', () => { const player = button.getAttribute('data-player').trim(); if (!player) { - showNotification('Invalid player name', 'error'); + showNotification('Invalid player name.', 'error'); return; } elements.givePlayerName.textContent = player; @@ -790,12 +832,15 @@ document.addEventListener('DOMContentLoaded', () => { 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); } }); }); @@ -881,7 +926,7 @@ document.addEventListener('DOMContentLoaded', () => { loginPage: elements.loginPage, mainContent: elements.mainContent }); - showNotification('Error: Page elements not found. Please refresh the page.', 'error'); + showNotification('Page error: Essential elements missing. Please refresh the page.', 'error', 'page-error'); return; } elements.loginPage.classList.remove('hidden'); @@ -912,7 +957,7 @@ document.addEventListener('DOMContentLoaded', () => { loginPage: elements.loginPage, mainContent: elements.mainContent }); - showNotification('Error: Page elements not found. Please refresh the page.', 'error'); + showNotification('Page error: Essential elements missing. Please refresh the page.', 'error', 'page-error'); return; } elements.loginPage.classList.add('hidden'); @@ -990,6 +1035,8 @@ document.addEventListener('DOMContentLoaded', () => { if (mod) { try { const offset = (currentPage - 1) * resultsPerPage; + const key = `action-search-mods-${mod}`; + const notification = showNotification(`Searching for mods matching "${mod}"...`, 'loading', key); const response = await wsRequest('/search', 'POST', { mod, offset }); if (elements.modResults) { totalResults = response.totalHits || response.results?.length || 0; @@ -1003,26 +1050,29 @@ document.addEventListener('DOMContentLoaded', () => { `).join('') : '

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', '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'); + showNotification(`Failed to search mods: ${error.message}`, 'error', key); } } else { - showNotification('Please enter a search term', 'error'); + showNotification('Please enter a mod name to search.', 'error', 'search-mods-error'); } } @@ -1030,16 +1080,20 @@ document.addEventListener('DOMContentLoaded', () => { 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'); } } } @@ -1048,26 +1102,28 @@ document.addEventListener('DOMContentLoaded', () => { const player = elements.tellPlayerName.textContent.trim(); const message = elements.tellMessage.value.trim(); if (!player || !message) { - showNotification('Player name and message are required', 'error'); + showNotification('Player name and message are required.', 'error', 'tell-error'); return; } try { const requestId = crypto.randomUUID(); - const notification = showNotification(`Sending message to ${player}...`); + 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}`, 'success'); + updateNotification(notification, `Message sent to ${player} successfully`, 'success', key); elements.tellModal.classList.add('hidden'); }, reject: (error) => { - updateNotification(notification, `Failed to send message: ${error.message}`, 'error'); + updateNotification(notification, `Failed to send message to ${player}: ${error.message}`, 'error', key); }, - notification + 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: ${error.message}`, 'error'); + showNotification(`Failed to send message to ${player}: ${error.message}`, 'error', 'tell-error'); } } @@ -1106,7 +1162,7 @@ document.addEventListener('DOMContentLoaded', () => { }).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'); + showNotification('At least one valid item and quantity are required.', 'error', 'give-error'); return; } } else { @@ -1114,18 +1170,20 @@ document.addEventListener('DOMContentLoaded', () => { } try { - const notification = showNotification(`Giving items to ${player}...`); + 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'); + updateNotification(notification, `Gave ${amount} ${item}${type ? ` (${type})` : ''} to ${player}`, 'success', key); }, reject: (error) => { - updateNotification(notification, `Failed to give ${item}: ${error.message}`, 'error'); + updateNotification(notification, `Failed to give ${item} to ${player}: ${error.message}`, 'error', key); }, - notification + notification, + key }); ws.send(JSON.stringify({ type: 'give-player', @@ -1140,7 +1198,7 @@ document.addEventListener('DOMContentLoaded', () => { elements.giveModal.classList.add('hidden'); } catch (error) { console.error(`Give items to ${player} error:`, error); - showNotification(`Failed to give items: ${error.message}`, 'error'); + showNotification(`Failed to give items to ${player}: ${error.message}`, 'error', 'give-error'); } } @@ -1214,13 +1272,15 @@ document.addEventListener('DOMContentLoaded', () => { 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) { - showNotification(`Failed to load server.properties: ${response.error}`, 'error'); + updateNotification(notification, `Failed to load server properties: ${response.error}`, 'error', key); return; } if (response.content && response.content.length > 4000) { - showNotification(`File too large to edit (${response.content.length} characters, max 4000)`, 'error'); + updateNotification(notification, `Server properties file is too large to edit (${response.content.length} characters, max 4000)`, 'error', key); return; } allProperties = parseServerProperties(response.content || ''); @@ -1229,14 +1289,17 @@ document.addEventListener('DOMContentLoaded', () => { ); 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'); + 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 => { @@ -1251,30 +1314,34 @@ document.addEventListener('DOMContentLoaded', () => { const content = propertiesToString(fullProperties); const response = await wsRequest('/server-properties', 'POST', { content }); if (response.error) { - showNotification(`Failed to save server.properties: ${response.error}`, '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'); + showNotification(`Failed to save server properties: ${error.message}`, 'error', 'save-properties-error'); } } async function updateMods() { try { + const key = `action-update-mods`; + const notification = showNotification('Updating mods...', 'loading', key); const response = await wsRequest('/update-mods', 'POST'); if (response.error) { - showNotification(`Failed to update mods: ${response.error}`, 'error'); + updateNotification(notification, `Failed to update mods: ${response.error}`, 'error', key); elements.updateModsOutput.textContent = `Error: ${response.error}`; } else { const output = response.output || 'No output from mod update.'; elements.updateModsOutput.textContent = output; + updateNotification(notification, 'Mods updated successfully', 'success', key); } elements.updateModsModal.classList.remove('hidden'); } catch (error) { console.error('Update mods error:', error); - showNotification(`Failed to update mods: ${error.message}`, 'error'); + showNotification(`Failed to update mods: ${error.message}`, 'error', 'update-mods-error'); elements.updateModsOutput.textContent = `Error: ${error.message}`; elements.updateModsModal.classList.remove('hidden'); } @@ -1282,27 +1349,28 @@ document.addEventListener('DOMContentLoaded', () => { async function createBackup() { try { - showNotification('Your backup is being created, a download will begin once ready!', 'success'); + 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) { - showNotification(`Failed to create backup: ${response.error}`, '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 = ''; // Let the browser infer the filename from the URL + link.download = ''; document.body.appendChild(link); link.click(); document.body.removeChild(link); - showNotification('Backup download initiated', 'success'); + updateNotification(notification, 'Backup created and download started', 'success', key); } else { - showNotification('Backup created but no download URL provided', 'error'); + 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'); + showNotification(`Failed to create backup: ${error.message}`, 'error', 'backup-error'); } } @@ -1322,42 +1390,56 @@ document.addEventListener('DOMContentLoaded', () => { elements.generateMyLinkBtn.addEventListener('click', async () => { try { + const key = `action-my-link`; + const notification = showNotification('Generating connection link...', 'loading', key); await wsRequest('/my-link'); + updateNotification(notification, '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`; + const notification = showNotification('Generating Geyser link...', 'loading', key); await wsRequest('/my-geyser-link'); + updateNotification(notification, '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`; + const notification = showNotification('Generating SFTP link...', 'loading', key); await wsRequest('/my-sftp'); + updateNotification(notification, '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'); } }); document.getElementById('refresh').addEventListener('click', async () => { if (ws && ws.readyState === WebSocket.OPEN) { - const notification = showNotification('Refreshing data...'); + const key = `action-refresh`; + const notification = showNotification('Refreshing server data...', 'loading', key); ws.send(JSON.stringify({ type: 'refresh' })); initializeTerminal(); - setTimeout(() => updateNotification(notification, 'Data refresh requested', 'success'), 1000); + setTimeout(() => updateNotification(notification, 'Server data refreshed successfully', 'success', key), 1000); } else { - showNotification('WebSocket not connected', 'error'); + showNotification('Not connected to server. Please log in.', 'error', 'ws-disconnected'); } }); document.getElementById('startBtn').addEventListener('click', async () => { try { - const notification = showNotification('Starting server...'); + const key = `action-start`; + const notification = showNotification('Starting server...', 'loading', key); await wsRequest('/start'); initializeTerminal(); if (ws && ws.readyState === WebSocket.OPEN) { @@ -1366,11 +1448,11 @@ document.addEventListener('DOMContentLoaded', () => { try { const message = JSON.parse(event.data); if (message.type === 'docker') { - state.serverStatus = message.data?.status || 'Unknown'; // Force update state - elements.serverStatus.textContent = state.serverStatus; // Update UI - toggleSections(state.serverStatus); // Update section visibility + 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'); + updateNotification(notification, 'Server started successfully', 'success', key); ws.removeEventListener('message', messageHandler); } } @@ -1383,69 +1465,74 @@ document.addEventListener('DOMContentLoaded', () => { if (ws && ws.readyState === WebSocket.OPEN) { ws.removeEventListener('message', messageHandler); if (state.serverStatus !== 'running') { - updateNotification(notification, 'Server failed to start', 'error'); - toggleSections(state.serverStatus); // Ensure UI reflects failure + updateNotification(notification, 'Failed to start server. Please try again.', 'error', key); + toggleSections(state.serverStatus); } } }, 30000); } else { - updateNotification(notification, 'WebSocket not connected', 'error'); + 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'); + 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 notification = showNotification('Restarting server...'); - 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'; // Force update state - elements.serverStatus.textContent = state.serverStatus; // Update UI - toggleSections(state.serverStatus); // Update section visibility - if (message.data?.status === 'running') { - updateNotification(notification, 'Server restarted successfully', 'success'); - ws.removeEventListener('message', messageHandler); + 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); } } - } 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 restart', 'error'); - toggleSections(state.serverStatus); // Ensure UI reflects failure - } - } - }, 60000); // Increased timeout for restart - } else { - updateNotification(notification, 'WebSocket not connected', 'error'); + }, 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'); } - } catch (error) { - console.error('Restart server error:', error); - showNotification(`Failed to restart server: ${error.message}`, 'error'); - } -}); + }); elements.updateModsBtn.addEventListener('click', updateMods);