Files
LuHost/views/config/index.ejs
Nathan Schneider 3aed09b60f Initial commit: LuHost - Luanti Server Management Web Interface
A modern web interface for Luanti (Minetest) server management with ContentDB integration.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-23 17:32:37 -06:00

687 lines
20 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<%
const body = `
<div class="page-header">
<h2>⚙️ Server Configuration</h2>
<p>Configure your Luanti server's global settings</p>
<div class="config-help">
<small class="text-muted">
💡 These are global server settings. For world-specific settings, visit
<a href="/worlds">🌍 World Management</a>
</small>
</div>
</div>
<div class="row">
<div class="col-md-9">
<div class="card">
<div class="card-header">
<div class="config-header-flex">
<h3>⚙️ Server Configuration</h3>
<div class="config-status" id="configStatus">
<span class="status-indicator saved">✅ Saved</span>
</div>
</div>
<p class="section-description">Configure all server settings below</p>
</div>
<div class="card-body">
<form id="configForm">
<div id="configSections">
<!-- All configuration sections will be loaded here -->
<div class="loading">Loading configuration...</div>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card">
<div class="card-header">
<h3>💾 Actions</h3>
</div>
<div class="card-body">
<div class="config-actions">
<button class="btn btn-success btn-sm btn-block" id="saveBtn">
💾 Save Changes
</button>
<button class="btn btn-outline-secondary btn-sm btn-block" id="reloadBtn">
🔄 Reload
</button>
<button class="btn btn-outline-warning btn-sm btn-block" id="resetBtn">
↩️ Reset All
</button>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3>📝 Configuration File</h3>
</div>
<div class="card-body">
<p class="text-muted small">
Location: <code id="configPath">~/.minetest/minetest.conf</code>
</p>
<button class="btn btn-outline-info btn-sm btn-block" id="downloadBtn">
📄 Download Config
</button>
</div>
</div>
<div class="card">
<div class="card-header">
<h3> Status</h3>
</div>
<div class="card-body">
<div class="status-info">
<div class="info-item">
<strong>Data Directory:</strong><br>
<small id="currentDataDir">Loading...</small>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal temporarily removed for debugging -->
<style>
.config-help {
margin-top: 0.5rem;
padding: 0.75rem;
background: var(--bg-accent);
border-radius: var(--border-radius);
border-left: 4px solid var(--primary-color);
}
.config-header-flex {
display: flex;
justify-content: space-between;
align-items: center;
}
.config-divider {
margin: 1rem 0;
}
/* Navigation styles removed - now showing all sections */
.config-actions {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--border-color);
}
.section-description {
margin: 0.5rem 0 0 0;
color: var(--text-muted);
font-size: 0.9rem;
}
.config-status {
font-size: 0.9rem;
}
.status-indicator {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-weight: bold;
}
.status-indicator.saved {
color: var(--success-color);
}
.status-indicator.modified {
background: var(--warning-color);
color: white;
}
.status-indicator.error {
background: var(--danger-color);
color: white;
}
.config-section-header {
margin-top: 2rem;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--border-color);
}
.config-section-header:first-child {
margin-top: 0;
}
.config-section-header h3 {
margin: 0;
color: var(--text-primary);
font-size: 1.25rem;
}
.config-section-header p {
margin: 0.25rem 0 0 0;
color: var(--text-muted);
font-size: 0.9rem;
}
.config-section-content {
margin-bottom: 2rem;
}
.setting-group {
margin-bottom: 1.5rem;
padding: 1rem;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
background: var(--bg-secondary);
}
.setting-label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
color: var(--text-primary);
}
.setting-description {
font-size: 0.875rem;
color: var(--text-muted);
margin-bottom: 0.75rem;
line-height: 1.4;
}
.setting-input {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
background: var(--bg-primary);
color: var(--text-primary);
}
.setting-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(var(--primary-rgb), 0.2);
}
.setting-input.number {
max-width: 200px;
}
.setting-input.boolean {
width: auto;
}
.setting-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 0.5rem;
font-size: 0.75rem;
color: var(--text-muted);
}
.setting-default {
font-style: italic;
}
.setting-validation {
color: var(--danger-color);
font-size: 0.875rem;
margin-top: 0.25rem;
}
.loading {
text-align: center;
padding: 2rem;
color: var(--text-muted);
}
/* Modal styles temporarily removed for debugging */
.status-info .info-item {
margin-bottom: 1rem;
padding: 0.5rem;
background: var(--bg-accent);
border-radius: var(--border-radius);
}
.status-info .info-item:last-child {
margin-bottom: 0;
}
@media (max-width: 768px) {
.config-nav {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
}
.nav-btn {
text-align: center;
font-size: 0.8rem;
padding: 0.5rem;
}
/* Modal media query removed */
}
</style>
<script>
// Configuration page JavaScript - properly outside template literal
let configData = {};
let originalConfig = {};
let configSections = {};
let unsavedChanges = false;
console.log('Configuration page JavaScript is loading...');
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM Content Loaded - Config Page');
loadConfiguration();
// Add event listeners for action buttons
const saveBtn = document.getElementById('saveBtn');
if (saveBtn) {
saveBtn.addEventListener('click', saveConfiguration);
}
const reloadBtn = document.getElementById('reloadBtn');
if (reloadBtn) {
reloadBtn.addEventListener('click', reloadConfiguration);
}
const resetBtn = document.getElementById('resetBtn');
if (resetBtn) {
resetBtn.addEventListener('click', resetSection);
}
const downloadBtn = document.getElementById('downloadBtn');
if (downloadBtn) {
downloadBtn.addEventListener('click', downloadConfig);
}
// Warn about unsaved changes
window.addEventListener('beforeunload', function(e) {
if (unsavedChanges) {
e.preventDefault();
e.returnValue = '';
}
});
});
async function loadConfiguration() {
try {
console.log('loadConfiguration function started...');
console.log('About to fetch /api/config...');
const response = await fetch('/api/config');
console.log('Fetch response:', response);
if (!response.ok) {
console.error('Response not ok:', response.status, response.statusText);
throw new Error('HTTP ' + response.status + ': ' + response.statusText);
}
console.log('About to parse JSON...');
const data = await response.json();
console.log('Configuration data received:', data);
console.log('Data keys:', Object.keys(data));
configData = data.current;
originalConfig = { ...data.current };
configSections = data.sections;
console.log('About to render config sections...');
renderConfigSections();
console.log('Config sections rendered, updating status...');
updateStatus('saved');
// Update status info
document.getElementById('currentDataDir').textContent = data.current.data_directory || 'Not configured';
document.getElementById('configPath').textContent = (data.current.data_directory || '~/.minetest') + '/minetest.conf';
} catch (error) {
console.error('Failed to load configuration:', error);
console.error('Error type:', typeof error);
console.error('Error message:', error.message);
console.error('Error stack:', error.stack);
// Show a basic fallback form
const sectionsElement = document.getElementById('configSections');
console.log('Setting error content to element:', sectionsElement);
if (sectionsElement) {
sectionsElement.innerHTML =
'<div class="alert alert-warning">' +
'<strong>⚠️ Configuration Loading Failed</strong><br>' +
error.message + '<br>' +
'Showing basic form instead.' +
'</div>';
} else {
console.error('configSections element not found!');
}
}
}
function renderConfigSections() {
const container = document.getElementById('configSections');
container.innerHTML = '';
// Icons for each section
const sectionIcons = {
'System': '🏗️',
'Server': '🖥️',
'World': '🌍',
'Performance': '⚡',
'Security': '🔒',
'Network': '🌐',
'Advanced': '🔧'
};
for (const [sectionName, section] of Object.entries(configSections)) {
// Create section header
const sectionHeaderDiv = document.createElement('div');
sectionHeaderDiv.className = 'config-section-header';
sectionHeaderDiv.innerHTML =
'<h3>' + (sectionIcons[sectionName] || '⚙️') + ' ' + sectionName + ' Configuration</h3>' +
'<p>' + (section.description || (sectionName + ' configuration settings')) + '</p>';
container.appendChild(sectionHeaderDiv);
// Create section content
const sectionDiv = document.createElement('div');
sectionDiv.className = 'config-section-content';
if (section.note) {
const noteDiv = document.createElement('div');
noteDiv.className = 'alert alert-info';
noteDiv.innerHTML = '💡 ' + section.note;
sectionDiv.appendChild(noteDiv);
}
for (const [settingKey, setting] of Object.entries(section.settings)) {
const settingDiv = document.createElement('div');
settingDiv.className = 'setting-group';
const currentValue = configData[settingKey] !== undefined ?
configData[settingKey] : setting.default;
settingDiv.innerHTML =
'<label class="setting-label" for="' + settingKey + '">' +
settingKey +
'</label>' +
'<div class="setting-description">' +
setting.description +
'</div>' +
renderSettingInput(settingKey, setting, currentValue) +
'<div class="setting-meta">' +
'<span class="setting-default">Default: ' + setting.default + '</span>' +
'<span class="setting-type">' + setting.type + '</span>' +
'</div>' +
'<div class="setting-validation" id="validation-' + settingKey + '"></div>';
sectionDiv.appendChild(settingDiv);
}
container.appendChild(sectionDiv);
}
// Add event listeners for changes
container.addEventListener('input', handleSettingChange);
container.addEventListener('change', handleSettingChange);
}
function renderSettingInput(key, setting, value) {
switch (setting.type) {
case 'boolean':
return '<input type="checkbox" ' +
'id="' + key + '" ' +
'name="' + key + '" ' +
'class="setting-input boolean" ' +
(value ? 'checked' : '') + '>';
case 'number':
const step = setting.step || (setting.min !== undefined && setting.min < 1 ? '0.01' : '1');
return '<input type="number" ' +
'id="' + key + '" ' +
'name="' + key + '" ' +
'class="setting-input number" ' +
'value="' + value + '"' +
'step="' + step + '"' +
(setting.min !== undefined ? 'min="' + setting.min + '"' : '') +
(setting.max !== undefined ? 'max="' + setting.max + '"' : '') + '>';
case 'text':
return '<textarea id="' + key + '" ' +
'name="' + key + '" ' +
'class="setting-input" ' +
'rows="3">' + (value || '') + '</textarea>';
default: // string
return '<input type="text" ' +
'id="' + key + '" ' +
'name="' + key + '" ' +
'class="setting-input" ' +
'value="' + (value || '') + '">';
}
}
function handleSettingChange(event) {
const input = event.target;
const key = input.name;
if (!key) return;
let value;
if (input.type === 'checkbox') {
value = input.checked;
} else if (input.type === 'number') {
value = parseFloat(input.value) || 0;
} else {
value = input.value;
}
configData[key] = value;
// Validate setting
validateSetting(key, value);
// Mark as modified
if (JSON.stringify(configData) !== JSON.stringify(originalConfig)) {
updateStatus('modified');
unsavedChanges = true;
} else {
updateStatus('saved');
unsavedChanges = false;
}
}
function validateSetting(key, value) {
const validationElement = document.getElementById('validation-' + key);
if (!validationElement) return;
// Find setting definition
let setting = null;
for (const section of Object.values(configSections)) {
if (section.settings && section.settings[key]) {
setting = section.settings[key];
break;
}
}
if (!setting) return;
// Validate based on type
let isValid = true;
let errorMessage = '';
if (setting.type === 'number') {
const num = Number(value);
if (isNaN(num)) {
isValid = false;
errorMessage = 'Must be a number';
} else {
if (setting.min !== undefined && num < setting.min) {
isValid = false;
errorMessage = 'Must be at least ' + setting.min;
}
if (setting.max !== undefined && num > setting.max) {
isValid = false;
errorMessage = 'Must be at most ' + setting.max;
}
}
}
if (isValid) {
validationElement.textContent = '';
const input = document.getElementById(key);
if (input) input.style.borderColor = '';
} else {
validationElement.textContent = errorMessage;
const input = document.getElementById(key);
if (input) input.style.borderColor = 'var(--danger-color)';
}
return isValid;
}
// showSection function removed - now showing all sections at once
async function saveConfiguration() {
try {
// Validate all current settings
let hasErrors = false;
for (const [key, value] of Object.entries(configData)) {
if (!validateSetting(key, value)) {
hasErrors = true;
}
}
if (hasErrors) {
updateStatus('error');
alert('Please fix validation errors before saving.');
return;
}
updateStatus('saving');
const response = await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ settings: configData })
});
const result = await response.json();
if (result.success) {
originalConfig = { ...configData };
unsavedChanges = false;
updateStatus('saved');
// Show success message briefly
const statusElement = document.getElementById('configStatus');
if (statusElement) {
statusElement.innerHTML = '<span class="status-indicator saved">✅ Configuration saved!</span>';
setTimeout(() => {
statusElement.innerHTML = '<span class="status-indicator saved">✅ Saved</span>';
}, 3000);
}
} else {
updateStatus('error');
alert('Failed to save configuration: ' + (result.error || 'Unknown error'));
}
} catch (error) {
updateStatus('error');
alert('Failed to save configuration: ' + error.message);
}
}
async function reloadConfiguration() {
if (unsavedChanges) {
if (!confirm('You have unsaved changes. Are you sure you want to reload?')) {
return;
}
}
await loadConfiguration();
}
async function resetSection() {
if (!confirm('Reset all configuration settings to defaults?')) {
return;
}
try {
const response = await fetch('/api/config/reset', {
method: 'POST'
});
const result = await response.json();
if (result.success) {
await loadConfiguration();
} else {
alert('Failed to reset configuration: ' + (result.error || 'Unknown error'));
}
} catch (error) {
alert('Failed to reset configuration: ' + error.message);
}
}
async function downloadConfig() {
try {
const response = await fetch('/api/config');
const data = await response.json();
// Generate config file content
let content = '# Minetest configuration file\\n';
content += '# Generated by LuHost\\n\\n';
for (const [key, value] of Object.entries(data.current)) {
content += key + ' = ' + value + '\\n';
}
// Download as file
const blob = new Blob([content], { type: 'text/plain' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'minetest.conf';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
} catch (error) {
alert('Failed to download config: ' + error.message);
}
}
function updateStatus(status) {
const statusElement = document.getElementById('configStatus');
if (!statusElement) return;
switch (status) {
case 'saved':
statusElement.innerHTML = '<span class="status-indicator saved">✅ Saved</span>';
break;
case 'modified':
statusElement.innerHTML = '<span class="status-indicator modified">⚠️ Modified</span>';
break;
case 'saving':
statusElement.innerHTML = '<span class="status-indicator">💾 Saving...</span>';
break;
case 'error':
statusElement.innerHTML = '<span class="status-indicator error">❌ Error</span>';
break;
}
}
</script>
`;
%>
<%- include('../layout', { body: body, currentPage: 'config', title: title }) %>