From e891a304e0661cb353592806aa2c88e5e4f1bb68 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Thu, 16 Apr 2026 02:40:50 -0500 Subject: [PATCH] MC versioning overhaul: calendar scheme + DB-backed versions (REQ-2026-04-16-mc-versioning) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../REQ-2026-04-16-mc-versioning.md | 0 docs/code-bridge/status/ACTIVE_CONTEXT.md | 11 +- .../migrations/145_mc_versions.sql | 28 +++ services/arbiter-3.0/package.json | 1 + services/arbiter-3.0/src/index.js | 2 + .../src/routes/admin/modpack-installer.js | 35 +++- .../arbiter-3.0/src/services/mcVersionSync.js | 181 ++++++++++++++++++ .../admin/modpack-installer/_pack_details.ejs | 7 +- .../admin/modpack-installer/_vanilla_form.ejs | 43 +++-- .../views/admin/modpack-installer/index.ejs | 19 +- 10 files changed, 297 insertions(+), 30 deletions(-) rename docs/code-bridge/{requests => archive}/REQ-2026-04-16-mc-versioning.md (100%) create mode 100644 services/arbiter-3.0/migrations/145_mc_versions.sql create mode 100644 services/arbiter-3.0/src/services/mcVersionSync.js diff --git a/docs/code-bridge/requests/REQ-2026-04-16-mc-versioning.md b/docs/code-bridge/archive/REQ-2026-04-16-mc-versioning.md similarity index 100% rename from docs/code-bridge/requests/REQ-2026-04-16-mc-versioning.md rename to docs/code-bridge/archive/REQ-2026-04-16-mc-versioning.md diff --git a/docs/code-bridge/status/ACTIVE_CONTEXT.md b/docs/code-bridge/status/ACTIVE_CONTEXT.md index 10ac78e..1ae53d0 100644 --- a/docs/code-bridge/status/ACTIVE_CONTEXT.md +++ b/docs/code-bridge/status/ACTIVE_CONTEXT.md @@ -5,7 +5,7 @@ Local Nitro (Windows 11) at `C:\Users\mkrau\firefrost-services`. Git remote: Gitea. Identity: `Claude Code `. ## Current Focus -Bridge queue empty. Seven features shipped tonight, all pending deploy by Michael. +Bridge queue empty. Eight features shipped tonight, all pending deploy by Michael. ## Session 2026-04-16 @@ -35,6 +35,15 @@ Bridge queue empty. Seven features shipped tonight, all pending deploy by Michae - `index.ejs`: "New Vanilla / Paper Server" button loads vanilla form directly (skips pack search) - No migrations. **Deploy:** standard restart only, ensure `standard-plugins/1.21.1/` populated in NextCloud before first vanilla install +- **MC Versioning Overhaul** (REQ-2026-04-16-mc-versioning) + - Migration 145: `mc_versions` table with loader support flags, seeded with 11 known versions + - `mcVersionSync.js`: new `javaVersionForMC()` handles both legacy 1.x.y and calendar 26.x.y schemes; daily 3AM pg-boss cron syncs from Paper API + Modrinth `/tag/game_version`; `getVersions(filter)` for DB-backed lookups + - `/mc-versions` endpoint returns cached versions (filterable by loader), with hardcoded fallback + - `_vanilla_form.ejs` + `index.ejs` search filter: replaced hardcoded version dropdowns with dynamic fetch from mc_versions + - `_pack_details.ejs`: updated client-side Java auto-select for calendar versioning + - Added `semver ^7.6.3` to package.json + - **Deploy:** `npm install` (semver) + run migration 145 + restart + - **Discord Action Log — Issue #1** (`49f8f79`, +263 lines) - Migration 142: `discord_action_log` table - `discordActionLog.js` service (silent-fail logAction) diff --git a/services/arbiter-3.0/migrations/145_mc_versions.sql b/services/arbiter-3.0/migrations/145_mc_versions.sql new file mode 100644 index 0000000..c093f73 --- /dev/null +++ b/services/arbiter-3.0/migrations/145_mc_versions.sql @@ -0,0 +1,28 @@ +-- Migration 145: mc_versions table (REQ-2026-04-16-mc-versioning) +-- Caches available MC versions with loader support flags. +-- Refreshed daily at 3AM via pg-boss cron job. + +CREATE TABLE IF NOT EXISTS mc_versions ( + version VARCHAR(20) PRIMARY KEY, + java_version INT NOT NULL DEFAULT 21, + paper_supported BOOLEAN DEFAULT FALSE, + forge_supported BOOLEAN DEFAULT FALSE, + neoforge_supported BOOLEAN DEFAULT FALSE, + fabric_supported BOOLEAN DEFAULT FALSE, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Seed with known versions so installer works before first cron run +INSERT INTO mc_versions (version, java_version, paper_supported, forge_supported, neoforge_supported, fabric_supported) VALUES + ('26.1.2', 21, true, false, true, true), + ('26.1.1', 21, true, false, true, true), + ('26.1', 21, true, false, true, true), + ('1.21.4', 21, true, false, true, true), + ('1.21.1', 21, true, true, true, true), + ('1.20.4', 17, true, true, false, true), + ('1.20.1', 17, true, true, false, true), + ('1.19.4', 17, true, true, false, true), + ('1.19.2', 17, true, true, false, true), + ('1.18.2', 17, true, true, false, true), + ('1.16.5', 8, true, true, false, true) +ON CONFLICT (version) DO NOTHING; diff --git a/services/arbiter-3.0/package.json b/services/arbiter-3.0/package.json index ae4e6f0..614761a 100644 --- a/services/arbiter-3.0/package.json +++ b/services/arbiter-3.0/package.json @@ -25,6 +25,7 @@ "node-cron": "^3.0.3", "passport": "^0.7.0", "pg-boss": "^10.1.5", + "semver": "^7.6.3", "passport-discord": "^0.1.4", "pg": "^8.11.3", "socket.io-client": "^4.7.5", diff --git a/services/arbiter-3.0/src/index.js b/services/arbiter-3.0/src/index.js index cf1dbce..bb0229a 100644 --- a/services/arbiter-3.0/src/index.js +++ b/services/arbiter-3.0/src/index.js @@ -161,6 +161,7 @@ const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_BOT_TOKEN) // Initialize pg-boss job queue (Task #101 — Modpack Installer) const PgBoss = require('pg-boss'); const { handleInstallJob } = require('./services/modpackInstaller'); +const { registerCronJob: registerVersionSync } = require('./services/mcVersionSync'); (async () => { try { const boss = new PgBoss({ @@ -174,6 +175,7 @@ const { handleInstallJob } = require('./services/modpackInstaller'); await boss.work('modpack-installs', { teamConcurrency: 2 }, handleInstallJob); app.locals.pgBoss = boss; console.log('✅ pg-boss job queue initialized (modpack-installs concurrency: 2)'); + await registerVersionSync(boss); } catch (err) { console.error('⚠️ pg-boss init failed (installer will not work):', err.message); } diff --git a/services/arbiter-3.0/src/routes/admin/modpack-installer.js b/services/arbiter-3.0/src/routes/admin/modpack-installer.js index ffdf6fa..52e5cb4 100644 --- a/services/arbiter-3.0/src/routes/admin/modpack-installer.js +++ b/services/arbiter-3.0/src/routes/admin/modpack-installer.js @@ -18,11 +18,12 @@ const axios = require('axios'); const db = require('../../database'); const { getProvider, listProviders } = require('../../services/providerApi'); const { estimateRam, slugify } = require('../../services/modpackInstaller'); +const { javaVersionForMC, getVersions: getMCVersions } = require('../../services/mcVersionSync'); const PANEL_URL = process.env.PANEL_URL || 'https://panel.firefrostgaming.com'; const PANEL_ADMIN_KEY = process.env.PANEL_ADMIN_KEY || ''; -// Aikar G1GC flags template — {RAM} replaced with allocation in MB +// Aikar G1GC flags template const AIKAR_FLAGS = '-XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200 ' + '-XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:+AlwaysPreTouch ' + '-XX:G1NewSizePercent=30 -XX:G1MaxNewSizePercent=40 -XX:G1HeapRegionSize=8M ' + @@ -32,15 +33,6 @@ const AIKAR_FLAGS = '-XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMill '-XX:MaxTenuringThreshold=1 -Dusing.aikars.flags=https://mcflags.emc.gs ' + '-Daikars.new.flags=true'; -function javaVersionForMC(mcVersion) { - if (!mcVersion) return 21; - const parts = mcVersion.split('.').map(Number); - const minor = parts[1] || 0; - if (minor <= 16) return 8; - if (minor <= 20) return 17; - return 21; -} - // ─── Node resource usage (JSON for form) ──────────────────────────────────── router.get('/node-info', async (req, res) => { try { @@ -99,6 +91,29 @@ router.get('/next-port', async (req, res) => { } }); +// ─── MC versions (JSON for dynamic dropdowns) ────────────────────────────── +router.get('/mc-versions', async (req, res) => { + try { + const filter = {}; + if (req.query.paper) filter.paper = true; + if (req.query.forge) filter.forge = true; + if (req.query.neoforge) filter.neoforge = true; + if (req.query.fabric) filter.fabric = true; + const versions = await getMCVersions(filter); + res.json(versions); + } catch (err) { + console.error('[Installer] mc-versions error:', err.message); + // Fallback to hardcoded list if DB is down + res.json([ + { version: '26.1.2', java_version: 21 }, + { version: '1.21.1', java_version: 21 }, + { version: '1.20.1', java_version: 17 }, + { version: '1.18.2', java_version: 17 }, + { version: '1.16.5', java_version: 8 } + ]); + } +}); + // ─── Main page ────────────────────────────────────────────────────────────── router.get('/', (req, res) => { res.render('admin/modpack-installer/index', { diff --git a/services/arbiter-3.0/src/services/mcVersionSync.js b/services/arbiter-3.0/src/services/mcVersionSync.js new file mode 100644 index 0000000..4dc78f4 --- /dev/null +++ b/services/arbiter-3.0/src/services/mcVersionSync.js @@ -0,0 +1,181 @@ +/** + * MC Version Sync — daily refresh of mc_versions table from Paper + Modrinth APIs. + * REQ-2026-04-16-mc-versioning + * + * Runs as a pg-boss cron job at 3AM daily. Also callable on-demand from admin. + * + * Data sources: + * 1. Paper API → paper_supported versions + * 2. Modrinth /tag/game_version → all MC versions + loader tags + * + * Java version mapping handles both legacy (1.x.y) and new calendar (26.x.y) schemes. + */ + +const axios = require('axios'); +const semver = require('semver'); +const db = require('../database'); + +/** + * Determine Java version for any MC version string. + * Handles both 1.x.y legacy and 26.x.y calendar scheme. + */ +function javaVersionForMC(mcVersion) { + if (!mcVersion) return 21; + const major = parseInt(mcVersion.split('.')[0]); + + // New 2026+ calendar scheme — all Java 21 + if (major >= 26) return 21; + + // Legacy 1.x.y scheme + if (major === 1) { + const minor = parseInt(mcVersion.split('.')[1]) || 0; + if (minor >= 21) return 21; + if (minor >= 17) return 17; + return 8; + } + + return 21; // Frostwall fallback +} + +/** + * Fetch Paper-supported versions from Paper API. + */ +async function fetchPaperVersions() { + try { + const resp = await axios.get('https://api.papermc.io/v2/projects/paper', { timeout: 10000 }); + return new Set(resp.data.versions || []); + } catch (err) { + console.error('[MCVersionSync] Paper API error:', err.message); + return new Set(); + } +} + +/** + * Fetch all MC versions + loader tags from Modrinth. + */ +async function fetchModrinthVersions() { + try { + const resp = await axios.get('https://api.modrinth.com/v2/tag/game_version', { + headers: { 'User-Agent': 'FirefrostGaming/Arbiter (contact@firefrostgaming.com)' }, + timeout: 10000 + }); + // Filter to releases only (skip snapshots) + return (resp.data || []).filter(v => v.version_type === 'release'); + } catch (err) { + console.error('[MCVersionSync] Modrinth API error:', err.message); + return []; + } +} + +/** + * Sort MC version strings properly — handles both 1.x.y and 26.x.y. + * Uses semver.rcompare where possible, falls back to string compare. + */ +function sortVersions(versions) { + return versions.sort((a, b) => { + // Coerce to semver-compatible (pad to 3 parts) + const pad = v => { + const parts = v.split('.'); + while (parts.length < 3) parts.push('0'); + return parts.join('.'); + }; + const sa = semver.valid(semver.coerce(pad(a))); + const sb = semver.valid(semver.coerce(pad(b))); + if (sa && sb) return semver.rcompare(sa, sb); + return b.localeCompare(a); + }); +} + +/** + * Run the full sync. Upserts mc_versions from Paper + Modrinth data. + * Returns { synced, paperCount, total }. + */ +async function syncVersions() { + console.log('[MCVersionSync] Starting version sync...'); + + const [paperVersions, modrinthVersions] = await Promise.all([ + fetchPaperVersions(), + fetchModrinthVersions() + ]); + + // Build unified version set + const allVersions = new Set(); + for (const v of paperVersions) allVersions.add(v); + for (const v of modrinthVersions) allVersions.add(v.version); + + // We only care about "major" releases (x.y.z or x.y), skip pre-releases/snapshots + const releaseVersions = [...allVersions].filter(v => /^\d+\.\d+(\.\d+)?$/.test(v)); + const sorted = sortVersions(releaseVersions); + + // Determine loader support from Modrinth tags (best-effort) + const modrinthMap = new Map(); + for (const v of modrinthVersions) { + modrinthMap.set(v.version, v); + } + + let synced = 0; + for (const version of sorted) { + const java = javaVersionForMC(version); + const paper = paperVersions.has(version); + // Modrinth doesn't give per-version loader flags in the tag endpoint, + // so we infer from version range heuristics + const major = parseInt(version.split('.')[0]); + const minor = parseInt(version.split('.')[1]) || 0; + const forge = major === 1 && minor >= 7; + const neoforge = (major === 1 && minor >= 21) || major >= 26; + const fabric = (major === 1 && minor >= 14) || major >= 26; + + await db.query(` + INSERT INTO mc_versions (version, java_version, paper_supported, forge_supported, neoforge_supported, fabric_supported, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, NOW()) + ON CONFLICT (version) DO UPDATE SET + java_version = EXCLUDED.java_version, + paper_supported = EXCLUDED.paper_supported, + forge_supported = EXCLUDED.forge_supported, + neoforge_supported = EXCLUDED.neoforge_supported, + fabric_supported = EXCLUDED.fabric_supported, + updated_at = NOW() + `, [version, java, paper, forge, neoforge, fabric]); + synced++; + } + + console.log(`[MCVersionSync] Done. Synced ${synced} versions (${paperVersions.size} Paper-supported).`); + return { synced, paperCount: paperVersions.size, total: sorted.length }; +} + +/** + * Get cached versions from DB, optionally filtered by loader. + */ +async function getVersions(filter = {}) { + let where = 'WHERE 1=1'; + if (filter.paper) where += ' AND paper_supported = true'; + if (filter.forge) where += ' AND forge_supported = true'; + if (filter.neoforge) where += ' AND neoforge_supported = true'; + if (filter.fabric) where += ' AND fabric_supported = true'; + + const result = await db.query(` + SELECT * FROM mc_versions ${where} + ORDER BY + CASE WHEN version ~ '^[0-9]+\\.' THEN CAST(SPLIT_PART(version, '.', 1) AS INT) ELSE 0 END DESC, + CASE WHEN version ~ '^[0-9]+\\.[0-9]+' THEN CAST(SPLIT_PART(version, '.', 2) AS INT) ELSE 0 END DESC, + CASE WHEN version ~ '^[0-9]+\\.[0-9]+\\.[0-9]+' THEN CAST(SPLIT_PART(version, '.', 3) AS INT) ELSE 0 END DESC + `); + return result.rows; +} + +/** + * Register the 3AM daily cron job with pg-boss. + */ +async function registerCronJob(boss) { + try { + await boss.schedule('mc-version-sync', '0 3 * * *'); + await boss.work('mc-version-sync', async () => { + await syncVersions(); + }); + console.log('✅ MC version sync cron registered (daily 3AM)'); + } catch (err) { + console.error('[MCVersionSync] Failed to register cron:', err.message); + } +} + +module.exports = { javaVersionForMC, syncVersions, getVersions, registerCronJob }; diff --git a/services/arbiter-3.0/src/views/admin/modpack-installer/_pack_details.ejs b/services/arbiter-3.0/src/views/admin/modpack-installer/_pack_details.ejs index a9f2661..c753ecf 100644 --- a/services/arbiter-3.0/src/views/admin/modpack-installer/_pack_details.ejs +++ b/services/arbiter-3.0/src/views/admin/modpack-installer/_pack_details.ejs @@ -190,9 +190,10 @@ function onVersionChange(sel) { var mc = sel.options[sel.selectedIndex].dataset.mc || ''; - var parts = mc.split('.').map(Number); - var minor = parts[1] || 0; - var java = minor <= 16 ? 8 : minor <= 20 ? 17 : 21; + 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; } diff --git a/services/arbiter-3.0/src/views/admin/modpack-installer/_vanilla_form.ejs b/services/arbiter-3.0/src/views/admin/modpack-installer/_vanilla_form.ejs index 830838f..2323492 100644 --- a/services/arbiter-3.0/src/views/admin/modpack-installer/_vanilla_form.ejs +++ b/services/arbiter-3.0/src/views/admin/modpack-installer/_vanilla_form.ejs @@ -21,17 +21,12 @@
- +
@@ -195,6 +190,26 @@
diff --git a/services/arbiter-3.0/src/views/admin/modpack-installer/index.ejs b/services/arbiter-3.0/src/views/admin/modpack-installer/index.ejs index 39c35dd..febb9f4 100644 --- a/services/arbiter-3.0/src/views/admin/modpack-installer/index.ejs +++ b/services/arbiter-3.0/src/views/admin/modpack-installer/index.ejs @@ -52,11 +52,7 @@ onkeydown="if(event.key==='Enter')doSearch()"> @@ -69,6 +65,19 @@