Add holesail key section along with holesail tutorial and easy share tutorial so folks can share easy on discord
This commit is contained in:
749
public/js/app.js
749
public/js/app.js
@ -48,6 +48,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
myLink: document.getElementById('myLink'),
|
||||
geyserLink: document.getElementById('geyserLink'),
|
||||
sftpLink: document.getElementById('sftpLink'),
|
||||
holesailHash: document.getElementById('holesailHash'),
|
||||
geyserHash: document.getElementById('geyserHash'),
|
||||
sftpHash: document.getElementById('sftpHash'),
|
||||
modResults: document.getElementById('modResults'),
|
||||
consoleOutput: document.getElementById('consoleOutput'),
|
||||
consoleInput: document.getElementById('consoleInput'),
|
||||
@ -177,6 +180,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
myLink: '',
|
||||
geyserLink: '',
|
||||
sftpLink: '',
|
||||
holesailHash: '',
|
||||
geyserHash: '',
|
||||
sftpHash: '',
|
||||
hasShownStartNotification: false,
|
||||
connectionStatus: '',
|
||||
geyserStatus: '',
|
||||
@ -365,7 +371,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
'my-geyser-cache',
|
||||
'my-sftp-cache',
|
||||
'backup',
|
||||
'sftp-status'
|
||||
'sftp-status',
|
||||
'holesail-hashes'
|
||||
]
|
||||
}));
|
||||
responseTimeout = setTimeout(() => {
|
||||
@ -451,6 +458,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
updateModsUI(message);
|
||||
} else if (message.type === 'backup') {
|
||||
console.log('Received backup message:', message);
|
||||
} else if (message.type === 'holesail-hashes') {
|
||||
updateHolesailHashesUI(message);
|
||||
} else {
|
||||
updateNonDockerUI(message);
|
||||
}
|
||||
@ -494,7 +503,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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 sftpBtn = elements.sftpBtn;
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
const stopBtn = elements.stopBtn;
|
||||
const restartBtn = elements.restartBtn;
|
||||
@ -629,14 +638,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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.
|
||||
// state.user = /etc/hosts assignment on the host OS.
|
||||
if (message.data?.hostname && message.data?.port && message.data?.user && message.data?.password) {
|
||||
sftpCredentials = {
|
||||
hostname: state.user,
|
||||
@ -649,7 +651,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
elements.sftpLink.textContent = sftpLinkText;
|
||||
state.sftpLink = sftpLinkText;
|
||||
}
|
||||
// Call connectSftp only if it hasn't been attempted yet
|
||||
if (!hasAttemptedSftpConnect) {
|
||||
hasAttemptedSftpConnect = true;
|
||||
connectSftp();
|
||||
@ -657,6 +658,43 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function updateHolesailHashesUI(message) {
|
||||
if (message.error) {
|
||||
showNotification(`Failed to load Holesail hashes: ${message.error}`, 'error', 'holesail-hashes-error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.data?.myHash && elements.holesailHash) {
|
||||
const hashText = message.data.myHash;
|
||||
if (state.holesailHash !== hashText) {
|
||||
elements.holesailHash.textContent = hashText;
|
||||
state.holesailHash = hashText;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.data?.geyserHash && elements.geyserHash) {
|
||||
const hashText = message.data.geyserHash;
|
||||
if (state.geyserHash !== hashText) {
|
||||
elements.geyserHash.textContent = hashText;
|
||||
state.geyserHash = hashText;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.data?.sftpHash && elements.sftpHash) {
|
||||
const hashText = message.data.sftpHash;
|
||||
if (state.sftpHash !== hashText) {
|
||||
elements.sftpHash.textContent = hashText;
|
||||
state.sftpHash = hashText;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.data?.errors?.length > 0) {
|
||||
message.data.errors.forEach(error => {
|
||||
showNotification(error, 'error', `holesail-hashes-error-${error}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateModsUI(message) {
|
||||
if (message.error) {
|
||||
elements.updateModsOutput.textContent = `Error: ${message.error}`;
|
||||
@ -1410,386 +1448,382 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
}
|
||||
|
||||
let displayProperties = {}; // Store original properties for filtering
|
||||
let displayProperties = {};
|
||||
|
||||
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;
|
||||
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, filter = '') {
|
||||
const fieldsContainer = elements.propertiesFields;
|
||||
|
||||
let searchContainer = fieldsContainer.querySelector('#searchContainer');
|
||||
if (!searchContainer) {
|
||||
searchContainer = document.createElement('div');
|
||||
searchContainer.id = 'searchContainer';
|
||||
searchContainer.className = 'mb-4';
|
||||
searchContainer.style.display = 'block';
|
||||
|
||||
const searchLabel = document.createElement('label');
|
||||
searchLabel.textContent = 'Search Properties';
|
||||
searchLabel.className = 'block text-sm font-medium mb-1 text-white';
|
||||
searchLabel.setAttribute('for', 'propertiesSearch');
|
||||
|
||||
const searchInput = document.createElement('input');
|
||||
searchInput.type = 'text';
|
||||
searchInput.id = 'propertiesSearch';
|
||||
searchInput.placeholder = 'Search properties...';
|
||||
searchInput.className = 'bg-gray-700 px-4 py-2 rounded text-white w-full';
|
||||
searchInput.setAttribute('aria-label', 'Search server properties');
|
||||
|
||||
searchContainer.appendChild(searchLabel);
|
||||
searchContainer.appendChild(searchInput);
|
||||
|
||||
const addButton = document.createElement('button');
|
||||
addButton.textContent = 'Add Property';
|
||||
addButton.type = 'button';
|
||||
addButton.className = 'mt-2 bg-blue-600 hover:bg-blue-700 text-white px-2 py-1 rounded text-xs';
|
||||
addButton.addEventListener('click', showCustomPropertyForm);
|
||||
searchContainer.appendChild(addButton);
|
||||
|
||||
fieldsContainer.appendChild(searchContainer);
|
||||
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
renderPropertiesList(displayProperties, e.target.value);
|
||||
});
|
||||
|
||||
elements.searchInput = searchInput;
|
||||
}
|
||||
});
|
||||
return properties;
|
||||
}
|
||||
|
||||
function renderPropertiesFields(properties, filter = '') {
|
||||
const fieldsContainer = elements.propertiesFields;
|
||||
let propertiesList = fieldsContainer.querySelector('#propertiesList');
|
||||
if (!propertiesList) {
|
||||
propertiesList = document.createElement('div');
|
||||
propertiesList.id = 'propertiesList';
|
||||
propertiesList.className = 'space-y-2';
|
||||
fieldsContainer.appendChild(propertiesList);
|
||||
}
|
||||
|
||||
// Create search box only if not already present
|
||||
let searchContainer = fieldsContainer.querySelector('#searchContainer');
|
||||
if (!searchContainer) {
|
||||
searchContainer = document.createElement('div');
|
||||
searchContainer.id = 'searchContainer';
|
||||
searchContainer.className = 'mb-4';
|
||||
searchContainer.style.display = 'block';
|
||||
renderPropertiesList(properties, filter);
|
||||
|
||||
const searchLabel = document.createElement('label');
|
||||
searchLabel.textContent = 'Search Properties';
|
||||
searchLabel.className = 'block text-sm font-medium mb-1 text-white';
|
||||
searchLabel.setAttribute('for', 'propertiesSearch');
|
||||
|
||||
const searchInput = document.createElement('input');
|
||||
searchInput.type = 'text';
|
||||
searchInput.id = 'propertiesSearch';
|
||||
searchInput.placeholder = 'Search properties...';
|
||||
searchInput.className = 'bg-gray-700 px-4 py-2 rounded text-white w-full';
|
||||
searchInput.setAttribute('aria-label', 'Search server properties');
|
||||
|
||||
searchContainer.appendChild(searchLabel);
|
||||
searchContainer.appendChild(searchInput);
|
||||
|
||||
// Add custom property button (mini style)
|
||||
const addButton = document.createElement('button');
|
||||
addButton.textContent = 'Add Property';
|
||||
addButton.type = 'button';
|
||||
addButton.className = 'mt-2 bg-blue-600 hover:bg-blue-700 text-white px-2 py-1 rounded text-xs';
|
||||
addButton.addEventListener('click', showCustomPropertyForm);
|
||||
searchContainer.appendChild(addButton);
|
||||
|
||||
fieldsContainer.appendChild(searchContainer);
|
||||
|
||||
// Add input event listener
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
renderPropertiesList(displayProperties, e.target.value);
|
||||
});
|
||||
|
||||
// Store reference to search input
|
||||
elements.searchInput = searchInput;
|
||||
const closeButton = elements.editPropertiesModal.querySelector('.close-button');
|
||||
if (closeButton && !closeButton.dataset.closeHandlerAdded) {
|
||||
closeButton.addEventListener('click', () => {
|
||||
elements.searchInput.value = '';
|
||||
renderPropertiesList(displayProperties, '');
|
||||
elements.editPropertiesModal.classList.add('hidden');
|
||||
hideCustomPropertyForm();
|
||||
});
|
||||
closeButton.dataset.closeHandlerAdded = 'true';
|
||||
}
|
||||
}
|
||||
|
||||
// Create or update properties list container
|
||||
let propertiesList = fieldsContainer.querySelector('#propertiesList');
|
||||
if (!propertiesList) {
|
||||
propertiesList = document.createElement('div');
|
||||
propertiesList.id = 'propertiesList';
|
||||
propertiesList.className = 'space-y-2';
|
||||
fieldsContainer.appendChild(propertiesList);
|
||||
}
|
||||
|
||||
renderPropertiesList(properties, filter);
|
||||
|
||||
// Add modal close handler
|
||||
const closeButton = elements.editPropertiesModal.querySelector('.close-button');
|
||||
if (closeButton && !closeButton.dataset.closeHandlerAdded) {
|
||||
closeButton.addEventListener('click', () => {
|
||||
elements.searchInput.value = '';
|
||||
renderPropertiesList(displayProperties, '');
|
||||
elements.editPropertiesModal.classList.add('hidden');
|
||||
hideCustomPropertyForm();
|
||||
});
|
||||
closeButton.dataset.closeHandlerAdded = 'true';
|
||||
}
|
||||
}
|
||||
|
||||
function showCustomPropertyForm() {
|
||||
let formContainer = elements.propertiesFields.querySelector('#customPropertyForm');
|
||||
if (formContainer) {
|
||||
formContainer.remove();
|
||||
}
|
||||
|
||||
formContainer = document.createElement('div');
|
||||
formContainer.id = 'customPropertyForm';
|
||||
formContainer.className = 'mb-4 p-4 bg-gray-800 rounded';
|
||||
|
||||
const form = document.createElement('form');
|
||||
form.addEventListener('submit', (e) => e.preventDefault());
|
||||
|
||||
const keyInput = document.createElement('input');
|
||||
keyInput.type = 'text';
|
||||
keyInput.placeholder = 'Property name';
|
||||
keyInput.className = 'bg-gray-700 px-4 py-2 rounded text-white w-full mb-2';
|
||||
|
||||
const valueInput = document.createElement('input');
|
||||
valueInput.type = 'text';
|
||||
valueInput.placeholder = 'Property value';
|
||||
valueInput.className = 'bg-gray-700 px-4 py-2 rounded text-white w-full mb-2';
|
||||
|
||||
const addButton = document.createElement('button');
|
||||
addButton.type = 'button';
|
||||
addButton.textContent = 'Add';
|
||||
addButton.className = 'bg-green-600 hover:bg-green-700 text-white px-2 py-1 rounded text-xs mr-2';
|
||||
addButton.addEventListener('click', () => {
|
||||
const key = keyInput.value.trim();
|
||||
const value = valueInput.value.trim();
|
||||
if (key && value) {
|
||||
displayProperties[key] = value;
|
||||
allProperties[key] = value;
|
||||
renderPropertiesList(displayProperties, elements.searchInput.value);
|
||||
function showCustomPropertyForm() {
|
||||
let formContainer = elements.propertiesFields.querySelector('#customPropertyForm');
|
||||
if (formContainer) {
|
||||
formContainer.remove();
|
||||
} else {
|
||||
showNotification('Please enter both property name and value', 'error', 'custom-property-error');
|
||||
}
|
||||
});
|
||||
|
||||
const cancelButton = document.createElement('button');
|
||||
cancelButton.type = 'button';
|
||||
cancelButton.textContent = 'Cancel';
|
||||
cancelButton.className = 'bg-gray-600 hover:bg-gray-700 text-white px-2 py-1 rounded text-xs';
|
||||
cancelButton.addEventListener('click', hideCustomPropertyForm);
|
||||
formContainer = document.createElement('div');
|
||||
formContainer.id = 'customPropertyForm';
|
||||
formContainer.className = 'mb-4 p-4 bg-gray-800 rounded';
|
||||
|
||||
form.appendChild(keyInput);
|
||||
form.appendChild(valueInput);
|
||||
form.appendChild(addButton);
|
||||
form.appendChild(cancelButton);
|
||||
formContainer.appendChild(form);
|
||||
const form = document.createElement('form');
|
||||
form.addEventListener('submit', (e) => e.preventDefault());
|
||||
|
||||
elements.propertiesFields.insertBefore(formContainer, elements.propertiesFields.querySelector('#propertiesList'));
|
||||
}
|
||||
const keyInput = document.createElement('input');
|
||||
keyInput.type = 'text';
|
||||
keyInput.placeholder = 'Property name';
|
||||
keyInput.className = 'bg-gray-700 px-4 py-2 rounded text-white w-full mb-2';
|
||||
|
||||
function hideCustomPropertyForm() {
|
||||
const formContainer = elements.propertiesFields.querySelector('#customPropertyForm');
|
||||
if (formContainer) {
|
||||
formContainer.remove();
|
||||
const valueInput = document.createElement('input');
|
||||
valueInput.type = 'text';
|
||||
valueInput.placeholder = 'Property value';
|
||||
valueInput.className = 'bg-gray-700 px-4 py-2 rounded text-white w-full mb-2';
|
||||
|
||||
const addButton = document.createElement('button');
|
||||
addButton.type = 'button';
|
||||
addButton.textContent = 'Add';
|
||||
addButton.className = 'bg-green-600 hover:bg-green-700 text-white px-2 py-1 rounded text-xs mr-2';
|
||||
addButton.addEventListener('click', () => {
|
||||
const key = keyInput.value.trim();
|
||||
const value = valueInput.value.trim();
|
||||
if (key && value) {
|
||||
displayProperties[key] = value;
|
||||
allProperties[key] = value;
|
||||
renderPropertiesList(displayProperties, elements.searchInput.value);
|
||||
formContainer.remove();
|
||||
} else {
|
||||
showNotification('Please enter both property name and value', 'error', 'custom-property-error');
|
||||
}
|
||||
});
|
||||
|
||||
const cancelButton = document.createElement('button');
|
||||
cancelButton.type = 'button';
|
||||
cancelButton.textContent = 'Cancel';
|
||||
cancelButton.className = 'bg-gray-600 hover:bg-gray-700 text-white px-2 py-1 rounded text-xs';
|
||||
cancelButton.addEventListener('click', hideCustomPropertyForm);
|
||||
|
||||
form.appendChild(keyInput);
|
||||
form.appendChild(valueInput);
|
||||
form.appendChild(addButton);
|
||||
form.appendChild(cancelButton);
|
||||
formContainer.appendChild(form);
|
||||
|
||||
elements.propertiesFields.insertBefore(formContainer, elements.propertiesFields.querySelector('#propertiesList'));
|
||||
}
|
||||
}
|
||||
|
||||
function showDeleteConfirmationModal(key) {
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'deleteConfirmationModal';
|
||||
modal.className = 'absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
|
||||
|
||||
const modalContent = document.createElement('div');
|
||||
modalContent.className = 'bg-gray-800 p-6 rounded-lg max-w-md w-full';
|
||||
|
||||
const header = document.createElement('h3');
|
||||
header.className = 'text-lg font-medium text-white mb-4';
|
||||
header.textContent = 'Confirm Deletion';
|
||||
|
||||
const message = document.createElement('p');
|
||||
message.className = 'text-white mb-6';
|
||||
message.textContent = `Are you sure you want to delete the property "${key}"? This action cannot be undone.`;
|
||||
|
||||
const buttonContainer = document.createElement('div');
|
||||
buttonContainer.className = 'flex justify-end space-x-2';
|
||||
|
||||
const cancelButton = document.createElement('button');
|
||||
cancelButton.type = 'button';
|
||||
cancelButton.className = 'bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded';
|
||||
cancelButton.textContent = 'Cancel';
|
||||
cancelButton.addEventListener('click', () => modal.remove());
|
||||
|
||||
const deleteButton = document.createElement('button');
|
||||
deleteButton.type = 'button';
|
||||
deleteButton.className = 'bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded';
|
||||
deleteButton.textContent = 'Delete';
|
||||
deleteButton.addEventListener('click', () => {
|
||||
delete displayProperties[key];
|
||||
delete allProperties[key];
|
||||
renderPropertiesList(displayProperties, elements.searchInput.value);
|
||||
modal.remove();
|
||||
showNotification(`Property "${key}" deleted`, 'success', 'delete-property-success');
|
||||
});
|
||||
|
||||
buttonContainer.appendChild(cancelButton);
|
||||
buttonContainer.appendChild(deleteButton);
|
||||
modalContent.appendChild(header);
|
||||
modalContent.appendChild(message);
|
||||
modalContent.appendChild(buttonContainer);
|
||||
modal.appendChild(modalContent);
|
||||
|
||||
elements.editPropertiesModal.appendChild(modal);
|
||||
}
|
||||
|
||||
function renderPropertiesList(properties, filter = '') {
|
||||
const propertiesList = elements.propertiesFields.querySelector('#propertiesList');
|
||||
propertiesList.innerHTML = '';
|
||||
|
||||
const filteredProperties = Object.entries(properties).filter(([key]) =>
|
||||
key.toLowerCase().includes(filter.toLowerCase())
|
||||
);
|
||||
|
||||
filteredProperties.forEach(([key, value]) => {
|
||||
if (filteredSettings.includes(key)) {
|
||||
return;
|
||||
function hideCustomPropertyForm() {
|
||||
const formContainer = elements.propertiesFields.querySelector('#customPropertyForm');
|
||||
if (formContainer) {
|
||||
formContainer.remove();
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Rendering field for ${key}: ${value}`);
|
||||
function showDeleteConfirmationModal(key) {
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'deleteConfirmationModal';
|
||||
modal.className = 'absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
|
||||
|
||||
const fieldDiv = document.createElement('div');
|
||||
fieldDiv.className = 'flex items-center space-x-2';
|
||||
fieldDiv.style.display = 'flex';
|
||||
const modalContent = document.createElement('div');
|
||||
modalContent.className = 'bg-gray-800 p-6 rounded-lg max-w-md w-full';
|
||||
|
||||
const header = document.createElement('h3');
|
||||
header.className = 'text-lg font-medium text-white mb-4';
|
||||
header.textContent = 'Confirm Deletion';
|
||||
|
||||
const message = document.createElement('p');
|
||||
message.className = 'text-white mb-6';
|
||||
message.textContent = `Are you sure you want to delete the property "${key}"? This action cannot be undone.`;
|
||||
|
||||
const buttonContainer = document.createElement('div');
|
||||
buttonContainer.className = 'flex justify-end space-x-2';
|
||||
|
||||
const cancelButton = document.createElement('button');
|
||||
cancelButton.type = 'button';
|
||||
cancelButton.className = 'bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded';
|
||||
cancelButton.textContent = 'Cancel';
|
||||
cancelButton.addEventListener('click', () => modal.remove());
|
||||
|
||||
const deleteButton = document.createElement('button');
|
||||
deleteButton.type = 'button';
|
||||
deleteButton.className = 'text-red-500 hover:text-red-700';
|
||||
deleteButton.innerHTML = '✕';
|
||||
deleteButton.title = `Delete ${key}`;
|
||||
deleteButton.addEventListener('click', () => showDeleteConfirmationModal(key));
|
||||
deleteButton.className = 'bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded';
|
||||
deleteButton.textContent = 'Delete';
|
||||
deleteButton.addEventListener('click', () => {
|
||||
delete displayProperties[key];
|
||||
delete allProperties[key];
|
||||
renderPropertiesList(displayProperties, elements.searchInput.value);
|
||||
modal.remove();
|
||||
showNotification(`Property "${key}" deleted`, 'success', 'delete-property-success');
|
||||
});
|
||||
|
||||
let inputType = 'text';
|
||||
let isBoolean = value.toLowerCase() === 'true' || value.toLowerCase() === 'false';
|
||||
if (isBoolean) {
|
||||
inputType = 'switch';
|
||||
} else if (/^\d+$/.test(value) && !isNaN(parseInt(value))) {
|
||||
inputType = 'number';
|
||||
buttonContainer.appendChild(cancelButton);
|
||||
buttonContainer.appendChild(deleteButton);
|
||||
modalContent.appendChild(header);
|
||||
modalContent.appendChild(message);
|
||||
modalContent.appendChild(buttonContainer);
|
||||
modal.appendChild(modalContent);
|
||||
|
||||
elements.editPropertiesModal.appendChild(modal);
|
||||
}
|
||||
|
||||
function renderPropertiesList(properties, filter = '') {
|
||||
const propertiesList = elements.propertiesFields.querySelector('#propertiesList');
|
||||
propertiesList.innerHTML = '';
|
||||
|
||||
const filteredProperties = Object.entries(properties).filter(([key]) =>
|
||||
key.toLowerCase().includes(filter.toLowerCase())
|
||||
);
|
||||
|
||||
filteredProperties.forEach(([key, value]) => {
|
||||
if (filteredSettings.includes(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Rendering field for ${key}: ${value}`);
|
||||
|
||||
const fieldDiv = document.createElement('div');
|
||||
fieldDiv.className = 'flex items-center space-x-2';
|
||||
fieldDiv.style.display = 'flex';
|
||||
|
||||
const deleteButton = document.createElement('button');
|
||||
deleteButton.type = 'button';
|
||||
deleteButton.className = 'text-red-500 hover:text-red-700';
|
||||
deleteButton.innerHTML = '✕';
|
||||
deleteButton.title = `Delete ${key}`;
|
||||
deleteButton.addEventListener('click', () => showDeleteConfirmationModal(key));
|
||||
|
||||
let inputType = 'text';
|
||||
let isBoolean = value.toLowerCase() === 'true' || value.toLowerCase() === 'false';
|
||||
if (isBoolean) {
|
||||
inputType = 'switch';
|
||||
} 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 text-white';
|
||||
label.setAttribute('for', `prop-${key}`);
|
||||
|
||||
if (inputType === 'switch') {
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'text';
|
||||
hiddenInput.id = `prop-${key}`;
|
||||
hiddenInput.name = key;
|
||||
hiddenInput.value = value.toLowerCase();
|
||||
hiddenInput.style.display = 'none';
|
||||
|
||||
const switchContainer = document.createElement('div');
|
||||
switchContainer.className = 'relative inline-block';
|
||||
switchContainer.setAttribute('role', 'switch');
|
||||
switchContainer.setAttribute('aria-checked', value.toLowerCase());
|
||||
switchContainer.setAttribute('tabindex', '0');
|
||||
switchContainer.dataset.name = key;
|
||||
switchContainer.style.width = '40px';
|
||||
switchContainer.style.height = '24px';
|
||||
switchContainer.style.display = 'inline-block';
|
||||
switchContainer.style.position = 'relative';
|
||||
switchContainer.style.cursor = 'pointer';
|
||||
switchContainer.style.zIndex = '10';
|
||||
|
||||
const switchTrack = document.createElement('div');
|
||||
switchTrack.className = 'block w-full h-full rounded-full';
|
||||
switchTrack.style.backgroundColor = value.toLowerCase() === 'true' ? '#10B981' : '#4B5563';
|
||||
switchTrack.style.transition = 'background-color 0.2s ease-in-out';
|
||||
|
||||
const switchHandle = document.createElement('div');
|
||||
switchHandle.className = 'absolute rounded-full bg-white';
|
||||
switchHandle.style.width = '16px';
|
||||
switchHandle.style.height = '16px';
|
||||
switchHandle.style.top = '4px';
|
||||
switchHandle.style.left = value.toLowerCase() === 'true' ? '20px' : '4px';
|
||||
switchHandle.style.transition = 'left 0.2s ease-in-out';
|
||||
switchHandle.style.position = 'absolute';
|
||||
switchHandle.style.zIndex = '11';
|
||||
|
||||
const toggleSwitch = () => {
|
||||
const currentValue = hiddenInput.value === 'true';
|
||||
const newValue = !currentValue;
|
||||
hiddenInput.value = newValue.toString();
|
||||
switchContainer.setAttribute('aria-checked', newValue.toString());
|
||||
switchTrack.style.backgroundColor = newValue ? '#10B981' : '#4B5563';
|
||||
switchHandle.style.left = newValue ? '20px' : '4px';
|
||||
};
|
||||
|
||||
switchContainer.addEventListener('click', toggleSwitch);
|
||||
switchContainer.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
toggleSwitch();
|
||||
}
|
||||
});
|
||||
|
||||
switchContainer.appendChild(switchTrack);
|
||||
switchContainer.appendChild(switchHandle);
|
||||
fieldDiv.appendChild(deleteButton);
|
||||
fieldDiv.appendChild(label);
|
||||
fieldDiv.appendChild(hiddenInput);
|
||||
fieldDiv.appendChild(switchContainer);
|
||||
} else {
|
||||
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';
|
||||
input.type = inputType;
|
||||
input.value = value;
|
||||
if (inputType === 'number') {
|
||||
input.min = '0';
|
||||
}
|
||||
fieldDiv.appendChild(deleteButton);
|
||||
fieldDiv.appendChild(label);
|
||||
fieldDiv.appendChild(input);
|
||||
}
|
||||
|
||||
propertiesList.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('Fetching 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 || '');
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.textContent = key;
|
||||
label.className = 'w-1/3 text-sm font-medium text-white';
|
||||
label.setAttribute('for', `prop-${key}`);
|
||||
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:not(#propertiesSearch):not([id="customPropertyKey"]):not([id="customPropertyValue"])');
|
||||
|
||||
if (inputType === 'switch') {
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'text';
|
||||
hiddenInput.id = `prop-${key}`;
|
||||
hiddenInput.name = key;
|
||||
hiddenInput.value = value.toLowerCase();
|
||||
hiddenInput.style.display = 'none';
|
||||
|
||||
const switchContainer = document.createElement('div');
|
||||
switchContainer.className = 'relative inline-block';
|
||||
switchContainer.setAttribute('role', 'switch');
|
||||
switchContainer.setAttribute('aria-checked', value.toLowerCase());
|
||||
switchContainer.setAttribute('tabindex', '0');
|
||||
switchContainer.dataset.name = key;
|
||||
switchContainer.style.width = '40px';
|
||||
switchContainer.style.height = '24px';
|
||||
switchContainer.style.display = 'inline-block';
|
||||
switchContainer.style.position = 'relative';
|
||||
switchContainer.style.cursor = 'pointer';
|
||||
switchContainer.style.zIndex = '10';
|
||||
|
||||
const switchTrack = document.createElement('div');
|
||||
switchTrack.className = 'block w-full h-full rounded-full';
|
||||
switchTrack.style.backgroundColor = value.toLowerCase() === 'true' ? '#10B981' : '#4B5563';
|
||||
switchTrack.style.transition = 'background-color 0.2s ease-in-out';
|
||||
|
||||
const switchHandle = document.createElement('div');
|
||||
switchHandle.className = 'absolute rounded-full bg-white';
|
||||
switchHandle.style.width = '16px';
|
||||
switchHandle.style.height = '16px';
|
||||
switchHandle.style.top = '4px';
|
||||
switchHandle.style.left = value.toLowerCase() === 'true' ? '20px' : '4px';
|
||||
switchHandle.style.transition = 'left 0.2s ease-in-out';
|
||||
switchHandle.style.position = 'absolute';
|
||||
switchHandle.style.zIndex = '11';
|
||||
|
||||
const toggleSwitch = () => {
|
||||
const currentValue = hiddenInput.value === 'true';
|
||||
const newValue = !currentValue;
|
||||
hiddenInput.value = newValue.toString();
|
||||
switchContainer.setAttribute('aria-checked', newValue.toString());
|
||||
switchTrack.style.backgroundColor = newValue ? '#10B981' : '#4B5563';
|
||||
switchHandle.style.left = newValue ? '20px' : '4px';
|
||||
};
|
||||
|
||||
switchContainer.addEventListener('click', toggleSwitch);
|
||||
switchContainer.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
toggleSwitch();
|
||||
inputs.forEach(input => {
|
||||
const key = input.name;
|
||||
let value = input.value.trim();
|
||||
if (value !== '') {
|
||||
properties[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
switchContainer.appendChild(switchTrack);
|
||||
switchContainer.appendChild(switchHandle);
|
||||
fieldDiv.appendChild(deleteButton);
|
||||
fieldDiv.appendChild(label);
|
||||
fieldDiv.appendChild(hiddenInput);
|
||||
fieldDiv.appendChild(switchContainer);
|
||||
} else {
|
||||
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';
|
||||
input.type = inputType;
|
||||
input.value = value;
|
||||
if (inputType === 'number') {
|
||||
input.min = '0';
|
||||
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;
|
||||
}
|
||||
fieldDiv.appendChild(deleteButton);
|
||||
fieldDiv.appendChild(label);
|
||||
fieldDiv.appendChild(input);
|
||||
}
|
||||
|
||||
propertiesList.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 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 || '');
|
||||
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', 'fetch-properties-error');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveServerProperties() {
|
||||
try {
|
||||
const key = `action-save-properties`;
|
||||
const properties = {};
|
||||
const inputs = elements.propertiesFields.querySelectorAll('input:not(#propertiesSearch):not([id="customPropertyKey"]):not([id="customPropertyValue"])');
|
||||
|
||||
// Collect modified properties
|
||||
inputs.forEach(input => {
|
||||
const key = input.name;
|
||||
let value = input.value.trim();
|
||||
if (value !== '') {
|
||||
properties[key] = value;
|
||||
if (elements.searchInput) {
|
||||
elements.searchInput.value = '';
|
||||
renderPropertiesList(displayProperties, '');
|
||||
}
|
||||
});
|
||||
|
||||
// Merge with allProperties to include all properties, even if not displayed
|
||||
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');
|
||||
}
|
||||
// Clear search input and reset properties list
|
||||
if (elements.searchInput) {
|
||||
elements.searchInput.value = '';
|
||||
renderPropertiesList(displayProperties, '');
|
||||
}
|
||||
elements.editPropertiesModal.classList.add('hidden');
|
||||
} 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 notification = showNotification('Updating mods...', 'loading', key);
|
||||
const response = await wsRequest('/update-mods', 'POST');
|
||||
if (response.error) {
|
||||
elements.updateModsOutput.textContent = `Error: ${response.error}`;
|
||||
@ -1836,11 +1870,10 @@ async function saveServerProperties() {
|
||||
}
|
||||
|
||||
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 (!isSftpOnline) {
|
||||
showNotification('SFTP is offline. Please try again later.', 'error', 'sftp-offline');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sftpCredentials) {
|
||||
showNotification('SFTP credentials not available.', 'error', 'sftp-credentials');
|
||||
|
Reference in New Issue
Block a user