Update notification system to remove duplicates, make notifications easier to understand.

This commit is contained in:
MCHost
2025-06-22 19:28:20 -04:00
parent 35d6acee9a
commit 9cc45ea48e

View File

@ -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', () => {
<span>${message}</span>
`;
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 = `<span>${message}</span>`;
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);
@ -428,7 +462,7 @@ document.addEventListener('DOMContentLoaded', () => {
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('') : '<p class="text-gray-400">No results found.</p>';
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,31 +1465,36 @@ 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 () => {
document.getElementById('restartBtn').addEventListener('click', async () => {
try {
const notification = showNotification('Restarting server...');
const key = `action-restart`;
const notification = showNotification('Restarting server...', 'loading', key);
await wsRequest('/restart');
initializeTerminal();
if (ws && ws.readyState === WebSocket.OPEN) {
@ -1416,11 +1503,11 @@ document.getElementById('restartBtn').addEventListener('click', async () => {
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 restarted successfully', 'success');
updateNotification(notification, 'Server restarted successfully', 'success', key);
ws.removeEventListener('message', messageHandler);
}
}
@ -1433,19 +1520,19 @@ document.getElementById('restartBtn').addEventListener('click', async () => {
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
updateNotification(notification, 'Failed to restart server. Please try again.', 'error', key);
toggleSections(state.serverStatus);
}
}
}, 60000); // Increased timeout for restart
}, 60000);
} else {
updateNotification(notification, 'WebSocket not connected', 'error');
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');
showNotification(`Failed to restart server: ${error.message}`, 'error', 'restart-error');
}
});
});
elements.updateModsBtn.addEventListener('click', updateMods);