Migration 145: mc_versions table with java_version, paper/forge/neoforge/fabric support flags, seeded with 11 known versions for immediate use. New: src/services/mcVersionSync.js - javaVersionForMC() handles both 1.x.y legacy and 26.x.y calendar schemes - Daily 3AM pg-boss cron fetches Paper API + Modrinth /tag/game_version, upserts mc_versions with correct loader flags and Java version mapping - getVersions(filter) for DB-backed lookups with loader filtering - semver.rcompare for proper version sorting across both schemes Routes: GET /admin/modpack-installer/mc-versions — JSON endpoint for dynamic version dropdowns, filterable by loader (paper/forge/neoforge/fabric), with hardcoded fallback if DB is unavailable. Views updated: - _vanilla_form.ejs: MC version dropdown loaded dynamically (Paper-only filter), Java auto-select reads java_version from DB row - index.ejs: search filter version dropdown loaded dynamically from mc_versions - _pack_details.ejs: client-side Java logic handles major >= 26 index.js: registers mc-version-sync cron via pg-boss after queue init. package.json: added semver ^7.6.3. ACTIVE_CONTEXT updated.
205 lines
9.6 KiB
Plaintext
205 lines
9.6 KiB
Plaintext
<!-- Pack details + install form partial (HTMX) -->
|
||
<!-- REQ-2026-04-16-modpack-installer-tweaks: all 7 tweaks applied -->
|
||
|
||
<%
|
||
// Derive first MC version for Java auto-select
|
||
var firstVersion = versions.length > 0 ? (versions[0].gameVersions || [])[0] || '' : '';
|
||
var autoJava = javaVersionForMC(firstVersion);
|
||
// Estimate pack size from first version file
|
||
var firstFile = versions.length > 0 ? (versions[0].files ? versions[0].files[0] : versions[0]) : {};
|
||
var estSizeMb = firstFile.fileLength ? Math.round(firstFile.fileLength / 1024 / 1024) : (firstFile.size ? Math.round(firstFile.size / 1024 / 1024) : 0);
|
||
%>
|
||
|
||
<div class="bg-white dark:bg-darkcard rounded-lg border border-gray-200 dark:border-gray-700 p-5">
|
||
<h2 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3">3. Configure Install</h2>
|
||
|
||
<!-- Pack info + resource estimates (tweak #7) -->
|
||
<div class="flex gap-4 mb-4">
|
||
<% if (pack.thumbnail) { %>
|
||
<img src="<%= pack.thumbnail %>" alt="" class="w-20 h-20 rounded object-cover shrink-0">
|
||
<% } %>
|
||
<div>
|
||
<h3 class="text-lg font-bold"><%= pack.name %></h3>
|
||
<p class="text-sm text-gray-400 mt-1"><%= (pack.summary || '').substring(0, 200) %></p>
|
||
<div class="flex gap-3 mt-2 text-xs">
|
||
<% if (pack.categories && pack.categories.length > 0) { %>
|
||
<% pack.categories.slice(0, 4).forEach(function(c) { %>
|
||
<span class="bg-gray-700 text-gray-300 px-1.5 py-0.5 rounded"><%= c %></span>
|
||
<% }) %>
|
||
<% } %>
|
||
<% if (estSizeMb > 0) { %>
|
||
<span class="bg-gray-700 text-cyan-300 px-1.5 py-0.5 rounded">~<%= estSizeMb %> MB</span>
|
||
<% } %>
|
||
<% if (pack.downloadCount) { %>
|
||
<span class="text-gray-500">⬇ <%= pack.downloadCount.toLocaleString() %></span>
|
||
<% } %>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<form method="POST" action="/admin/modpack-installer/install" class="space-y-4">
|
||
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
|
||
<input type="hidden" name="provider" value="<%= provider %>">
|
||
<input type="hidden" name="packId" value="<%= pack.id %>">
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<!-- Version selector -->
|
||
<div>
|
||
<label class="block text-sm font-medium mb-1">Version</label>
|
||
<select name="versionId" id="dd-version" required
|
||
onchange="onVersionChange(this)"
|
||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm">
|
||
<% versions.slice(0, 20).forEach(function(v, i) { %>
|
||
<option value="<%= v.fileId || v.versionId %>"
|
||
data-mc="<%= (v.gameVersions || [])[0] || '' %>"
|
||
<%= i === 0 ? 'selected' : '' %>>
|
||
<%= v.displayName || v.versionNumber || v.fileName %>
|
||
<% if (v.gameVersions && v.gameVersions.length) { %>(MC <%= v.gameVersions[0] %>)<% } %>
|
||
</option>
|
||
<% }) %>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Display name -->
|
||
<div>
|
||
<label class="block text-sm font-medium mb-1">Display Name</label>
|
||
<input name="displayName" required value="<%= pack.name %>"
|
||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm">
|
||
</div>
|
||
|
||
<!-- Short name / subdomain -->
|
||
<div>
|
||
<label class="block text-sm font-medium mb-1">Short Name <span class="text-gray-500 text-xs">(Discord + subdomain)</span></label>
|
||
<input name="shortName" id="dd-shortname" required value="<%= slugify(pack.name) %>" pattern="[a-z0-9-]+"
|
||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm font-mono"
|
||
oninput="document.getElementById('subdomain-preview').textContent=this.value+'.firefrostgaming.com'">
|
||
<div class="text-[10px] text-gray-500 mt-1">→ <span id="subdomain-preview" class="text-cyan-400"><%= slugify(pack.name) %>.firefrostgaming.com</span></div>
|
||
</div>
|
||
|
||
<!-- Node (tweak #1: live resource usage) -->
|
||
<div>
|
||
<label class="block text-sm font-medium mb-1">Node</label>
|
||
<select name="node" id="dd-node" required onchange="onNodeChange(this.value)"
|
||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm">
|
||
<option value="NC1">❄️ NC1 — Charlotte</option>
|
||
<option value="TX1">🔥 TX1 — Dallas</option>
|
||
</select>
|
||
<div id="node-usage" class="text-[10px] text-gray-500 mt-1">Loading node stats...</div>
|
||
</div>
|
||
|
||
<!-- RAM -->
|
||
<div>
|
||
<label class="block text-sm font-medium mb-1">RAM (MB)</label>
|
||
<input name="ramMb" id="dd-ram" type="number" required value="8192" min="4096" max="32768" step="1024"
|
||
onchange="updateJvmFlags()"
|
||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm">
|
||
</div>
|
||
|
||
<!-- Disk (tweak #2) -->
|
||
<div>
|
||
<label class="block text-sm font-medium mb-1">Disk (MB)</label>
|
||
<input name="diskMb" id="dd-disk" type="number" required value="<%= estSizeMb > 0 ? Math.max(estSizeMb * 3, 10240) : 20480 %>" min="5120" max="500000" step="1024"
|
||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm">
|
||
<% if (estSizeMb > 0) { %>
|
||
<div class="text-[10px] text-gray-500 mt-1">Pack size ~<%= estSizeMb %>MB → pre-filled 3× for world growth</div>
|
||
<% } %>
|
||
</div>
|
||
|
||
<!-- Java version (tweak #5: auto-select from MC version) -->
|
||
<div>
|
||
<label class="block text-sm font-medium mb-1">Java Version</label>
|
||
<select name="javaVersion" id="dd-java" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm">
|
||
<option value="8" <%= autoJava === 8 ? 'selected' : '' %>>Java 8</option>
|
||
<option value="17" <%= autoJava === 17 ? 'selected' : '' %>>Java 17</option>
|
||
<option value="21" <%= autoJava === 21 ? 'selected' : '' %>>Java 21</option>
|
||
</select>
|
||
<div class="text-[10px] text-gray-500 mt-1">Auto-selected for MC <%= firstVersion || 'unknown' %></div>
|
||
</div>
|
||
|
||
<!-- Port (tweak #6: auto-assign) -->
|
||
<div>
|
||
<label class="block text-sm font-medium mb-1">Port</label>
|
||
<div class="flex gap-2">
|
||
<input name="port" id="dd-port" type="number" readonly value="25565"
|
||
class="flex-1 bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-400">
|
||
<button type="button" onclick="refreshPort()" class="bg-gray-700 hover:bg-gray-600 text-white px-3 py-1 rounded text-xs">🔄 Re-roll</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Spawn type (tweak #4: add vanilla as default) -->
|
||
<div>
|
||
<label class="block text-sm font-medium mb-1">Spawn Type</label>
|
||
<select name="spawnType" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm">
|
||
<option value="vanilla" selected>Vanilla (no spawn changes)</option>
|
||
<option value="standard">Standard (Bitch Bot pastes spawn)</option>
|
||
<option value="skyblock">Skyblock (no spawn paste)</option>
|
||
<option value="has_lobby">Has Lobby (pack provides its own)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- JVM args (tweak #3: Aikar flags auto-populated) -->
|
||
<div class="md:col-span-2">
|
||
<label class="block text-sm font-medium mb-1">JVM Arguments <span class="text-gray-500 text-xs">(Aikar G1GC — auto-populated)</span></label>
|
||
<textarea name="jvmArgs" id="dd-jvm" rows="3"
|
||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-xs font-mono text-gray-300"><%= aikarFlags %></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit"
|
||
class="w-full bg-cyan-600 hover:bg-cyan-700 text-white font-semibold py-3 rounded-md text-base transition mt-4">
|
||
🚀 Install Server
|
||
</button>
|
||
</form>
|
||
</div>
|
||
|
||
<script>
|
||
var nodeData = {};
|
||
|
||
// Load node stats on partial render
|
||
fetch('/admin/modpack-installer/node-info', { headers: { 'HX-Request': 'true' } })
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
nodeData = d;
|
||
showNodeUsage(document.getElementById('dd-node').value);
|
||
});
|
||
|
||
// Load initial port
|
||
refreshPort();
|
||
|
||
function showNodeUsage(node) {
|
||
var el = document.getElementById('node-usage');
|
||
var n = nodeData[node];
|
||
if (!n) { el.textContent = ''; return; }
|
||
var ramPct = Math.round(n.ramUsedMb / n.ramTotalMb * 100);
|
||
var diskGb = Math.round(n.diskUsedMb / 1024);
|
||
var diskTotalGb = Math.round(n.diskTotalMb / 1024);
|
||
el.innerHTML = 'RAM: <strong>' + Math.round(n.ramUsedMb/1024) + 'GB / ' + Math.round(n.ramTotalMb/1024) + 'GB</strong> (' + ramPct + '%) · Disk: <strong>' + diskGb + 'GB / ' + diskTotalGb + 'GB</strong>';
|
||
}
|
||
|
||
function onNodeChange(node) {
|
||
showNodeUsage(node);
|
||
refreshPort();
|
||
}
|
||
|
||
function refreshPort() {
|
||
var node = document.getElementById('dd-node').value;
|
||
fetch('/admin/modpack-installer/next-port?node=' + node, { headers: { 'HX-Request': 'true' } })
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) { document.getElementById('dd-port').value = d.port; });
|
||
}
|
||
|
||
function onVersionChange(sel) {
|
||
var mc = sel.options[sel.selectedIndex].dataset.mc || '';
|
||
var major = parseInt(mc.split('.')[0]) || 0;
|
||
var minor = parseInt(mc.split('.')[1]) || 0;
|
||
// Calendar scheme (26+) → Java 21; Legacy 1.x → 8/17/21
|
||
var java = major >= 26 ? 21 : minor <= 16 ? 8 : minor <= 20 ? 17 : 21;
|
||
document.getElementById('dd-java').value = java;
|
||
}
|
||
|
||
function updateJvmFlags() {
|
||
// Aikar flags don't change per RAM — they're static. RAM is in -Xms/-Xmx which
|
||
// Pterodactyl handles via {{SERVER_MEMORY}} var. No rewrite needed.
|
||
}
|
||
</script>
|