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>
435 lines
14 KiB
Plaintext
435 lines
14 KiB
Plaintext
<%
|
|
const body = `
|
|
<div class="page-header">
|
|
<h2>🧩 Extensions</h2>
|
|
<p>Manage games, mods, and texture packs for your Luanti server</p>
|
|
</div>
|
|
|
|
<div class="extensions-layout">
|
|
<!-- Sidebar -->
|
|
<div class="extensions-sidebar">
|
|
<!-- Overview Card -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h4>📊 Overview</h4>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="overview-stats">
|
|
<div class="stat-item">
|
|
<strong>${statistics.games || 0}</strong>
|
|
<span>Games</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<strong>${(statistics.global_packages || 0) + (statistics.local_mods || 0)}</strong>
|
|
<span>Mods</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<strong>${statistics.total_packages || 0}</strong>
|
|
<span>Total</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Browse ContentDB Card -->
|
|
<div class="card">
|
|
<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>
|
|
|
|
<!-- Main Content -->
|
|
<div class="extensions-main">
|
|
<div class="extensions-header">
|
|
<div class="extensions-tabs">
|
|
<button class="tab-btn active" onclick="filterExtensions('all')">
|
|
All (${allContent.length})
|
|
</button>
|
|
<button class="tab-btn" onclick="filterExtensions('game')">
|
|
Games (${allContent.filter(c => (c.package_type || c.type) === 'game').length})
|
|
</button>
|
|
<button class="tab-btn" onclick="filterExtensions('mod')">
|
|
Mods (${allContent.filter(c => (c.package_type || c.type) === 'mod').length})
|
|
</button>
|
|
<button class="tab-btn" onclick="filterExtensions('txp')">
|
|
Texture Packs (${allContent.filter(c => (c.package_type || c.type) === 'txp').length})
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
${allContent.length === 0 ? `
|
|
<div class="card">
|
|
<div class="card-body text-center">
|
|
<h3>📭 No Extensions Installed</h3>
|
|
<p>Install games, mods, and texture packs from ContentDB or add them manually.</p>
|
|
<a href="/contentdb" class="btn btn-primary">
|
|
Browse ContentDB
|
|
</a>
|
|
</div>
|
|
</div>
|
|
` : `
|
|
<div class="extensions-grid" id="extensionsGrid">
|
|
${allContent.map(ext => {
|
|
const type = ext.package_type || ext.type;
|
|
const typeIcon = type === 'game' ? '🎮' : type === 'txp' ? '🎨' : '📦';
|
|
const typeBadge = type === 'game' ? 'success' : type === 'txp' ? 'warning' : 'primary';
|
|
const sourceIcon = ext.source === 'contentdb' ? '🌐' : '📁';
|
|
|
|
return `
|
|
<div class="card extension-card" data-type="${type}">
|
|
<div class="card-header">
|
|
<div class="extension-title">
|
|
<h4>${typeIcon} ${ext.title || ext.name}</h4>
|
|
<small class="text-muted">
|
|
${sourceIcon} ${ext.author || 'Local'}
|
|
${ext.source === 'contentdb' ? '(ContentDB)' : '(Local)'}
|
|
</small>
|
|
</div>
|
|
<div class="extension-badges">
|
|
<span class="badge badge-${typeBadge}">
|
|
${type === 'txp' ? 'Texture Pack' : type.charAt(0).toUpperCase() + type.slice(1)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="extension-details">
|
|
<p class="extension-description">
|
|
${ext.short_description || ext.description || 'No description available.'}
|
|
</p>
|
|
|
|
<div class="extension-meta">
|
|
<div class="meta-item">
|
|
<strong>Location:</strong>
|
|
<span class="location-badge ${ext.install_location === 'global' || ext.location === 'global' ? 'global' : 'world'}">
|
|
${ext.install_location === 'global' || ext.location === 'global' ? 'Global' :
|
|
ext.install_location ? ext.install_location.replace('world:', '') : ext.location || 'Games'}
|
|
</span>
|
|
</div>
|
|
${ext.version ? `
|
|
<div class="meta-item">
|
|
<strong>Version:</strong>
|
|
<span>${ext.version}</span>
|
|
</div>
|
|
` : ''}
|
|
<div class="meta-item">
|
|
<strong>Modified:</strong>
|
|
<span>${ext.installed_at ? new Date(ext.installed_at).toLocaleDateString() :
|
|
new Date(ext.lastModified).toLocaleDateString()}</span>
|
|
</div>
|
|
</div>
|
|
|
|
${ext.dependencies && ext.dependencies.length > 0 ? `
|
|
<div class="dependencies">
|
|
<strong>Dependencies (${ext.dependencies.length}):</strong>
|
|
<div class="dep-list">
|
|
${ext.dependencies.map(dep =>
|
|
typeof dep === 'string' ? dep : `${dep.author}/${dep.name}`
|
|
).join(', ')}
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
|
|
<div class="extension-actions">
|
|
${ext.contentdb_url ? `
|
|
<a href="${ext.contentdb_url}" target="_blank" class="btn btn-outline-primary btn-sm">
|
|
View on ContentDB
|
|
</a>
|
|
` : ''}
|
|
${ext.source === 'contentdb' ? `
|
|
<button class="btn btn-outline-warning btn-sm"
|
|
onclick="checkForUpdate('${ext.author}', '${ext.name}')">
|
|
Check Update
|
|
</button>
|
|
` : ''}
|
|
<button class="btn btn-outline-danger btn-sm"
|
|
onclick="uninstallExtension('${ext.name}', '${type}', '${ext.install_location || ext.location}')">
|
|
Remove
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('')}
|
|
</div>
|
|
`}
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.extensions-layout {
|
|
display: flex;
|
|
gap: 2rem;
|
|
}
|
|
|
|
.extensions-sidebar {
|
|
flex: 0 0 300px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.extensions-main {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.extensions-header {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.extensions-tabs {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.tab-btn {
|
|
padding: 0.5rem 1rem;
|
|
border: 1px solid var(--border-color);
|
|
background: var(--bg-secondary);
|
|
color: var(--text-secondary);
|
|
border-radius: var(--border-radius);
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.tab-btn:hover {
|
|
background: var(--bg-accent);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.tab-btn.active {
|
|
background: var(--primary-color);
|
|
color: white;
|
|
border-color: var(--primary-color);
|
|
}
|
|
|
|
.extensions-grid {
|
|
display: grid;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.extension-card {
|
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
}
|
|
|
|
.extension-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: var(--shadow-block), 0 8px 16px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.extension-card[data-type="game"] {
|
|
border-left: 4px solid var(--success-color);
|
|
}
|
|
|
|
.extension-card[data-type="mod"] {
|
|
border-left: 4px solid var(--primary-color);
|
|
}
|
|
|
|
.extension-card[data-type="txp"] {
|
|
border-left: 4px solid var(--warning-color);
|
|
}
|
|
|
|
.extension-title h4 {
|
|
margin: 0;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.extension-description {
|
|
color: var(--text-secondary);
|
|
margin-bottom: 1rem;
|
|
font-size: 0.9rem;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.extension-meta {
|
|
background: var(--bg-accent);
|
|
padding: 0.75rem;
|
|
border-radius: var(--border-radius);
|
|
margin-bottom: 1rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.extension-badges {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.stat-item {
|
|
text-align: center;
|
|
padding: 0.75rem 0;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
|
|
.stat-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.stat-item strong {
|
|
display: block;
|
|
font-size: 1.5rem;
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
.stat-item span {
|
|
font-size: 0.875rem;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.meta-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.meta-item:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.location-badge {
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 4px;
|
|
font-size: 0.75rem;
|
|
font-weight: bold;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.location-badge.global {
|
|
background: var(--success-color);
|
|
color: white;
|
|
}
|
|
|
|
.location-badge.world {
|
|
background: var(--primary-color);
|
|
color: white;
|
|
}
|
|
|
|
.dependencies {
|
|
background: var(--bg-secondary);
|
|
padding: 0.75rem;
|
|
border-radius: var(--border-radius);
|
|
margin-bottom: 1rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.dep-list {
|
|
margin-top: 0.5rem;
|
|
color: var(--text-muted);
|
|
font-family: monospace;
|
|
}
|
|
|
|
.extension-actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
flex-wrap: wrap;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.badge {
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 4px;
|
|
font-size: 0.75rem;
|
|
font-weight: bold;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.badge-primary {
|
|
background: var(--primary-color);
|
|
color: white;
|
|
}
|
|
|
|
.badge-success {
|
|
background: var(--success-color);
|
|
color: white;
|
|
}
|
|
|
|
.badge-warning {
|
|
background: var(--warning-color);
|
|
color: white;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.extensions-layout {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.extensions-sidebar {
|
|
flex: none;
|
|
order: 2;
|
|
}
|
|
|
|
.extensions-main {
|
|
order: 1;
|
|
}
|
|
|
|
.extensions-tabs {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.tab-btn {
|
|
text-align: center;
|
|
}
|
|
|
|
.extension-actions {
|
|
justify-content: stretch;
|
|
}
|
|
|
|
.extension-actions .btn {
|
|
flex: 1;
|
|
text-align: center;
|
|
}
|
|
|
|
.meta-item {
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
function filterExtensions(type) {
|
|
const cards = document.querySelectorAll('.extension-card');
|
|
const tabs = document.querySelectorAll('.tab-btn');
|
|
|
|
// Update active tab
|
|
tabs.forEach(tab => tab.classList.remove('active'));
|
|
event.target.classList.add('active');
|
|
|
|
// Filter cards
|
|
cards.forEach(card => {
|
|
if (type === 'all') {
|
|
card.style.display = 'block';
|
|
} else {
|
|
const cardType = card.getAttribute('data-type');
|
|
card.style.display = cardType === type ? 'block' : 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
function checkForUpdate(author, name) {
|
|
alert('Update checking feature coming soon!');
|
|
// TODO: Implement update checking
|
|
}
|
|
|
|
function uninstallExtension(name, type, location) {
|
|
if (confirm('Are you sure you want to remove ' + name + '?\\n\\nThis will permanently delete the extension files.')) {
|
|
alert('Uninstall feature coming soon!');
|
|
// TODO: Implement extension removal
|
|
}
|
|
}
|
|
</script>
|
|
`;
|
|
%>
|
|
|
|
<%- include('../layout', { body: body, currentPage: 'extensions', title: title }) %> |