WHAT WAS DONE: - Built browser dashboard (dashboard.html) showing installed vs latest version for all Pterodactyl game servers - Built PHP proxy (proxy.php + config.php) for Billing VPS deployment - Created isolated Nginx server block (version-proxy.conf) - Created full deployment guide (DEPLOYMENT-GUIDE.md) ARCHITECTURE: - PHP proxy at /var/www/version-proxy on Billing VPS (38.68.14.188) - Isolated from Paymenter/Laravel routing — separate directory + port - API keys (Pterodactyl ptlc_, CurseForge) live server-side only - FTB packs: fully automatic via .manifest.json + FTB public API - CurseForge packs: reads manifest.json, needs CF Project ID + API key - config.php blocked from direct web access via Nginx PENDING AT DEPLOYMENT: - Verify port 8080 is free (ss -tlnp) before enabling Nginx block - Fill real API keys into config.php on server - Enter CurseForge Project IDs for CF packs (saved in localStorage) COLLABORATION: - PHP proxy architecture designed by Gemini (consultation session 2026-03-29) - Dashboard HTML and detection logic by Chronicler #47 - Gemini identified Laravel routing conflict and content-type gotcha WHY: - Interim solution before full Blueprint extension (post-launch) - Hands-off modpack update monitoring for staff - Zero manual checking required after initial CF Project ID setup Signed-off-by: claude@firefrostgaming.com
547 lines
23 KiB
HTML
547 lines
23 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Firefrost — Modpack Version Checker</title>
|
|
<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;500;600;700&family=Share+Tech+Mono&display=swap');
|
|
|
|
:root {
|
|
--fire: #FF6B35;
|
|
--frost: #4ECDC4;
|
|
--gold: #FFD700;
|
|
--dark: #0F0F1E;
|
|
--dark2: #151528;
|
|
--dark3: #1a1a35;
|
|
--border: rgba(78,205,196,0.15);
|
|
--up-to-date: #4ade80;
|
|
--outdated: #f87171;
|
|
--unknown: #94a3b8;
|
|
--checking: #60a5fa;
|
|
}
|
|
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
body {
|
|
background: var(--dark);
|
|
color: #e2e8f0;
|
|
font-family: 'Rajdhani', sans-serif;
|
|
min-height: 100vh;
|
|
padding: 24px;
|
|
}
|
|
|
|
body::before {
|
|
content: '';
|
|
position: fixed;
|
|
inset: 0;
|
|
background-image:
|
|
linear-gradient(rgba(78,205,196,0.03) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(78,205,196,0.03) 1px, transparent 1px);
|
|
background-size: 40px 40px;
|
|
pointer-events: none;
|
|
}
|
|
|
|
header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 32px;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
}
|
|
|
|
.logo { display: flex; align-items: center; gap: 12px; }
|
|
.logo-text { font-size: 28px; font-weight: 700; letter-spacing: 2px; text-transform: uppercase; }
|
|
.logo-fire { color: var(--fire); }
|
|
.logo-frost { color: var(--frost); }
|
|
.subtitle { font-size: 13px; color: #64748b; letter-spacing: 3px; text-transform: uppercase; font-family: 'Share Tech Mono', monospace; }
|
|
|
|
.header-right { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
|
|
.last-checked {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 12px; color: #475569;
|
|
padding: 6px 12px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
background: var(--dark2);
|
|
}
|
|
|
|
button {
|
|
font-family: 'Rajdhani', sans-serif;
|
|
font-size: 14px; font-weight: 600;
|
|
letter-spacing: 1px; text-transform: uppercase;
|
|
padding: 8px 20px; border: none; border-radius: 4px;
|
|
cursor: pointer; transition: all 0.2s;
|
|
}
|
|
|
|
.btn-refresh { background: var(--frost); color: var(--dark); }
|
|
.btn-refresh:hover { background: #5ee8de; transform: translateY(-1px); }
|
|
.btn-refresh:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
|
|
|
|
.summary { display: flex; gap: 16px; margin-bottom: 28px; flex-wrap: wrap; }
|
|
.summary-card {
|
|
flex: 1; min-width: 140px;
|
|
background: var(--dark2); border: 1px solid var(--border);
|
|
border-radius: 8px; padding: 16px 20px;
|
|
display: flex; align-items: center; gap: 12px;
|
|
}
|
|
.summary-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
|
|
.summary-label { font-size: 12px; color: #64748b; text-transform: uppercase; letter-spacing: 1px; }
|
|
.summary-count { font-size: 28px; font-weight: 700; line-height: 1; }
|
|
|
|
.servers-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
|
gap: 16px;
|
|
}
|
|
|
|
.server-card {
|
|
background: var(--dark2); border: 1px solid var(--border);
|
|
border-radius: 8px; overflow: hidden;
|
|
transition: border-color 0.2s, transform 0.2s;
|
|
}
|
|
.server-card:hover { border-color: rgba(78,205,196,0.35); transform: translateY(-2px); }
|
|
.server-card.outdated { border-color: rgba(248,113,113,0.3); }
|
|
.server-card.up-to-date { border-color: rgba(74,222,128,0.2); }
|
|
|
|
.card-header {
|
|
padding: 14px 18px; background: var(--dark3);
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
|
}
|
|
.server-name { font-size: 15px; font-weight: 600; letter-spacing: 0.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.node-badge { font-family: 'Share Tech Mono', monospace; font-size: 10px; padding: 2px 8px; border-radius: 3px; white-space: nowrap; flex-shrink: 0; }
|
|
.node-nc { background: rgba(255,107,53,0.15); color: var(--fire); border: 1px solid rgba(255,107,53,0.3); }
|
|
.node-tx { background: rgba(78,205,196,0.12); color: var(--frost); border: 1px solid rgba(78,205,196,0.25); }
|
|
|
|
.card-body { padding: 16px 18px; display: flex; flex-direction: column; gap: 10px; }
|
|
.platform-row { display: flex; align-items: center; gap: 8px; }
|
|
.platform-badge { font-family: 'Share Tech Mono', monospace; font-size: 10px; padding: 2px 8px; border-radius: 3px; font-weight: 600; }
|
|
.platform-ftb { background: rgba(255,107,53,0.15); color: #fb923c; border: 1px solid rgba(255,107,53,0.3); }
|
|
.platform-curseforge { background: rgba(249,115,22,0.12); color: #f97316; border: 1px solid rgba(249,115,22,0.25); }
|
|
.platform-modrinth { background: rgba(34,197,94,0.12); color: #4ade80; border: 1px solid rgba(34,197,94,0.25); }
|
|
.platform-unknown { background: rgba(148,163,184,0.1); color: #94a3b8; border: 1px solid rgba(148,163,184,0.2); }
|
|
|
|
.pack-name { font-size: 13px; color: #94a3b8; font-family: 'Share Tech Mono', monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
|
|
.version-comparison { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
.version-box { background: var(--dark3); border-radius: 6px; padding: 10px 12px; border: 1px solid rgba(255,255,255,0.05); }
|
|
.version-label { font-size: 10px; color: #475569; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px; }
|
|
.version-value { font-family: 'Share Tech Mono', monospace; font-size: 16px; font-weight: 600; }
|
|
|
|
.v-installed { color: #e2e8f0; }
|
|
.v-latest-current { color: var(--up-to-date); }
|
|
.v-latest-newer { color: var(--outdated); }
|
|
.v-unknown { color: #475569; }
|
|
|
|
.status-row { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 6px; font-size: 13px; font-weight: 600; letter-spacing: 0.5px; }
|
|
.status-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
.status-up-to-date { background: rgba(74,222,128,0.08); border: 1px solid rgba(74,222,128,0.2); color: var(--up-to-date); }
|
|
.status-outdated { background: rgba(248,113,113,0.08); border: 1px solid rgba(248,113,113,0.2); color: var(--outdated); }
|
|
.status-checking { background: rgba(96,165,250,0.08); border: 1px solid rgba(96,165,250,0.2); color: var(--checking); }
|
|
.status-unknown { background: rgba(148,163,184,0.08); border: 1px solid rgba(148,163,184,0.15); color: var(--unknown); }
|
|
|
|
.mc-version { font-family: 'Share Tech Mono', monospace; font-size: 11px; color: #475569; }
|
|
|
|
.curseforge-id-row { display: flex; align-items: center; gap: 8px; }
|
|
.curseforge-id-row label { font-size: 11px; color: #475569; text-transform: uppercase; letter-spacing: 1px; white-space: nowrap; flex-shrink: 0; }
|
|
.curseforge-id-row input {
|
|
background: var(--dark3); border: 1px solid rgba(78,205,196,0.15);
|
|
border-radius: 3px; padding: 4px 8px; color: #94a3b8;
|
|
font-family: 'Share Tech Mono', monospace; font-size: 12px;
|
|
outline: none; width: 100%; transition: border-color 0.2s;
|
|
}
|
|
.curseforge-id-row input:focus { border-color: var(--frost); color: #e2e8f0; }
|
|
|
|
.btn-check-single {
|
|
background: transparent; border: 1px solid rgba(78,205,196,0.3);
|
|
color: var(--frost); padding: 4px 12px; font-size: 11px;
|
|
border-radius: 3px; flex-shrink: 0;
|
|
}
|
|
.btn-check-single:hover { background: rgba(78,205,196,0.1); }
|
|
|
|
.spinner {
|
|
display: inline-block; width: 12px; height: 12px;
|
|
border: 2px solid rgba(96,165,250,0.3); border-top-color: var(--checking);
|
|
border-radius: 50%; animation: spin 0.8s linear infinite; flex-shrink: 0;
|
|
}
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
|
|
.error-state {
|
|
font-family: 'Share Tech Mono', monospace; font-size: 11px; color: #f87171;
|
|
padding: 6px 10px; background: rgba(248,113,113,0.06);
|
|
border-radius: 4px; border: 1px solid rgba(248,113,113,0.15);
|
|
}
|
|
|
|
footer { margin-top: 40px; text-align: center; font-size: 11px; color: #1e293b; font-family: 'Share Tech Mono', monospace; letter-spacing: 2px; }
|
|
|
|
@media (max-width: 600px) {
|
|
.servers-grid { grid-template-columns: 1fr; }
|
|
.version-comparison { grid-template-columns: 1fr 1fr; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<div class="logo">
|
|
<div>
|
|
<div class="logo-text"><span class="logo-fire">FIRE</span><span class="logo-frost">FROST</span></div>
|
|
<div class="subtitle">Modpack Version Monitor</div>
|
|
</div>
|
|
</div>
|
|
<div class="header-right">
|
|
<div class="last-checked" id="lastChecked">Never checked</div>
|
|
<button class="btn-refresh" id="refreshBtn" onclick="checkAll()">↻ Check All</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="summary">
|
|
<div class="summary-card">
|
|
<div class="summary-dot" style="background:var(--up-to-date)"></div>
|
|
<div><div class="summary-label">Up to Date</div><div class="summary-count" id="countCurrent" style="color:var(--up-to-date)">—</div></div>
|
|
</div>
|
|
<div class="summary-card">
|
|
<div class="summary-dot" style="background:var(--outdated)"></div>
|
|
<div><div class="summary-label">Outdated</div><div class="summary-count" id="countOutdated" style="color:var(--outdated)">—</div></div>
|
|
</div>
|
|
<div class="summary-card">
|
|
<div class="summary-dot" style="background:var(--unknown)"></div>
|
|
<div><div class="summary-label">Unknown</div><div class="summary-count" id="countUnknown" style="color:var(--unknown)">—</div></div>
|
|
</div>
|
|
<div class="summary-card">
|
|
<div class="summary-dot" style="background:var(--frost)"></div>
|
|
<div><div class="summary-label">Total Servers</div><div class="summary-count" id="countTotal" style="color:var(--frost)">—</div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="servers-grid" id="serversGrid">
|
|
<div style="color:#475569;font-family:'Share Tech Mono',monospace;font-size:13px;grid-column:1/-1;padding:40px;text-align:center;">
|
|
Click "Check All" to load servers and check versions.
|
|
</div>
|
|
</div>
|
|
|
|
<footer>FIRE + FROST + FOUNDATION | FOR CHILDREN NOT YET BORN 💙</footer>
|
|
|
|
<script>
|
|
// ─── CONFIGURATION ───────────────────────────────────────────────
|
|
// Point this to your PHP proxy. Verify port with `ss -tlnp` before deploying.
|
|
// Default: http://38.68.14.188:8080/proxy.php
|
|
// If 8080 is taken, change to 8081 or 8090 here and in your Nginx config.
|
|
const PROXY_URL = 'http://38.68.14.188:8080/proxy.php';
|
|
|
|
// ─── STATE ───────────────────────────────────────────────────────
|
|
const serverStates = {};
|
|
const packMappings = JSON.parse(localStorage.getItem('ffg_pack_mappings') || '{}');
|
|
|
|
function saveMappings() {
|
|
localStorage.setItem('ffg_pack_mappings', JSON.stringify(packMappings));
|
|
}
|
|
|
|
// ─── PROXY API CALLS ─────────────────────────────────────────────
|
|
// All requests go through the PHP proxy — no API keys in the browser.
|
|
|
|
async function apiCall(action, params = '') {
|
|
const resp = await fetch(`${PROXY_URL}?action=${action}${params}`);
|
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
return action === 'read' ? resp.text() : resp.json();
|
|
}
|
|
|
|
async function getFileContents(identifier, path) {
|
|
try {
|
|
return await apiCall('read', `&server=${identifier}&file=${encodeURIComponent(path)}`);
|
|
} catch { return null; }
|
|
}
|
|
|
|
async function listFiles(identifier) {
|
|
try {
|
|
const data = await apiCall('files', `&server=${identifier}`);
|
|
return data.data.map(f => f.attributes.name);
|
|
} catch { return []; }
|
|
}
|
|
|
|
// ─── VERSION DETECTION ───────────────────────────────────────────
|
|
|
|
async function detectInstalledVersion(identifier) {
|
|
const files = await listFiles(identifier);
|
|
|
|
// FTB (.manifest.json)
|
|
if (files.includes('.manifest.json')) {
|
|
const content = await getFileContents(identifier, '.manifest.json');
|
|
if (content) {
|
|
try {
|
|
const data = JSON.parse(content);
|
|
return { platform: 'ftb', packName: data.name, version: data.versionName, ftbPackId: data.id, ftbVersionId: data.versionId };
|
|
} catch {}
|
|
}
|
|
}
|
|
|
|
// Modrinth (modrinth.index.json)
|
|
if (files.includes('modrinth.index.json')) {
|
|
const content = await getFileContents(identifier, 'modrinth.index.json');
|
|
if (content) {
|
|
try {
|
|
const data = JSON.parse(content);
|
|
return { platform: 'modrinth', packName: data.name, version: data.versionId || 'unknown' };
|
|
} catch {}
|
|
}
|
|
}
|
|
|
|
// CurseForge (manifest.json)
|
|
if (files.includes('manifest.json')) {
|
|
const content = await getFileContents(identifier, 'manifest.json');
|
|
if (content) {
|
|
try {
|
|
const data = JSON.parse(content);
|
|
if (data.minecraft || data.name) {
|
|
return { platform: 'curseforge', packName: data.name || 'Unknown', version: data.version || 'unknown' };
|
|
}
|
|
} catch {}
|
|
}
|
|
}
|
|
|
|
return { platform: 'unknown', packName: null, version: null };
|
|
}
|
|
|
|
// ─── LATEST VERSION LOOKUPS ──────────────────────────────────────
|
|
|
|
async function getLatestFtbVersion(packId) {
|
|
try {
|
|
// FTB API is public — no proxy needed, no CORS issue
|
|
const resp = await fetch(`https://api.modpacks.ch/public/modpack/${packId}`);
|
|
if (!resp.ok) return null;
|
|
const data = await resp.json();
|
|
const versions = (data.versions || []).filter(v => v.type === 'Release' || v.type === 'release');
|
|
if (versions.length === 0) return data.versions?.[0]?.name || null;
|
|
return versions[versions.length - 1].name;
|
|
} catch { return null; }
|
|
}
|
|
|
|
async function getLatestCurseForgeVersion(projectId) {
|
|
// Routes through proxy — CF API key stays server-side
|
|
if (!projectId) return null;
|
|
try {
|
|
const data = await apiCall('curseforge', `&project=${projectId}`);
|
|
const file = data.data?.[0];
|
|
if (!file) return null;
|
|
return file.displayName || file.fileName;
|
|
} catch { return null; }
|
|
}
|
|
|
|
// ─── VERSION COMPARISON ──────────────────────────────────────────
|
|
|
|
function compareVersions(installed, latest) {
|
|
if (!installed || !latest) return 'unknown';
|
|
const a = installed.trim().toLowerCase().replace(/^v/, '');
|
|
const b = latest.trim().toLowerCase().replace(/^v/, '');
|
|
if (a === b) return 'current';
|
|
const aParts = a.split('.').map(x => parseInt(x) || 0);
|
|
const bParts = b.split('.').map(x => parseInt(x) || 0);
|
|
const len = Math.max(aParts.length, bParts.length);
|
|
for (let i = 0; i < len; i++) {
|
|
const av = aParts[i] || 0;
|
|
const bv = bParts[i] || 0;
|
|
if (bv > av) return 'outdated';
|
|
if (av > bv) return 'current';
|
|
}
|
|
return 'current';
|
|
}
|
|
|
|
// ─── CARD RENDERING ──────────────────────────────────────────────
|
|
|
|
function renderCard(server, state) {
|
|
const identifier = server.identifier;
|
|
const name = server.name;
|
|
const node = name.includes('- NC') ? 'NC' : name.includes('- TX') ? 'TX' : '?';
|
|
const mapping = packMappings[identifier] || {};
|
|
const platformLabels = { ftb: 'FTB', curseforge: 'CurseForge', modrinth: 'Modrinth', unknown: 'Unknown' };
|
|
const platformClass = { ftb: 'platform-ftb', curseforge: 'platform-curseforge', modrinth: 'platform-modrinth', unknown: 'platform-unknown' };
|
|
|
|
let statusHtml = '', installedVersionHtml = '', latestVersionHtml = '', cardClass = '', extraControls = '';
|
|
|
|
if (state.loading) {
|
|
statusHtml = `<div class="status-row status-checking"><div class="spinner"></div> Checking...</div>`;
|
|
installedVersionHtml = `<span class="v-unknown">—</span>`;
|
|
latestVersionHtml = `<span class="v-unknown">—</span>`;
|
|
} else if (state.error) {
|
|
statusHtml = `<div class="error-state">Error: ${state.error}</div>`;
|
|
installedVersionHtml = `<span class="v-unknown">—</span>`;
|
|
latestVersionHtml = `<span class="v-unknown">—</span>`;
|
|
} else {
|
|
const installed = state.version;
|
|
const latest = state.latestVersion;
|
|
const platform = state.platform || 'unknown';
|
|
const status = state.status || 'unknown';
|
|
|
|
installedVersionHtml = installed
|
|
? `<span class="v-installed">${installed}</span>`
|
|
: `<span class="v-unknown">Not detected</span>`;
|
|
|
|
latestVersionHtml = latest
|
|
? `<span class="${status === 'outdated' ? 'v-latest-newer' : 'v-latest-current'}">${latest}</span>`
|
|
: `<span class="v-unknown">—</span>`;
|
|
|
|
if (status === 'current') {
|
|
statusHtml = `<div class="status-row status-up-to-date"><div class="status-dot" style="background:var(--up-to-date)"></div> Up to Date</div>`;
|
|
cardClass = 'up-to-date';
|
|
} else if (status === 'outdated') {
|
|
statusHtml = `<div class="status-row status-outdated"><div class="status-dot" style="background:var(--outdated)"></div> Update Available</div>`;
|
|
cardClass = 'outdated';
|
|
} else {
|
|
statusHtml = `<div class="status-row status-unknown"><div class="status-dot" style="background:var(--unknown)"></div> Unknown</div>`;
|
|
}
|
|
|
|
if (platform === 'curseforge' || platform === 'unknown') {
|
|
const cfId = mapping.cfId || '';
|
|
extraControls = `
|
|
<div class="curseforge-id-row">
|
|
<label>CF Project ID</label>
|
|
<input type="text" placeholder="e.g. 389615" value="${cfId}"
|
|
onchange="updateMapping('${identifier}', 'cfId', this.value)" />
|
|
<button class="btn-check-single" onclick="checkSingle('${identifier}')">Check</button>
|
|
</div>`;
|
|
}
|
|
}
|
|
|
|
const packNameDisplay = state.packName
|
|
? `<span class="pack-name" title="${state.packName}">${state.packName}</span>`
|
|
: '';
|
|
|
|
return `
|
|
<div class="server-card ${cardClass}" id="card-${identifier}">
|
|
<div class="card-header">
|
|
<div class="server-name" title="${name}">${name.replace(' - TX','').replace(' - NC','')}</div>
|
|
<span class="node-badge node-${node.toLowerCase()}">${node}</span>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="platform-row">
|
|
<span class="platform-badge ${platformClass[state.platform || 'unknown']}">${platformLabels[state.platform || 'unknown']}</span>
|
|
${packNameDisplay}
|
|
</div>
|
|
<div class="version-comparison">
|
|
<div class="version-box"><div class="version-label">Installed</div><div class="version-value">${installedVersionHtml}</div></div>
|
|
<div class="version-box"><div class="version-label">Latest</div><div class="version-value">${latestVersionHtml}</div></div>
|
|
</div>
|
|
${statusHtml}
|
|
${extraControls}
|
|
<div class="mc-version">MC ${server.mcVersion || '?'}</div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
// ─── STATE MANAGEMENT ────────────────────────────────────────────
|
|
|
|
function updateMapping(identifier, key, value) {
|
|
if (!packMappings[identifier]) packMappings[identifier] = {};
|
|
packMappings[identifier][key] = value;
|
|
saveMappings();
|
|
}
|
|
|
|
function updateCardDisplay(identifier) {
|
|
const card = document.getElementById(`card-${identifier}`);
|
|
if (!card) return;
|
|
card.outerHTML = renderCard(serverStates[identifier]._serverData, serverStates[identifier]);
|
|
}
|
|
|
|
function updateSummary() {
|
|
const states = Object.values(serverStates);
|
|
document.getElementById('countCurrent').textContent = states.filter(s => s.status === 'current').length;
|
|
document.getElementById('countOutdated').textContent = states.filter(s => s.status === 'outdated').length;
|
|
document.getElementById('countUnknown').textContent = states.filter(s => s.status === 'unknown' || s.loading).length;
|
|
document.getElementById('countTotal').textContent = states.length;
|
|
}
|
|
|
|
// ─── CORE CHECK LOGIC ────────────────────────────────────────────
|
|
|
|
async function checkServerVersion(identifier) {
|
|
const state = serverStates[identifier];
|
|
const detected = await detectInstalledVersion(identifier);
|
|
|
|
state.platform = detected.platform;
|
|
state.packName = detected.packName;
|
|
state.version = detected.version;
|
|
|
|
const mapping = packMappings[identifier] || {};
|
|
|
|
if (detected.platform === 'ftb' && detected.ftbPackId) {
|
|
state.latestVersion = await getLatestFtbVersion(detected.ftbPackId);
|
|
} else if (detected.platform === 'curseforge' && mapping.cfId) {
|
|
state.latestVersion = await getLatestCurseForgeVersion(mapping.cfId);
|
|
} else if (mapping.cfId) {
|
|
state.platform = 'curseforge';
|
|
state.latestVersion = await getLatestCurseForgeVersion(mapping.cfId);
|
|
} else {
|
|
state.latestVersion = null;
|
|
}
|
|
|
|
state.status = compareVersions(state.version, state.latestVersion);
|
|
state.loading = false;
|
|
}
|
|
|
|
async function checkSingle(identifier) {
|
|
if (!serverStates[identifier]) return;
|
|
serverStates[identifier].loading = true;
|
|
serverStates[identifier].error = null;
|
|
updateCardDisplay(identifier);
|
|
try {
|
|
await checkServerVersion(identifier);
|
|
} catch(e) {
|
|
serverStates[identifier].loading = false;
|
|
serverStates[identifier].error = e.message;
|
|
}
|
|
updateCardDisplay(identifier);
|
|
updateSummary();
|
|
}
|
|
|
|
async function checkAll() {
|
|
const btn = document.getElementById('refreshBtn');
|
|
btn.disabled = true;
|
|
btn.textContent = 'Checking...';
|
|
|
|
const grid = document.getElementById('serversGrid');
|
|
grid.innerHTML = '<div style="color:#475569;font-family:\'Share Tech Mono\',monospace;font-size:13px;grid-column:1/-1;padding:40px;text-align:center;">Loading servers...</div>';
|
|
|
|
try {
|
|
const allServersResp = await apiCall('servers');
|
|
const servers = allServersResp.data.map(s => ({
|
|
identifier: s.attributes.identifier,
|
|
name: s.attributes.name,
|
|
mcVersion: null,
|
|
}));
|
|
|
|
for (const server of servers) {
|
|
serverStates[server.identifier] = {
|
|
loading: true, error: null, platform: 'unknown',
|
|
packName: null, version: null, latestVersion: null, status: 'unknown',
|
|
_serverData: server
|
|
};
|
|
}
|
|
|
|
grid.innerHTML = servers.map(s => renderCard(s, serverStates[s.identifier])).join('');
|
|
document.getElementById('countTotal').textContent = servers.length;
|
|
|
|
await Promise.all(servers.map(async (server) => {
|
|
try {
|
|
await checkServerVersion(server.identifier);
|
|
} catch(e) {
|
|
serverStates[server.identifier].loading = false;
|
|
serverStates[server.identifier].error = e.message;
|
|
}
|
|
updateCardDisplay(server.identifier);
|
|
updateSummary();
|
|
}));
|
|
|
|
document.getElementById('lastChecked').textContent = 'Last: ' + new Date().toLocaleTimeString();
|
|
} catch(e) {
|
|
grid.innerHTML = `<div style="color:#f87171;font-family:'Share Tech Mono',monospace;font-size:13px;grid-column:1/-1;padding:40px;text-align:center;">Error: ${e.message}<br><br><small style="color:#475569">Is the proxy running? Check http://38.68.14.188:[PORT]/proxy.php</small></div>`;
|
|
}
|
|
|
|
btn.disabled = false;
|
|
btn.textContent = '↻ Check All';
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|