Files
firefrost-services/services/arbiter-3.0/src/views/admin/modpack-installer/_pack_details.ejs
Claude Code e891a304e0 MC versioning overhaul: calendar scheme + DB-backed versions (REQ-2026-04-16-mc-versioning)
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.
2026-04-16 02:40:50 -05:00

205 lines
9.6 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
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.
<!-- 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>