Fix server management issues and improve overall stability
Major server management fixes: - Replace Flatpak-specific pkill with universal process tree termination using pstree + process.kill() - Fix signal format errors (SIGTERM/SIGKILL instead of TERM/KILL strings) - Add 5-second cooldown after server stop to prevent race conditions with external detection - Enable Stop Server button for external servers in UI - Implement proper timeout handling with process tree killing ContentDB improvements: - Fix download retry logic and "closed" error by preventing concurrent zip extraction - Implement smart root directory detection and stripping during package extraction - Add game-specific timeout handling (8s for VoxeLibre vs 3s for simple games) World creation fixes: - Make world creation asynchronous to prevent browser hangs - Add WebSocket notifications for world creation completion status Other improvements: - Remove excessive debug logging - Improve error handling and user feedback throughout the application - Clean up temporary files and unnecessary logging 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,53 @@ const body = `
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${isFirstUser ? `
|
||||
<div class="form-group" style="margin-top: 2rem;">
|
||||
<label for="dataDirectory">Luanti Data Directory*</label>
|
||||
<select id="dataDirectory" name="dataDirectory" class="form-control" required>
|
||||
${detectedDirectories.length > 0 ?
|
||||
detectedDirectories.map(dir => `
|
||||
<option value="${dir.path}"
|
||||
${dir.path === defaultDataDir ? 'selected' : ''}
|
||||
data-confidence="${dir.confidence}">
|
||||
${dir.type}: ${dir.path}
|
||||
${dir.hasConfig ? ' ✓ Config' : ''}
|
||||
${dir.hasWorlds ? ' ✓ Worlds' : ''}
|
||||
${dir.hasDebug ? ' ✓ Active' : ''}
|
||||
</option>
|
||||
`).join('') :
|
||||
`<option value="${defaultDataDir}">${defaultDataDir} (Default)</option>`
|
||||
}
|
||||
<option value="custom">Custom directory...</option>
|
||||
</select>
|
||||
<small style="color: var(--text-secondary);">
|
||||
Choose the correct Luanti data directory based on your installation method.
|
||||
Directories with ✓ marks have existing Luanti files.
|
||||
</small>
|
||||
|
||||
<div id="customDirGroup" style="display: none; margin-top: 1rem;">
|
||||
<label for="customDataDirectory">Custom Directory Path</label>
|
||||
<input type="text"
|
||||
id="customDataDirectory"
|
||||
name="customDataDirectory"
|
||||
class="form-control"
|
||||
placeholder="/path/to/luanti/data/directory">
|
||||
<small style="color: var(--text-secondary);">Enter the full path to your Luanti data directory</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<h4 style="margin-top: 0;">⚠️ Important: Data Directory Selection</h4>
|
||||
<p>The data directory must match where your Luanti installation stores its data:</p>
|
||||
<ul style="margin: 0.5rem 0;">
|
||||
<li><strong>Flatpak:</strong> ~/.var/app/org.luanti.luanti/.minetest</li>
|
||||
<li><strong>System Package:</strong> ~/.minetest or ~/.luanti</li>
|
||||
<li><strong>Snap:</strong> ~/snap/luanti/current/.local/share/minetest</li>
|
||||
</ul>
|
||||
<p style="margin-bottom: 0;">Choosing the wrong directory will prevent the web interface from managing your worlds and server properly.</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 2rem;">
|
||||
<a href="/login" class="btn btn-outline">
|
||||
Already have an account?
|
||||
@@ -109,6 +156,24 @@ document.getElementById('password').addEventListener('input', function() {
|
||||
confirmPassword.dispatchEvent(new Event('input'));
|
||||
}
|
||||
});
|
||||
|
||||
// Handle custom data directory selection
|
||||
const dataDirectorySelect = document.getElementById('dataDirectory');
|
||||
const customDirGroup = document.getElementById('customDirGroup');
|
||||
const customDirInput = document.getElementById('customDataDirectory');
|
||||
|
||||
if (dataDirectorySelect && customDirGroup) {
|
||||
dataDirectorySelect.addEventListener('change', function() {
|
||||
if (this.value === 'custom') {
|
||||
customDirGroup.style.display = 'block';
|
||||
customDirInput.required = true;
|
||||
} else {
|
||||
customDirGroup.style.display = 'none';
|
||||
customDirInput.required = false;
|
||||
customDirInput.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
`;
|
||||
%>
|
||||
|
@@ -279,13 +279,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showStatus(result.message + ' ✅', 'success', false);
|
||||
showStatus(result.message + ' ✅ Redirecting to Extensions...', 'success', false);
|
||||
clearForm();
|
||||
|
||||
// Auto-hide success message after 5 seconds
|
||||
// Redirect to extensions page after 2 seconds to show the newly installed package
|
||||
setTimeout(() => {
|
||||
installStatus.style.display = 'none';
|
||||
}, 5000);
|
||||
window.location.href = '/extensions';
|
||||
}, 2000);
|
||||
} else {
|
||||
showStatus(result.error || 'Installation failed', 'error', false);
|
||||
}
|
||||
|
@@ -22,7 +22,7 @@ const body = `
|
||||
<div class="stat-value" style="font-size: 1rem; word-break: break-all;">
|
||||
${stats.minetestDir}
|
||||
</div>
|
||||
<div class="stat-label">Minetest Directory</div>
|
||||
<div class="stat-label">Data Directory</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -95,7 +95,7 @@ const body = `
|
||||
<td>${systemInfo.nodeVersion}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Minetest Directory</strong></td>
|
||||
<td><strong>Data Directory</strong></td>
|
||||
<td style="word-break: break-all;">${stats.minetestDir}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@@ -31,50 +31,17 @@ const body = `
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Install Card -->
|
||||
<!-- Browse ContentDB Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4>⚡ Quick Install</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="quickInstallForm">
|
||||
${typeof csrfToken !== 'undefined' && csrfToken ? `<input type="hidden" name="_csrf" value="${csrfToken}">` : ''}
|
||||
<div class="form-group mb-3">
|
||||
<label for="quickPackageUrl">Package URL or Author/Name:</label>
|
||||
<input type="text" id="quickPackageUrl" name="packageUrl" class="form-control"
|
||||
placeholder="e.g., mesecons or author/name" required>
|
||||
<div id="quickUrlValidation"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-3" id="quickLocationGroup">
|
||||
<label for="quickInstallLocation">Install Location:</label>
|
||||
<select id="quickInstallLocation" name="installLocation" class="form-control">
|
||||
<option value="global">Global</option>
|
||||
<option value="world">Specific World</option>
|
||||
</select>
|
||||
|
||||
<select id="quickWorldName" name="worldName" class="form-control mt-2" style="display: none;">
|
||||
<option value="">Select a world...</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-3">
|
||||
<label>
|
||||
<input type="checkbox" name="installDeps" value="on">
|
||||
Install Dependencies
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="quickInstallBtn" class="btn btn-primary btn-block">
|
||||
📦 Install
|
||||
</button>
|
||||
|
||||
<div id="quickInstallStatus" style="display: none;">
|
||||
<div id="quickStatusAlert" class="alert mt-2">
|
||||
<span id="quickStatusMessage"></span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="card-body text-center" style="padding: 2rem;">
|
||||
<div style="font-size: 3rem; margin-bottom: 1rem;">🌐</div>
|
||||
<h4 style="margin-bottom: 1rem;">Browse ContentDB</h4>
|
||||
<p style="color: var(--text-secondary); margin-bottom: 1.5rem; font-size: 0.9rem;">
|
||||
Discover and install thousands of games, mods, and texture packs from the Luanti community.
|
||||
</p>
|
||||
<a href="/contentdb" class="btn btn-primary btn-lg" style="padding: 1rem 2rem; font-size: 1.1rem;">
|
||||
🚀 Explore ContentDB
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -431,231 +398,6 @@ const body = `
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const quickInstallForm = document.getElementById('quickInstallForm');
|
||||
const quickPackageUrlInput = document.getElementById('quickPackageUrl');
|
||||
const quickInstallLocationSelect = document.getElementById('quickInstallLocation');
|
||||
const quickWorldNameSelect = document.getElementById('quickWorldName');
|
||||
const quickLocationGroup = document.getElementById('quickLocationGroup');
|
||||
const quickInstallBtn = document.getElementById('quickInstallBtn');
|
||||
const quickInstallStatus = document.getElementById('quickInstallStatus');
|
||||
const quickUrlValidation = document.getElementById('quickUrlValidation');
|
||||
|
||||
// Load available worlds
|
||||
loadWorlds();
|
||||
|
||||
// Show/hide world selection
|
||||
quickInstallLocationSelect.addEventListener('change', function() {
|
||||
if (this.value === 'world') {
|
||||
quickWorldNameSelect.style.display = 'block';
|
||||
quickWorldNameSelect.required = true;
|
||||
} else {
|
||||
quickWorldNameSelect.style.display = 'none';
|
||||
quickWorldNameSelect.required = false;
|
||||
}
|
||||
});
|
||||
|
||||
// URL validation
|
||||
let validationTimeout;
|
||||
quickPackageUrlInput.addEventListener('input', function() {
|
||||
clearTimeout(validationTimeout);
|
||||
validationTimeout = setTimeout(() => {
|
||||
validateUrl(this.value);
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// Quick install form submission
|
||||
quickInstallForm.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const url = quickPackageUrlInput.value.trim();
|
||||
if (!url) {
|
||||
showQuickStatus('Please enter a package URL', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
quickInstallBtn.disabled = true;
|
||||
quickInstallBtn.textContent = '⏳ Installing...';
|
||||
showQuickStatus('Installing package...', 'info');
|
||||
|
||||
try {
|
||||
const formData = new FormData(this);
|
||||
const params = new URLSearchParams();
|
||||
|
||||
for (let [key, value] of formData.entries()) {
|
||||
params.append(key, value);
|
||||
}
|
||||
|
||||
const response = await fetch('/extensions/install-url', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: params
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showQuickStatus(result.message + ' ✅', 'success');
|
||||
quickInstallForm.reset();
|
||||
quickWorldNameSelect.style.display = 'none';
|
||||
quickWorldNameSelect.required = false;
|
||||
|
||||
// Reload page after 2 seconds to show new extension
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
// Handle specific validation errors with better messaging
|
||||
if (result.type === 'invalid_installation_target' && result.packageType === 'game') {
|
||||
showQuickStatus('❌ ' + result.error, 'warning');
|
||||
} else {
|
||||
showQuickStatus(result.error || 'Installation failed', 'danger');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Installation error:', error);
|
||||
showQuickStatus('Installation failed: ' + error.message, 'danger');
|
||||
} finally {
|
||||
quickInstallBtn.disabled = false;
|
||||
quickInstallBtn.textContent = '📦 Install';
|
||||
}
|
||||
});
|
||||
|
||||
async function loadWorlds() {
|
||||
try {
|
||||
const response = await fetch('/api/worlds');
|
||||
const worlds = await response.json();
|
||||
|
||||
quickWorldNameSelect.innerHTML = '<option value="">Select a world...</option>';
|
||||
worlds.forEach(world => {
|
||||
const option = document.createElement('option');
|
||||
option.value = world.name;
|
||||
option.textContent = world.displayName || world.name;
|
||||
quickWorldNameSelect.appendChild(option);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load worlds:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function validateUrl(url) {
|
||||
if (!url.trim()) {
|
||||
quickUrlValidation.innerHTML = '';
|
||||
resetLocationOptions();
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = parseContentDBUrl(url);
|
||||
|
||||
if (parsed.author && parsed.name) {
|
||||
quickUrlValidation.innerHTML = '<small class="text-info">🔄 Checking package...</small>';
|
||||
|
||||
try {
|
||||
// Check package type via API
|
||||
const response = await fetch('/api/contentdb/package-info', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ author: parsed.author, name: parsed.name })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const packageInfo = await response.json();
|
||||
const packageType = packageInfo.type || 'mod';
|
||||
|
||||
if (packageType === 'game') {
|
||||
quickUrlValidation.innerHTML = '<small class="text-success">✅ Game: ' + parsed.author + '/' + parsed.name + '</small>';
|
||||
restrictLocationOptionsForGame();
|
||||
} else {
|
||||
quickUrlValidation.innerHTML = '<small class="text-success">✅ ' + packageType.charAt(0).toUpperCase() + packageType.slice(1) + ': ' + parsed.author + '/' + parsed.name + '</small>';
|
||||
resetLocationOptions();
|
||||
}
|
||||
} else {
|
||||
quickUrlValidation.innerHTML = '<small class="text-success">✅ Valid: ' + parsed.author + '/' + parsed.name + '</small>';
|
||||
resetLocationOptions();
|
||||
}
|
||||
} catch (error) {
|
||||
quickUrlValidation.innerHTML = '<small class="text-success">✅ Valid: ' + parsed.author + '/' + parsed.name + '</small>';
|
||||
resetLocationOptions();
|
||||
}
|
||||
} else {
|
||||
quickUrlValidation.innerHTML = '<small class="text-danger">❌ Invalid URL format</small>';
|
||||
resetLocationOptions();
|
||||
}
|
||||
}
|
||||
|
||||
function restrictLocationOptionsForGame() {
|
||||
// For games, only allow global installation
|
||||
quickInstallLocationSelect.innerHTML = '<option value="global">Global (Games are shared across all worlds)</option>';
|
||||
quickInstallLocationSelect.disabled = true;
|
||||
quickWorldNameSelect.style.display = 'none';
|
||||
quickWorldNameSelect.required = false;
|
||||
|
||||
// Add explanation
|
||||
const existingWarning = document.getElementById('game-warning');
|
||||
if (!existingWarning) {
|
||||
const warning = document.createElement('div');
|
||||
warning.id = 'game-warning';
|
||||
warning.className = 'alert alert-info mt-2';
|
||||
warning.innerHTML = '<small><strong>ℹ️ Note:</strong> Games are installed globally and shared across all worlds. To use this game, create a new world and select it during world creation.</small>';
|
||||
quickLocationGroup.appendChild(warning);
|
||||
}
|
||||
}
|
||||
|
||||
function resetLocationOptions() {
|
||||
// Reset to normal options
|
||||
quickInstallLocationSelect.innerHTML =
|
||||
'<option value="global">Global</option>' +
|
||||
'<option value="world">Specific World</option>';
|
||||
quickInstallLocationSelect.disabled = false;
|
||||
|
||||
// Remove warning if it exists
|
||||
const warning = document.getElementById('game-warning');
|
||||
if (warning) {
|
||||
warning.remove();
|
||||
}
|
||||
|
||||
// Reset world selection based on current value
|
||||
if (quickInstallLocationSelect.value === 'world') {
|
||||
quickWorldNameSelect.style.display = 'block';
|
||||
quickWorldNameSelect.required = true;
|
||||
} else {
|
||||
quickWorldNameSelect.style.display = 'none';
|
||||
quickWorldNameSelect.required = false;
|
||||
}
|
||||
}
|
||||
|
||||
function parseContentDBUrl(url) {
|
||||
url = url.replace(/^https?:\\/\\//, '').replace(/\\/$/, '');
|
||||
|
||||
const patterns = [
|
||||
/^content\\.luanti\\.org\\/packages\\/([^/]+)\\/([^/]+)$/,
|
||||
/^([^/]+)\\/([^/]+)$/
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = url.match(pattern);
|
||||
if (match) {
|
||||
return { author: match[1], name: match[2] };
|
||||
}
|
||||
}
|
||||
|
||||
return { author: null, name: null };
|
||||
}
|
||||
|
||||
function showQuickStatus(message, type) {
|
||||
const statusAlert = document.getElementById('quickStatusAlert');
|
||||
const statusMessage = document.getElementById('quickStatusMessage');
|
||||
|
||||
const alertClass = 'alert-' + type;
|
||||
statusAlert.className = 'alert mt-2 ' + alertClass;
|
||||
statusMessage.textContent = message;
|
||||
quickInstallStatus.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
function filterExtensions(type) {
|
||||
const cards = document.querySelectorAll('.extension-card');
|
||||
const tabs = document.querySelectorAll('.tab-btn');
|
||||
|
Reference in New Issue
Block a user