Files
LuHost/views/contentdb/index.ejs
Nathan Schneider 2d3b1166fe 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>
2025-08-24 19:17:38 -06:00

429 lines
16 KiB
Plaintext

<%
const body = `
<div class="page-header">
<h2>ContentDB Installer</h2>
<p>Install mods, games, and texture packs by pasting ContentDB URLs</p>
</div>
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h3>Install from URL</h3>
</div>
<div class="card-body">
<form method="POST" action="/contentdb/install-url" id="installForm">
${typeof csrfToken !== 'undefined' && csrfToken ? `<input type="hidden" name="_csrf" value="${csrfToken}">` : ''}
<div class="form-group">
<label for="packageUrl">ContentDB Package URL*</label>
<input type="text"
id="packageUrl"
name="packageUrl"
class="form-control"
placeholder="https://content.luanti.org/packages/author/package_name/"
required>
<small class="form-text text-muted">
Paste any ContentDB package URL or use format: author/package_name
</small>
<div id="urlValidation" class="mt-2"></div>
</div>
<div class="form-row" id="locationSelectionGroup">
<div class="form-group">
<label for="installLocation">Install Location</label>
<select name="installLocation" id="installLocation" class="form-control" required>
<option value="global">Global</option>
<option value="world">Specific World</option>
</select>
<small class="form-text text-muted" id="locationHelp">Choose where to install this content</small>
</div>
<div class="form-group" id="worldSelectionGroup" style="display: none;">
<label for="worldName">Target World</label>
<select name="worldName" id="worldName" class="form-control">
<option value="">Select a world...</option>
<!-- Will be populated dynamically -->
</select>
</div>
</div>
<div class="form-group" id="dependencyGroup">
<div class="checkbox-wrapper">
<input type="checkbox" id="installDeps" name="installDeps" checked>
<label for="installDeps" class="checkbox-label">
Install dependencies automatically
</label>
</div>
<small class="form-text text-muted" id="depsHelp">
Recommended: Automatically download and install required dependencies
</small>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success btn-lg" id="installBtn">
📦 Install Package
</button>
<button type="button" class="btn btn-secondary" onclick="clearForm()">
Clear
</button>
</div>
</form>
<div id="installStatus" style="display: none;">
<div class="alert" id="statusAlert">
<div id="statusMessage"></div>
<div id="installProgress" class="mt-2">
<div class="spinner"></div>
<span>Installing package...</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h3>How to Use</h3>
</div>
<div class="card-body">
<h4>📋 Step 1: Copy URL</h4>
<p>Go to <a href="https://content.luanti.org" target="_blank">content.luanti.org</a> and copy the URL of any content (mods, games, texture packs).</p>
<h4>📍 Step 2: Auto-Detection</h4>
<p><strong>Games</strong> install automatically to games directory.<br>
<strong>Mods</strong> let you choose global or world-specific.<br>
<strong>Texture packs</strong> install automatically to textures directory.</p>
<h4>⚡ Step 3: Install</h4>
<p>Click install and dependencies will be resolved automatically.</p>
</div>
</div>
<div class="card">
<div class="card-header">
<h3>🔄 Package Updates</h3>
</div>
<div class="card-body">
<p>Check for updates to your installed packages:</p>
<a href="/contentdb/updates" class="btn btn-primary btn-block">Check for Updates</a>
<a href="/contentdb/installed" class="btn btn-outline-primary btn-block">View Installed</a>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3>📝 Supported URL Formats</h3>
</div>
<div class="card-body">
<div class="url-examples">
<h4>✅ Supported Formats:</h4>
<ul>
<li><code>https://content.luanti.org/packages/author/package_name/</code></li>
<li><code>content.luanti.org/packages/author/package_name/</code></li>
<li><code>author/package_name</code> (direct format)</li>
</ul>
<h4>📋 Example URLs:</h4>
<ul>
<li><strong>Mod:</strong> <code>https://content.luanti.org/packages/VanessaE/basic_materials/</code></li>
<li><strong>Game:</strong> <code>https://content.luanti.org/packages/GreenXenith/nodecore/</code></li>
<li><strong>Texture Pack:</strong> <code>https://content.luanti.org/packages/author/texture_pack/</code></li>
<li><strong>Direct:</strong> <code>VanessaE/basic_materials</code></li>
</ul>
</div>
</div>
</div>
<style>
.form-row {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.form-row .form-group {
flex: 1;
min-width: 250px;
}
.url-examples ul {
margin-left: 1.5rem;
}
.url-examples li {
margin-bottom: 0.5rem;
}
.url-examples code {
background: var(--bg-accent);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-family: monospace;
color: var(--text-primary);
}
.btn-block {
width: 100%;
margin-bottom: 0.5rem;
}
#urlValidation {
font-size: 0.875rem;
}
.validation-success {
color: var(--success-color);
}
.validation-error {
color: var(--danger-color);
}
.validation-info {
color: var(--primary-color);
}
#installProgress {
display: flex;
align-items: center;
gap: 0.5rem;
}
@media (max-width: 768px) {
.form-row {
flex-direction: column;
}
.form-row .form-group {
min-width: auto;
}
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const packageUrlInput = document.getElementById('packageUrl');
const installLocationSelect = document.getElementById('installLocation');
const worldSelectionGroup = document.getElementById('worldSelectionGroup');
const worldNameSelect = document.getElementById('worldName');
const installForm = document.getElementById('installForm');
const installBtn = document.getElementById('installBtn');
const installStatus = document.getElementById('installStatus');
const urlValidation = document.getElementById('urlValidation');
const locationSelectionGroup = document.getElementById('locationSelectionGroup');
const dependencyGroup = document.getElementById('dependencyGroup');
const locationHelp = document.getElementById('locationHelp');
const depsHelp = document.getElementById('depsHelp');
let currentPackageType = null;
// Load available worlds
loadWorlds();
// Show/hide world selection based on install location
installLocationSelect.addEventListener('change', function() {
if (this.value === 'world') {
worldSelectionGroup.style.display = 'block';
worldNameSelect.required = true;
} else {
worldSelectionGroup.style.display = 'none';
worldNameSelect.required = false;
}
});
// Real-time URL validation
let validationTimeout;
packageUrlInput.addEventListener('input', function() {
clearTimeout(validationTimeout);
validationTimeout = setTimeout(() => {
validateUrl(this.value);
}, 500);
});
// Form submission
installForm.addEventListener('submit', async function(e) {
e.preventDefault();
const url = packageUrlInput.value.trim();
if (!url) {
showError('Please enter a package URL');
return;
}
// Show installation status
installBtn.disabled = true;
installBtn.textContent = '⏳ Installing...';
showStatus('Installing package...', 'info', true);
try {
const formData = new FormData(this);
const params = new URLSearchParams();
// Convert FormData to URLSearchParams for proper encoding
for (let [key, value] of formData.entries()) {
params.append(key, value);
}
const response = await fetch('/contentdb/install-url', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: params
});
const result = await response.json();
if (result.success) {
showStatus(result.message + ' ✅ Redirecting to Extensions...', 'success', false);
clearForm();
// Redirect to extensions page after 2 seconds to show the newly installed package
setTimeout(() => {
window.location.href = '/extensions';
}, 2000);
} else {
showStatus(result.error || 'Installation failed', 'error', false);
}
} catch (error) {
console.error('Installation error:', error);
showStatus('Installation failed: ' + error.message, 'error', false);
} finally {
installBtn.disabled = false;
installBtn.textContent = '📦 Install Package';
}
});
async function loadWorlds() {
try {
const response = await fetch('/api/worlds');
const worlds = await response.json();
worldNameSelect.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;
worldNameSelect.appendChild(option);
});
} catch (error) {
console.error('Failed to load worlds:', error);
}
}
async function validateUrl(url) {
if (!url.trim()) {
urlValidation.innerHTML = '';
resetUIForPackageType();
return;
}
// Parse URL client-side for immediate feedback
const parsed = parseContentDBUrl(url);
if (parsed.author && parsed.name) {
// Show valid URL format - type detection happens during installation
urlValidation.innerHTML = '<span class="validation-success">✅ Valid: ' + parsed.author + '/' + parsed.name + ' (type will be detected during installation)</span>';
resetUIForPackageType();
} else {
urlValidation.innerHTML = '<span class="validation-error">❌ Invalid URL format</span>';
resetUIForPackageType();
}
}
function parseContentDBUrl(url) {
// Remove protocol and clean up
url = url.replace(/^https?:\\/\\//, '').replace(/\\/$/, '');
// Match patterns
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 updateUIForPackageType(packageType, author, name, title) {
const typeDisplayName = packageType === 'game' ? 'Game' :
packageType === 'txp' ? 'Texture Pack' : 'Mod';
const typeEmoji = packageType === 'game' ? '🎮' :
packageType === 'txp' ? '🎨' : '📦';
urlValidation.innerHTML = \`<span class="validation-success">✅ \${typeEmoji} \${typeDisplayName}: \${title || (author + '/' + name)}</span>\`;
if (packageType === 'game') {
// Games go to games directory - no location choice
locationSelectionGroup.style.display = 'none';
dependencyGroup.style.display = 'none';
installBtn.innerHTML = '🎮 Install Game';
} else if (packageType === 'txp') {
// Texture packs go to textures directory - no location choice
locationSelectionGroup.style.display = 'none';
dependencyGroup.style.display = 'none';
installBtn.innerHTML = '🎨 Install Texture Pack';
} else {
// Mods can be installed globally or per-world
locationSelectionGroup.style.display = 'block';
dependencyGroup.style.display = 'block';
locationHelp.textContent = 'Choose where to install this mod';
depsHelp.textContent = 'Recommended: Automatically download and install required dependencies';
installBtn.innerHTML = '📦 Install Mod';
}
}
function resetUIForPackageType() {
currentPackageType = null;
locationSelectionGroup.style.display = 'block';
dependencyGroup.style.display = 'block';
installBtn.innerHTML = '📦 Install Package';
locationHelp.textContent = 'Choose where to install this content';
depsHelp.textContent = 'Recommended: Automatically download and install required dependencies';
}
function showStatus(message, type, showProgress) {
const statusAlert = document.getElementById('statusAlert');
const statusMessage = document.getElementById('statusMessage');
const installProgress = document.getElementById('installProgress');
// Map alert types to Bootstrap classes
const alertClass = type === 'error' ? 'alert-danger' : type === 'info' ? 'alert-info' : 'alert-' + type;
statusAlert.className = 'alert ' + alertClass;
statusMessage.textContent = message;
installProgress.style.display = showProgress ? 'flex' : 'none';
installStatus.style.display = 'block';
// Scroll to status for better visibility
installStatus.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
function showError(message) {
showStatus(message, 'danger', false);
}
window.clearForm = function() {
installForm.reset();
urlValidation.innerHTML = '';
installStatus.style.display = 'none';
worldSelectionGroup.style.display = 'none';
worldNameSelect.required = false;
resetUIForPackageType();
};
});
</script>
`;
%>
<%- include('../layout', { body: body, currentPage: 'contentdb', title: title }) %>