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