1391 lines
50 KiB
JavaScript
1391 lines
50 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 allProperties = {};
|
|
const filteredSettings = [
|
|
'bug-report-link',
|
|
'query.port',
|
|
'rcon.password',
|
|
'region-file-compression',
|
|
'rcon.port',
|
|
'server-port',
|
|
'level-type',
|
|
'enable-rcon',
|
|
'enable-status'
|
|
];
|
|
|
|
const elements = {
|
|
loginPage: document.getElementById('loginPage'),
|
|
loginApiKey: document.getElementById('loginApiKey'),
|
|
loginBtn: document.getElementById('loginBtn'),
|
|
loginError: document.getElementById('loginError'),
|
|
authControls: document.getElementById('authControls'),
|
|
apiKeyInput: document.getElementById('apiKey'),
|
|
user: document.getElementById('user'),
|
|
serverStatus: document.getElementById('serverStatus'),
|
|
memoryPercent: document.getElementById('memoryPercent'),
|
|
cpuPercent: document.getElementById('cpuPercent'),
|
|
keyExpiry: document.getElementById('keyExpiry'),
|
|
playerList: document.getElementById('playerList'),
|
|
modList: document.getElementById('modList'),
|
|
logUrl: document.getElementById('logUrl'),
|
|
websiteUrl: document.getElementById('websiteUrl'),
|
|
mapUrl: document.getElementById('mapUrl'),
|
|
myLink: document.getElementById('myLink'),
|
|
geyserLink: document.getElementById('geyserLink'),
|
|
sftpLink: document.getElementById('sftpLink'),
|
|
modResults: document.getElementById('modResults'),
|
|
consoleOutput: document.getElementById('consoleOutput'),
|
|
consoleInput: document.getElementById('consoleInput'),
|
|
dockerLogsTerminal: document.getElementById('dockerLogsTerminal'),
|
|
mainContent: document.getElementById('mainContent'),
|
|
notificationContainer: document.getElementById('notificationContainer'),
|
|
generateMyLinkBtn: document.getElementById('generateMyLinkBtn'),
|
|
generateGeyserLinkBtn: document.getElementById('generateGeyserLinkBtn'),
|
|
generateSftpLinkBtn: document.getElementById('generateSftpLinkBtn'),
|
|
pagination: document.getElementById('pagination'),
|
|
modSearch: document.getElementById('modSearch'),
|
|
closeSearchBtn: document.getElementById('closeSearchBtn'),
|
|
tellModal: document.getElementById('tellModal'),
|
|
tellPlayerName: document.getElementById('tellPlayerName'),
|
|
tellMessage: document.getElementById('tellMessage'),
|
|
tellForm: document.getElementById('tellForm'),
|
|
giveModal: document.getElementById('giveModal'),
|
|
givePlayerName: document.getElementById('givePlayerName'),
|
|
loadoutSelect: document.getElementById('loadoutSelect'),
|
|
customGiveFields: document.getElementById('customGiveFields'),
|
|
giveForm: document.getElementById('giveForm'),
|
|
addItemBtn: document.getElementById('addItemBtn'),
|
|
itemList: document.getElementById('itemList'),
|
|
editPropertiesBtn: document.getElementById('editPropertiesBtn'),
|
|
editPropertiesModal: document.getElementById('editPropertiesModal'),
|
|
propertiesFields: document.getElementById('propertiesFields'),
|
|
editPropertiesForm: document.getElementById('editPropertiesForm'),
|
|
stopBtn: document.getElementById('stopBtn'),
|
|
restartBtn: document.getElementById('restartBtn'),
|
|
connectionStatus: document.getElementById('connectionStatus'),
|
|
geyserStatus: document.getElementById('geyserStatus'),
|
|
sftpStatus: document.getElementById('sftpStatus')
|
|
};
|
|
|
|
const loadouts = {
|
|
starter: [
|
|
{ item: 'torch', amount: 16 },
|
|
{ item: 'cooked_beef', amount: 8 },
|
|
{ item: 'stone_pickaxe', amount: 1 }
|
|
],
|
|
builder: [
|
|
{ item: 'stone', amount: 64 },
|
|
{ item: 'oak_planks', amount: 64 },
|
|
{ item: 'glass', amount: 32 },
|
|
{ item: 'ladder', amount: 16 }
|
|
],
|
|
combat: [
|
|
{ item: 'iron_sword', amount: 1 },
|
|
{ item: 'leather_chestplate', amount: 1 },
|
|
{ item: 'shield', amount: 1 },
|
|
{ item: 'arrow', amount: 32 }
|
|
],
|
|
miner: [
|
|
{ item: 'iron_pickaxe', amount: 1, enchantments: [{ name: 'efficiency', level: 2 }] },
|
|
{ item: 'torch', amount: 64 },
|
|
{ item: 'iron_shovel', amount: 1 },
|
|
{ item: 'bucket', amount: 1 }
|
|
],
|
|
adventurer: [
|
|
{ item: 'bow', amount: 1, enchantments: [{ name: 'power', level: 1 }] },
|
|
{ item: 'arrow', amount: 64 },
|
|
{ item: 'compass', amount: 1 },
|
|
{ item: 'map', amount: 1 }
|
|
],
|
|
alchemist: [
|
|
{ item: 'brewing_stand', amount: 1 },
|
|
{ item: 'potion', amount: 3, type: 'healing' },
|
|
{ item: 'potion', amount: 2, type: 'swiftness' },
|
|
{ item: 'nether_wart', amount: 16 }
|
|
],
|
|
enchanter: [
|
|
{ item: 'enchanting_table', amount: 1 },
|
|
{ item: 'book', amount: 10 },
|
|
{ item: 'lapis_lazuli', amount: 32 },
|
|
{ item: 'experience_bottle', amount: 16 }
|
|
],
|
|
farmer: [
|
|
{ item: 'wheat_seeds', amount: 32 },
|
|
{ item: 'iron_hoe', amount: 1 },
|
|
{ item: 'bone_meal', amount: 16 },
|
|
{ item: 'carrot', amount: 8 }
|
|
],
|
|
nether: [
|
|
{ item: 'potion', amount: 2, type: 'fire_resistance' },
|
|
{ item: 'obsidian', amount: 10 },
|
|
{ item: 'flint_and_steel', amount: 1 },
|
|
{ item: 'golden_apple', amount: 1 }
|
|
],
|
|
end: [
|
|
{ item: 'ender_pearl', amount: 16 },
|
|
{ item: 'blaze_rod', amount: 8 },
|
|
{ item: 'diamond_sword', amount: 1, enchantments: [{ name: 'sharpness', level: 2 }] },
|
|
{ item: 'pumpkin', amount: 1 }
|
|
]
|
|
};
|
|
|
|
let state = {
|
|
user: '',
|
|
serverStatus: '',
|
|
memoryPercent: 0,
|
|
cpuPercent: 0,
|
|
keyExpiry: '',
|
|
playerList: '',
|
|
modListHtml: '',
|
|
logUrl: '',
|
|
websiteUrl: '',
|
|
mapUrl: '',
|
|
myLink: '',
|
|
geyserLink: '',
|
|
sftpLink: '',
|
|
hasShownStartNotification: false,
|
|
connectionStatus: '',
|
|
geyserStatus: '',
|
|
sftpStatus: ''
|
|
};
|
|
|
|
function showNotification(message, type = 'loading') {
|
|
const notification = document.createElement('div');
|
|
notification.className = `notification ${type}`;
|
|
notification.innerHTML = `
|
|
${type === 'loading' ? '<div class="spinner"></div>' : ''}
|
|
<span>${message}</span>
|
|
`;
|
|
elements.notificationContainer.appendChild(notification);
|
|
if (type !== 'loading') {
|
|
setTimeout(() => {
|
|
notification.style.opacity = '0';
|
|
setTimeout(() => notification.remove(), 300);
|
|
}, 3000);
|
|
}
|
|
return notification;
|
|
}
|
|
|
|
function updateNotification(notification, message, type) {
|
|
notification.className = `notification ${type}`;
|
|
notification.innerHTML = `<span>${message}</span>`;
|
|
if (type !== 'loading') {
|
|
setTimeout(() => {
|
|
notification.style.opacity = '0';
|
|
setTimeout(() => notification.remove(), 300);
|
|
}, 3000);
|
|
}
|
|
}
|
|
|
|
function initializeCharts() {
|
|
if (memoryMeter || cpuMeter) {
|
|
return;
|
|
}
|
|
|
|
const memoryCanvas = document.getElementById('memoryMeter');
|
|
if (!memoryCanvas) {
|
|
console.error('Memory Meter canvas not found');
|
|
showNotification('Failed to initialize memory chart: Canvas not found', 'error');
|
|
return;
|
|
}
|
|
const memoryCtx = memoryCanvas.getContext('2d');
|
|
if (!memoryCtx) {
|
|
console.error('Failed to acquire 2D context for Memory Meter');
|
|
showNotification('Failed to initialize memory chart: Invalid canvas context', 'error');
|
|
return;
|
|
}
|
|
memoryMeter = new Chart(memoryCtx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
datasets: [{
|
|
data: [0, 100],
|
|
backgroundColor: ['#EF4444', '#4B5563'],
|
|
borderWidth: 0,
|
|
circumference: 180,
|
|
rotation: 270
|
|
}]
|
|
},
|
|
options: {
|
|
cutout: '80%',
|
|
responsive: false,
|
|
plugins: { title: { display: false }, tooltip: { enabled: false } }
|
|
}
|
|
});
|
|
|
|
const cpuCanvas = document.getElementById('cpuMeter');
|
|
if (!cpuCanvas) {
|
|
console.error('CPU Meter canvas not found');
|
|
showNotification('Failed to initialize CPU chart: Canvas not found', 'error');
|
|
return;
|
|
}
|
|
const cpuCtx = cpuCanvas.getContext('2d');
|
|
if (!cpuCtx) {
|
|
console.error('Failed to acquire 2D context for CPU Meter');
|
|
showNotification('Failed to initialize CPU chart: Invalid canvas context', 'error');
|
|
return;
|
|
}
|
|
cpuMeter = new Chart(cpuCtx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
datasets: [{
|
|
data: [0, 600],
|
|
backgroundColor: ['#3B82F6', '#4B5563'],
|
|
borderWidth: 0,
|
|
circumference: 180,
|
|
rotation: 270
|
|
}]
|
|
},
|
|
options: {
|
|
cutout: '80%',
|
|
responsive: false,
|
|
plugins: { title: { display: false }, tooltip: { enabled: false } }
|
|
}
|
|
});
|
|
}
|
|
|
|
function initializeTerminal() {
|
|
if (terminal) {
|
|
terminal.clear();
|
|
} else {
|
|
terminal = new Terminal({
|
|
rows: 8,
|
|
fontSize: 14,
|
|
fontFamily: 'monospace',
|
|
theme: {
|
|
background: '#1f2937',
|
|
foreground: '#ffffff',
|
|
cursor: '#ffffff'
|
|
},
|
|
scrollback: 1000,
|
|
rendererType: 'canvas'
|
|
});
|
|
fitAddon = new FitAddon.FitAddon();
|
|
terminal.loadAddon(fitAddon);
|
|
terminal.open(elements.dockerLogsTerminal);
|
|
terminal.element.style.border = 'none';
|
|
}
|
|
fitAddon.fit();
|
|
|
|
window.addEventListener('resize', () => {
|
|
if (fitAddon && terminal) {
|
|
fitAddon.fit();
|
|
}
|
|
});
|
|
}
|
|
|
|
function connectWebSocket() {
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
return;
|
|
}
|
|
|
|
ws = new WebSocket(`wss://${window.location.host}/ws?apiKey=${encodeURIComponent(apiKey)}`);
|
|
|
|
ws.onopen = () => {
|
|
showMainContent();
|
|
ws.send(JSON.stringify({
|
|
type: 'subscribe',
|
|
endpoints: ['docker', 'docker-logs', 'hello', 'time', 'list-players', 'mod-list', 'log', 'website', 'map', 'my-link-cache', 'my-geyser-cache', 'my-sftp-cache']
|
|
}));
|
|
responseTimeout = setTimeout(() => {
|
|
showNotification('No response from server. Please check connection or API key.', 'error');
|
|
handleLogout();
|
|
}, 5000);
|
|
};
|
|
|
|
ws.onmessage = (event) => {
|
|
try {
|
|
clearTimeout(responseTimeout);
|
|
const message = JSON.parse(event.data);
|
|
if (message.requestId && pendingRequests.has(message.requestId)) {
|
|
const { resolve, reject, notification } = pendingRequests.get(message.requestId);
|
|
pendingRequests.delete(message.requestId);
|
|
if (message.error) {
|
|
updateNotification(notification, `Error: ${message.error}`, 'error');
|
|
if (message.error.includes('Missing token') || message.error.includes('HTTP 403')) {
|
|
showNotification('Invalid or missing API key. Please log in again.', 'error');
|
|
elements.loginError.classList.remove('hidden');
|
|
elements.loginError.textContent = 'Invalid API key. Please try again.';
|
|
handleLogout();
|
|
}
|
|
reject(new Error(message.error));
|
|
} else {
|
|
updateNotification(notification, message.message || 'Action completed', 'success');
|
|
resolve(message.data || message);
|
|
}
|
|
} else {
|
|
updateUI(message);
|
|
}
|
|
} catch (error) {
|
|
console.error('WebSocket message parsing error:', error);
|
|
showNotification('Error processing server data', 'error');
|
|
}
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
showLoginPage();
|
|
clearTimeout(responseTimeout);
|
|
if (terminal) {
|
|
terminal.clear();
|
|
}
|
|
setTimeout(connectWebSocket, 3000);
|
|
};
|
|
|
|
ws.onerror = (error) => {
|
|
console.error('WebSocket error:', error);
|
|
showNotification('WebSocket connection error', 'error');
|
|
};
|
|
}
|
|
|
|
function wsRequest(endpoint, method = 'GET', body = null) {
|
|
return new Promise((resolve, reject) => {
|
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
showNotification('No connection to server', 'error');
|
|
reject(new Error('WebSocket not connected'));
|
|
return;
|
|
}
|
|
const requestId = crypto.randomUUID();
|
|
const notification = showNotification(`${method} ${endpoint}...`);
|
|
pendingRequests.set(requestId, { resolve, reject, notification });
|
|
ws.send(JSON.stringify({ type: 'request', requestId, endpoint, method, body }));
|
|
});
|
|
}
|
|
|
|
function updateUI(message) {
|
|
if (message.type === 'docker') {
|
|
updateDockerUI(message);
|
|
} else if (message.type === 'docker-logs') {
|
|
updateDockerLogsUI(message);
|
|
} else if (message.type === 'connection-status') {
|
|
updateConnectionStatusUI(message);
|
|
} else if (message.type === 'geyser-status') {
|
|
updateGeyserStatusUI(message);
|
|
} else if (message.type === 'sftp-status') {
|
|
updateSftpStatusUI(message);
|
|
} else {
|
|
updateNonDockerUI(message);
|
|
}
|
|
}
|
|
|
|
function updateDockerUI(message) {
|
|
if (message.error) {
|
|
if (elements.serverStatus) elements.serverStatus.textContent = 'Not Running';
|
|
toggleSections('error');
|
|
return;
|
|
}
|
|
|
|
const memoryPercent = parseFloat(message.data?.memory?.percent) || 0;
|
|
if (state.memoryPercent !== memoryPercent && elements.memoryPercent && memoryMeter) {
|
|
memoryMeter.data.datasets[0].data = [memoryPercent, 100 - memoryPercent];
|
|
memoryMeter.update();
|
|
elements.memoryPercent.textContent = `${memoryPercent.toFixed(1)}%`;
|
|
state.memoryPercent = memoryPercent;
|
|
}
|
|
|
|
const cpuPercent = parseFloat(message.data?.cpu) || 0;
|
|
if (state.cpuPercent !== cpuPercent && elements.cpuPercent && cpuMeter) {
|
|
const scaledCpuPercent = Math.min((cpuPercent / 600) * 100, 100);
|
|
cpuMeter.data.datasets[0].data = [scaledCpuPercent, 100 - scaledCpuPercent];
|
|
cpuMeter.update();
|
|
elements.cpuPercent.textContent = `${cpuPercent.toFixed(1)}%`;
|
|
state.cpuPercent = cpuPercent;
|
|
}
|
|
|
|
const status = message.data?.status || 'Unknown';
|
|
if (state.serverStatus !== status && elements.serverStatus) {
|
|
elements.serverStatus.textContent = status;
|
|
state.serverStatus = status;
|
|
toggleSections(status);
|
|
}
|
|
}
|
|
|
|
function toggleSections(status) {
|
|
const sections = document.querySelectorAll('.bg-gray-800.p-6.rounded-lg.shadow-lg.mb-6');
|
|
const serverStatusSection = document.querySelector('.bg-gray-800.p-6.rounded-lg.shadow-lg.mb-6[data-section="server-status"]');
|
|
const editPropertiesBtn = elements.editPropertiesBtn;
|
|
const startBtn = document.getElementById('startBtn');
|
|
const stopBtn = elements.stopBtn;
|
|
const restartBtn = elements.restartBtn;
|
|
|
|
if (startBtn) {
|
|
if (status.toLowerCase() === 'running') {
|
|
startBtn.disabled = true;
|
|
startBtn.classList.add('disabled-btn');
|
|
} else {
|
|
startBtn.disabled = false;
|
|
startBtn.classList.remove('disabled-btn');
|
|
}
|
|
}
|
|
|
|
if (status.toLowerCase() !== 'running') {
|
|
sections.forEach(section => {
|
|
if (section !== serverStatusSection) {
|
|
section.classList.add('hidden');
|
|
}
|
|
});
|
|
if (editPropertiesBtn) {
|
|
editPropertiesBtn.classList.add('hidden');
|
|
}
|
|
if (stopBtn) {
|
|
stopBtn.disabled = true;
|
|
stopBtn.classList.add('disabled-btn');
|
|
}
|
|
if (restartBtn) {
|
|
restartBtn.disabled = true;
|
|
restartBtn.classList.add('disabled-btn');
|
|
}
|
|
if (!state.hasShownStartNotification) {
|
|
showNotification('Server is not running. Please click the "Start" button to enable all features.', 'error');
|
|
state.hasShownStartNotification = true;
|
|
}
|
|
} else {
|
|
sections.forEach(section => {
|
|
section.classList.remove('hidden');
|
|
});
|
|
if (editPropertiesBtn) {
|
|
editPropertiesBtn.classList.remove('hidden');
|
|
}
|
|
if (stopBtn) {
|
|
stopBtn.disabled = false;
|
|
stopBtn.classList.remove('disabled-btn');
|
|
}
|
|
if (restartBtn) {
|
|
restartBtn.disabled = false;
|
|
restartBtn.classList.remove('disabled-btn');
|
|
}
|
|
state.hasShownStartNotification = false;
|
|
}
|
|
}
|
|
|
|
function updateDockerLogsUI(message) {
|
|
if (message.error) {
|
|
return;
|
|
}
|
|
if (message.data?.log && terminal) {
|
|
const logLines = message.data.log.split('\n');
|
|
const cleanedLog = logLines
|
|
.map(line =>
|
|
line
|
|
.replace(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z\s*/, '')
|
|
.replace(/\u001b\[32m0\|MineCraft Server\s*\|\s*\u001b\[39m/, '')
|
|
)
|
|
.join('\n');
|
|
terminal.write(cleanedLog);
|
|
const lines = terminal.buffer.active.length;
|
|
if (lines > 8) {
|
|
terminal.scrollToLine(lines - 8);
|
|
}
|
|
if (fitAddon) {
|
|
fitAddon.fit();
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateConnectionStatusUI(message) {
|
|
if (message.data?.isOnline !== undefined && elements.connectionStatus) {
|
|
const statusIcon = message.data.isOnline ? '✅' : '❌';
|
|
if (state.connectionStatus !== statusIcon) {
|
|
elements.connectionStatus.textContent = statusIcon;
|
|
state.connectionStatus = statusIcon;
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateGeyserStatusUI(message) {
|
|
if (message.data?.isOnline !== undefined && elements.geyserStatus) {
|
|
const statusIcon = message.data.isOnline ? '✅' : '❌';
|
|
if (state.geyserStatus !== statusIcon) {
|
|
elements.geyserStatus.textContent = statusIcon;
|
|
state.geyserStatus = statusIcon;
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateSftpStatusUI(message) {
|
|
if (message.data?.isOnline !== undefined && elements.sftpStatus) {
|
|
const statusIcon = message.data.isOnline ? '✅' : '❌';
|
|
if (state.sftpStatus !== statusIcon) {
|
|
elements.sftpStatus.textContent = statusIcon;
|
|
state.sftpStatus = statusIcon;
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateNonDockerUI(message) {
|
|
if (message.error) {
|
|
if (message.error.includes('Missing token') || message.error.includes('HTTP 403')) {
|
|
showNotification('Invalid or missing API key. Please log in again.', 'error');
|
|
elements.loginError.classList.remove('hidden');
|
|
elements.loginError.textContent = 'Invalid API key. Please try again.';
|
|
handleLogout();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (message.type === 'hello' && message.data?.message) {
|
|
const user = message.data.message.split(', ')[1]?.replace('!', '').trim() || 'Unknown';
|
|
if (state.user !== user && elements.user) {
|
|
elements.user.textContent = user;
|
|
state.user = user;
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify({ type: 'updateUser', user }));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (message.type === 'time' && message.data?.keyexpireString) {
|
|
if (state.keyExpiry !== message.data.keyexpireString && elements.keyExpiry) {
|
|
const expiryDate = new Date(message.data.keyexpireString);
|
|
const formattedDate = expiryDate.toLocaleString('en-US', {
|
|
month: 'long',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
hour12: true,
|
|
timeZoneName: 'short'
|
|
});
|
|
elements.keyExpiry.textContent = formattedDate;
|
|
state.keyExpiry = message.data.keyexpireString;
|
|
}
|
|
}
|
|
|
|
if (message.type === 'list-players' && message.data?.players) {
|
|
const players = message.data.players || [];
|
|
const playerListHtml = players.length > 0
|
|
? players.map(player => `
|
|
<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="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('.kick-player').forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
const player = button.getAttribute('data-player').trim();
|
|
if (!player) {
|
|
showNotification('Invalid player name', 'error');
|
|
return;
|
|
}
|
|
try {
|
|
const requestId = crypto.randomUUID();
|
|
const notification = showNotification(`Kicking ${player}...`);
|
|
pendingRequests.set(requestId, {
|
|
resolve: () => {
|
|
updateNotification(notification, `${player} kicked`, 'success');
|
|
wsRequest('/list-players').then(response => {
|
|
updateNonDockerUI({ type: 'list-players', data: response });
|
|
});
|
|
},
|
|
reject: (error) => {
|
|
updateNotification(notification, `Failed to kick ${player}: ${error.message}`, 'error');
|
|
},
|
|
notification
|
|
});
|
|
ws.send(JSON.stringify({ type: 'kick-player', requestId, player }));
|
|
} catch (error) {
|
|
console.error(`Kick player ${player} error:`, error);
|
|
showNotification(`Failed to kick ${player}: ${error.message}`, 'error');
|
|
}
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.ban-player').forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
const player = button.getAttribute('data-player').trim();
|
|
if (!player) {
|
|
showNotification('Invalid player name', 'error');
|
|
return;
|
|
}
|
|
try {
|
|
const requestId = crypto.randomUUID();
|
|
const notification = showNotification(`Banning ${player}...`);
|
|
pendingRequests.set(requestId, {
|
|
resolve: () => {
|
|
updateNotification(notification, `${player} banned`, 'success');
|
|
wsRequest('/list-players').then(response => {
|
|
updateNonDockerUI({ type: 'list-players', data: response });
|
|
});
|
|
},
|
|
reject: (error) => {
|
|
updateNotification(notification, `Failed to ban ${player}: ${error.message}`, 'error');
|
|
},
|
|
notification
|
|
});
|
|
ws.send(JSON.stringify({ type: 'ban-player', requestId, player }));
|
|
} catch (error) {
|
|
console.error(`Ban player ${player} error:`, error);
|
|
showNotification(`Failed to ban ${player}: ${error.message}`, 'error');
|
|
}
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.op-player').forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
const player = button.getAttribute('data-player').trim();
|
|
if (!player) {
|
|
showNotification('Invalid player name', 'error');
|
|
return;
|
|
}
|
|
try {
|
|
const requestId = crypto.randomUUID();
|
|
const notification = showNotification(`Opping ${player}...`);
|
|
pendingRequests.set(requestId, {
|
|
resolve: () => {
|
|
updateNotification(notification, `${player} opped`, 'success');
|
|
wsRequest('/list-players').then(response => {
|
|
updateNonDockerUI({ type: 'list-players', data: response });
|
|
});
|
|
},
|
|
reject: (error) => {
|
|
updateNotification(notification, `Failed to op ${player}: ${error.message}`, 'error');
|
|
},
|
|
notification
|
|
});
|
|
ws.send(JSON.stringify({ type: 'op-player', requestId, player }));
|
|
} catch (error) {
|
|
console.error(`Op player ${player} error:`, error);
|
|
showNotification(`Failed to op ${player}: ${error.message}`, 'error');
|
|
}
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.deop-player').forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
const player = button.getAttribute('data-player').trim();
|
|
if (!player) {
|
|
showNotification('Invalid player name', 'error');
|
|
return;
|
|
}
|
|
try {
|
|
const requestId = crypto.randomUUID();
|
|
const notification = showNotification(`Deopping ${player}...`);
|
|
pendingRequests.set(requestId, {
|
|
resolve: () => {
|
|
updateNotification(notification, `${player} deopped`, 'success');
|
|
wsRequest('/list-players').then(response => {
|
|
updateNonDockerUI({ type: 'list-players', data: response });
|
|
});
|
|
},
|
|
reject: (error) => {
|
|
updateNotification(notification, `Failed to deop ${player}: ${error.message}`, 'error');
|
|
},
|
|
notification
|
|
});
|
|
ws.send(JSON.stringify({ type: 'deop-player', requestId, player }));
|
|
} catch (error) {
|
|
console.error(`Deop player ${player} error:`, error);
|
|
showNotification(`Failed to deop ${player}: ${error.message}`, 'error');
|
|
}
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.tell-player').forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
const player = button.getAttribute('data-player').trim();
|
|
if (!player) {
|
|
showNotification('Invalid player name', 'error');
|
|
return;
|
|
}
|
|
elements.tellPlayerName.textContent = player;
|
|
elements.tellMessage.value = '';
|
|
elements.tellModal.classList.remove('hidden');
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.give-player').forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
const player = button.getAttribute('data-player').trim();
|
|
if (!player) {
|
|
showNotification('Invalid player name', 'error');
|
|
return;
|
|
}
|
|
elements.givePlayerName.textContent = player;
|
|
elements.loadoutSelect.value = 'custom';
|
|
elements.customGiveFields.classList.remove('hidden');
|
|
resetItemFields();
|
|
elements.giveModal.classList.remove('hidden');
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
if (message.type === 'mod-list' && message.data?.mods) {
|
|
const modListHtml = message.data.mods.map(mod => `
|
|
<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('');
|
|
if (state.modListHtml !== modListHtml && elements.modList) {
|
|
elements.modList.innerHTML = modListHtml;
|
|
state.modListHtml = modListHtml;
|
|
document.querySelectorAll('.uninstall-mod').forEach(button => {
|
|
button.addEventListener('click', async () => {
|
|
const modId = button.getAttribute('data-mod-id');
|
|
try {
|
|
await wsRequest('/uninstall', 'POST', { mod: modId });
|
|
const response = await wsRequest('/mod-list');
|
|
updateNonDockerUI({ type: 'mod-list', data: response });
|
|
} catch (error) {
|
|
console.error('Uninstall mod error:', error);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
if (message.type === 'log' && message.data?.message) {
|
|
if (state.logUrl !== message.data.message && elements.logUrl) {
|
|
elements.logUrl.href = message.data.message;
|
|
elements.logUrl.textContent = message.data.message;
|
|
state.logUrl = message.data.message;
|
|
}
|
|
}
|
|
|
|
if (message.type === 'website' && message.data?.message) {
|
|
if (state.websiteUrl !== message.data.message && elements.websiteUrl) {
|
|
elements.websiteUrl.href = message.data.message;
|
|
elements.websiteUrl.textContent = message.data.message;
|
|
state.websiteUrl = message.data.message;
|
|
}
|
|
}
|
|
|
|
if (message.type === 'map' && message.data?.message) {
|
|
if (state.mapUrl !== message.data.message && elements.mapUrl) {
|
|
elements.mapUrl.href = message.data.message;
|
|
elements.mapUrl.textContent = message.data.message;
|
|
state.mapUrl = message.data.message;
|
|
}
|
|
}
|
|
|
|
if (message.type === 'my-link-cache' && message.data?.hostname && message.data?.port) {
|
|
const myLinkText = `${message.data.hostname}:${message.data.port}`;
|
|
if (state.myLink !== myLinkText && elements.myLink) {
|
|
elements.myLink.textContent = myLinkText;
|
|
state.myLink = myLinkText;
|
|
}
|
|
}
|
|
|
|
if (message.type === 'my-geyser-cache' && message.data?.hostname && message.data?.port) {
|
|
const geyserLinkText = `${message.data.hostname}:${message.data.port}`;
|
|
if (state.geyserLink !== geyserLinkText && elements.geyserLink) {
|
|
elements.geyserLink.textContent = geyserLinkText;
|
|
state.geyserLink = geyserLinkText;
|
|
}
|
|
}
|
|
|
|
if (message.type === 'my-sftp-cache' && message.data?.hostname && message.data?.port && message.data?.user && message.data?.password) {
|
|
const sftpLinkText = `${message.data.hostname}:${message.data.port} (Auth: User: ${message.data.user} | Pass: ${message.data.password})`;
|
|
if (state.sftpLink !== sftpLinkText && elements.sftpLink) {
|
|
elements.sftpLink.textContent = sftpLinkText;
|
|
state.sftpLink = sftpLinkText;
|
|
}
|
|
}
|
|
|
|
if (message.type === 'my-link' && message.data?.hostname && message.data?.port) {
|
|
const myLinkText = `${message.data.hostname}:${message.data.port}`;
|
|
if (state.myLink !== myLinkText && elements.myLink) {
|
|
elements.myLink.textContent = myLinkText;
|
|
state.myLink = myLinkText;
|
|
}
|
|
}
|
|
|
|
if (message.type === 'my-geyser-link' && message.data?.hostname && message.data?.port) {
|
|
const geyserLinkText = `${message.data.hostname}:${message.data.port}`;
|
|
if (state.geyserLink !== geyserLinkText && elements.geyserLink) {
|
|
elements.geyserLink.textContent = geyserLinkText;
|
|
state.geyserLink = geyserLinkText;
|
|
}
|
|
}
|
|
|
|
if (message.type === 'my-sftp' && message.data?.hostname && message.data?.port && message.data?.user) {
|
|
const sftpLinkText = `${message.data.hostname}:${message.data.port} (User: ${message.data.user})`;
|
|
if (state.sftpLink !== sftpLinkText && elements.sftpLink) {
|
|
elements.sftpLink.textContent = sftpLinkText;
|
|
state.sftpLink = sftpLinkText;
|
|
}
|
|
}
|
|
}
|
|
|
|
function showLoginPage() {
|
|
if (!elements.loginPage || !elements.mainContent) {
|
|
console.error('Required elements not found:', {
|
|
loginPage: elements.loginPage,
|
|
mainContent: elements.mainContent
|
|
});
|
|
showNotification('Error: Page elements not found. Please refresh the page.', 'error');
|
|
return;
|
|
}
|
|
elements.loginPage.classList.remove('hidden');
|
|
elements.mainContent.classList.add('hidden');
|
|
elements.authControls.innerHTML = '<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('Error: Page elements not found. Please refresh the 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 closeSearch() {
|
|
elements.modSearch.value = '';
|
|
elements.modResults.innerHTML = '';
|
|
elements.pagination.innerHTML = '';
|
|
elements.closeSearchBtn.classList.add('hidden');
|
|
currentPage = 1;
|
|
totalResults = 0;
|
|
}
|
|
|
|
async function searchMods(page = currentPage) {
|
|
currentPage = page;
|
|
const mod = elements.modSearch.value.trim();
|
|
if (mod) {
|
|
try {
|
|
const offset = (currentPage - 1) * resultsPerPage;
|
|
const response = await wsRequest('/search', 'POST', { mod, offset });
|
|
if (elements.modResults) {
|
|
totalResults = response.totalHits || response.results?.length || 0;
|
|
elements.modResults.innerHTML = response.results?.length > 0 ? response.results.map(result => `
|
|
<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');
|
|
document.querySelectorAll('.install-mod').forEach(button => {
|
|
button.addEventListener('click', async () => {
|
|
const modId = button.getAttribute('data-mod-id');
|
|
try {
|
|
await wsRequest('/install', 'POST', { mod: modId });
|
|
const modResponse = await wsRequest('/mod-list');
|
|
updateNonDockerUI({ type: 'mod-list', data: modResponse });
|
|
} catch (error) {
|
|
console.error('Install mod error:', error);
|
|
showNotification('Failed to install mod', 'error');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Search mods error:', error);
|
|
showNotification('Failed to search mods', 'error');
|
|
}
|
|
} else {
|
|
showNotification('Please enter a search term', 'error');
|
|
}
|
|
}
|
|
|
|
async function sendConsoleCommand() {
|
|
const command = elements.consoleInput.value.trim();
|
|
if (command) {
|
|
try {
|
|
const response = await wsRequest('/console', 'POST', { command });
|
|
if (elements.consoleOutput) {
|
|
elements.consoleOutput.textContent += `> ${command}\n${response.message}\n`;
|
|
elements.consoleInput.value = '';
|
|
}
|
|
} catch (error) {
|
|
if (elements.consoleOutput) {
|
|
elements.consoleOutput.textContent += `> ${command}\nError: ${error.message}\n`;
|
|
}
|
|
console.error('Console command error:', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function sendTellMessage() {
|
|
const player = elements.tellPlayerName.textContent.trim();
|
|
const message = elements.tellMessage.value.trim();
|
|
if (!player || !message) {
|
|
showNotification('Player name and message are required', 'error');
|
|
return;
|
|
}
|
|
try {
|
|
const requestId = crypto.randomUUID();
|
|
const notification = showNotification(`Sending message to ${player}...`);
|
|
pendingRequests.set(requestId, {
|
|
resolve: () => {
|
|
updateNotification(notification, `Message sent to ${player}`, 'success');
|
|
elements.tellModal.classList.add('hidden');
|
|
},
|
|
reject: (error) => {
|
|
updateNotification(notification, `Failed to send message: ${error.message}`, 'error');
|
|
},
|
|
notification
|
|
});
|
|
ws.send(JSON.stringify({ type: 'tell-player', requestId, player, message }));
|
|
} catch (error) {
|
|
console.error(`Send message to ${player} error:`, error);
|
|
showNotification(`Failed to send message: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
function addItemField() {
|
|
const itemList = elements.itemList;
|
|
const itemEntry = document.createElement('div');
|
|
itemEntry.className = 'item-entry flex space-x-2 items-center';
|
|
itemEntry.innerHTML = `
|
|
<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 amount are required for custom give', 'error');
|
|
return;
|
|
}
|
|
} else {
|
|
items = loadouts[loadout] || [];
|
|
}
|
|
|
|
try {
|
|
const notification = showNotification(`Giving items to ${player}...`);
|
|
for (const itemData of items) {
|
|
const { item, amount, enchantments, type } = itemData;
|
|
const requestId = crypto.randomUUID();
|
|
pendingRequests.set(requestId, {
|
|
resolve: () => {
|
|
updateNotification(notification, `Gave ${amount} ${item}${type ? ` (${type})` : ''} to ${player}`, 'success');
|
|
},
|
|
reject: (error) => {
|
|
updateNotification(notification, `Failed to give ${item}: ${error.message}`, 'error');
|
|
},
|
|
notification
|
|
});
|
|
ws.send(JSON.stringify({
|
|
type: 'give-player',
|
|
requestId,
|
|
player,
|
|
item,
|
|
amount,
|
|
enchantments: enchantments || undefined,
|
|
potionType: type || undefined
|
|
}));
|
|
}
|
|
elements.giveModal.classList.add('hidden');
|
|
} catch (error) {
|
|
console.error(`Give items to ${player} error:`, error);
|
|
showNotification(`Failed to give items: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
function parseServerProperties(content) {
|
|
const properties = {};
|
|
const lines = content.split('\n');
|
|
lines.forEach(line => {
|
|
if (line.trim() && !line.trim().startsWith('#')) {
|
|
const [key, value] = line.split('=', 2).map(part => part.trim());
|
|
if (key && value !== undefined) {
|
|
properties[key] = value;
|
|
}
|
|
}
|
|
});
|
|
return properties;
|
|
}
|
|
|
|
function renderPropertiesFields(properties) {
|
|
const fieldsContainer = elements.propertiesFields;
|
|
fieldsContainer.innerHTML = '';
|
|
|
|
Object.entries(properties).forEach(([key, value]) => {
|
|
if (filteredSettings.includes(key)) {
|
|
return;
|
|
}
|
|
|
|
const fieldDiv = document.createElement('div');
|
|
fieldDiv.className = 'flex items-center space-x-2';
|
|
|
|
let inputType = 'text';
|
|
let isBoolean = value.toLowerCase() === 'true' || value.toLowerCase() === 'false';
|
|
if (isBoolean) {
|
|
inputType = 'checkbox';
|
|
} else if (/^\d+$/.test(value) && !isNaN(parseInt(value))) {
|
|
inputType = 'number';
|
|
}
|
|
|
|
const label = document.createElement('label');
|
|
label.textContent = key;
|
|
label.className = 'w-1/3 text-sm font-medium';
|
|
label.setAttribute('for', `prop-${key}`);
|
|
|
|
const input = document.createElement('input');
|
|
input.id = `prop-${key}`;
|
|
input.name = key;
|
|
input.className = 'bg-gray-700 px-4 py-2 rounded text-white w-2/3';
|
|
|
|
if (inputType === 'checkbox') {
|
|
input.type = 'checkbox';
|
|
input.checked = value.toLowerCase() === 'true';
|
|
} else {
|
|
input.type = inputType;
|
|
input.value = value;
|
|
if (inputType === 'number') {
|
|
input.min = '0';
|
|
}
|
|
}
|
|
|
|
fieldDiv.appendChild(label);
|
|
fieldDiv.appendChild(input);
|
|
fieldsContainer.appendChild(fieldDiv);
|
|
});
|
|
}
|
|
|
|
function propertiesToString(properties) {
|
|
let header = `#Minecraft server properties\n#${new Date().toUTCString()}\n`;
|
|
return header + Object.entries(properties)
|
|
.map(([key, value]) => `${key}=${value}`)
|
|
.join('\n');
|
|
}
|
|
|
|
async function fetchServerProperties() {
|
|
try {
|
|
const response = await wsRequest('/server-properties', 'GET');
|
|
if (response.error) {
|
|
showNotification(`Failed to load server.properties: ${response.error}`, 'error');
|
|
return;
|
|
}
|
|
if (response.content && response.content.length > 4000) {
|
|
showNotification(`File too large to edit (${response.content.length} characters, max 4000)`, 'error');
|
|
return;
|
|
}
|
|
allProperties = parseServerProperties(response.content || '');
|
|
const displayProperties = Object.fromEntries(
|
|
Object.entries(allProperties).filter(([key]) => !filteredSettings.includes(key))
|
|
);
|
|
renderPropertiesFields(displayProperties);
|
|
elements.editPropertiesModal.classList.remove('hidden');
|
|
} catch (error) {
|
|
console.error('Fetch server properties error:', error);
|
|
showNotification(`Failed to load server.properties: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
async function saveServerProperties() {
|
|
try {
|
|
const properties = {};
|
|
const inputs = elements.propertiesFields.querySelectorAll('input');
|
|
inputs.forEach(input => {
|
|
const key = input.name;
|
|
let value = input.type === 'checkbox' ? input.checked.toString() : input.value.trim();
|
|
if (value !== '') {
|
|
properties[key] = value;
|
|
}
|
|
});
|
|
|
|
const fullProperties = { ...allProperties, ...properties };
|
|
const content = propertiesToString(fullProperties);
|
|
const response = await wsRequest('/server-properties', 'POST', { content });
|
|
if (response.error) {
|
|
updateNotification(notification, `Failed to save server.properties: ${response.error}`, 'error');
|
|
return;
|
|
}
|
|
elements.editPropertiesModal.classList.add('hidden');
|
|
} catch (error) {
|
|
console.error('Save server properties error:', error);
|
|
showNotification(`Failed to save server.properties: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
elements.loginBtn.addEventListener('click', () => {
|
|
apiKey = elements.loginApiKey.value.trim();
|
|
if (apiKey) {
|
|
localStorage.setItem('apiKey', apiKey);
|
|
elements.loginError.classList.add('hidden');
|
|
connectWebSocket();
|
|
} else {
|
|
elements.loginError.classList.remove('hidden');
|
|
elements.loginError.textContent = 'Please enter an API key.';
|
|
}
|
|
});
|
|
|
|
elements.apiKeyInput.addEventListener('change', handleApiKeyChange);
|
|
|
|
elements.generateMyLinkBtn.addEventListener('click', async () => {
|
|
try {
|
|
await wsRequest('/my-link');
|
|
} catch (error) {
|
|
console.error('Generate connection link error:', error);
|
|
}
|
|
});
|
|
|
|
elements.generateGeyserLinkBtn.addEventListener('click', async () => {
|
|
try {
|
|
await wsRequest('/my-geyser-link');
|
|
} catch (error) {
|
|
console.error('Generate geyser link error:', error);
|
|
}
|
|
});
|
|
|
|
elements.generateSftpLinkBtn.addEventListener('click', async () => {
|
|
try {
|
|
await wsRequest('/my-sftp');
|
|
} catch (error) {
|
|
console.error('Generate SFTP link error:', error);
|
|
}
|
|
});
|
|
|
|
document.getElementById('refresh').addEventListener('click', async () => {
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
const notification = showNotification('Refreshing data...');
|
|
ws.send(JSON.stringify({ type: 'refresh' }));
|
|
initializeTerminal();
|
|
setTimeout(() => updateNotification(notification, 'Data refresh requested', 'success'), 1000);
|
|
} else {
|
|
showNotification('WebSocket not connected', 'error');
|
|
}
|
|
});
|
|
|
|
document.getElementById('startBtn').addEventListener('click', async () => {
|
|
try {
|
|
const notification = showNotification('Starting server...');
|
|
await wsRequest('/start');
|
|
initializeTerminal();
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify({ type: 'subscribe', endpoints: ['docker', 'docker-logs'] }));
|
|
// Set up a one-time message listener for the docker status
|
|
const messageHandler = (event) => {
|
|
try {
|
|
const message = JSON.parse(event.data);
|
|
if (message.type === 'docker' && message.data?.status === 'running') {
|
|
updateNotification(notification, 'Server started successfully', 'success');
|
|
toggleSections('running');
|
|
ws.removeEventListener('message', messageHandler); // Remove listener after success
|
|
}
|
|
} catch (error) {
|
|
console.error('Error parsing WebSocket message:', error);
|
|
}
|
|
};
|
|
ws.addEventListener('message', messageHandler);
|
|
// Timeout to handle case where running status isn't received
|
|
setTimeout(() => {
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.removeEventListener('message', messageHandler);
|
|
if (state.serverStatus !== 'running') {
|
|
updateNotification(notification, 'Server failed to start', 'error');
|
|
}
|
|
}
|
|
}, 30000); // 30 seconds timeout
|
|
} else {
|
|
updateNotification(notification, 'WebSocket not connected', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Start server error:', error);
|
|
showNotification(`Failed to start server: ${error.message}`, 'error');
|
|
}
|
|
});
|
|
|
|
document.getElementById('stopBtn').addEventListener('click', async () => {
|
|
try {
|
|
await wsRequest('/stop');
|
|
} catch (error) {
|
|
console.error('Stop server error:', error);
|
|
}
|
|
});
|
|
|
|
document.getElementById('restartBtn').addEventListener('click', async () => {
|
|
try {
|
|
await wsRequest('/restart');
|
|
initializeTerminal();
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify({ type: 'subscribe', endpoints: ['docker-logs'] }));
|
|
}
|
|
} catch (error) {
|
|
console.error('Restart server error:', error);
|
|
}
|
|
});
|
|
|
|
document.getElementById('searchBtn').addEventListener('click', () => {
|
|
searchMods(1);
|
|
});
|
|
|
|
document.getElementById('sendConsole').addEventListener('click', sendConsoleCommand);
|
|
|
|
elements.closeSearchBtn.addEventListener('click', closeSearch);
|
|
|
|
elements.tellModal.querySelector('.modal-close').addEventListener('click', () => {
|
|
elements.tellModal.classList.add('hidden');
|
|
});
|
|
|
|
elements.giveModal.querySelector('.modal-close').addEventListener('click', () => {
|
|
elements.giveModal.classList.add('hidden');
|
|
});
|
|
|
|
elements.loadoutSelect.addEventListener('change', () => {
|
|
const isCustom = elements.loadoutSelect.value === 'custom';
|
|
elements.customGiveFields.classList.toggle('hidden', !isCustom);
|
|
if (isCustom) {
|
|
resetItemFields();
|
|
}
|
|
});
|
|
|
|
elements.tellMessage.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
sendTellMessage();
|
|
}
|
|
});
|
|
|
|
elements.itemList.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && (e.target.classList.contains('item-name') || e.target.classList.contains('item-amount'))) {
|
|
e.preventDefault();
|
|
sendGiveCommand();
|
|
}
|
|
});
|
|
|
|
elements.tellForm.addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
sendTellMessage();
|
|
});
|
|
|
|
elements.giveForm.addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
sendGiveCommand();
|
|
});
|
|
|
|
elements.addItemBtn.addEventListener('click', addItemField);
|
|
|
|
elements.editPropertiesBtn.addEventListener('click', fetchServerProperties);
|
|
|
|
elements.editPropertiesModal.querySelector('.modal-close').addEventListener('click', () => {
|
|
elements.editPropertiesModal.classList.add('hidden');
|
|
});
|
|
|
|
elements.editPropertiesForm.addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
saveServerProperties();
|
|
});
|
|
|
|
if (apiKey) {
|
|
connectWebSocket();
|
|
} else {
|
|
showLoginPage();
|
|
}
|
|
}); |