/** * Minecraft Server Status - Sidebar Widget * Source : API Crafty Controller (proxy PHP crafty-stats.php) */ (function () { 'use strict'; const API = '/data/api/crafty-stats.php'; const MODS_API = '/data/api/mods-mc.php'; const INTERVAL = 30000; // 30s const SERVERS = [ { key: 'mc1', cardId: 'mc-card-0' }, { key: 'mc2', cardId: 'mc-card-1' }, ]; let timer = null; let modsData = null; // cache des mods, chargé une seule fois // ── Mods ──────────────────────────────────────────────────────────────── const modsStyle = document.createElement('style'); modsStyle.textContent = ` .mc-mods-section { margin-top: 0.5rem; border-top: 1px solid rgba(98, 114, 164, 0.3); padding-top: 0.5rem; } .mc-mods-toggle { display: flex; align-items: center; justify-content: space-between; cursor: pointer; padding: 0.25rem 0; user-select: none; } .mc-mods-toggle-label { font-size: 0.72rem; font-weight: 700; color: #6272a4; text-transform: uppercase; letter-spacing: 0.07em; } .mc-mods-toggle-meta { display: flex; align-items: center; gap: 0.4rem; font-size: 0.68rem; color: #6272a4; } .mc-mods-arrow { font-size: 0.6rem; color: #6272a4; transition: transform 0.2s; display: inline-block; } .mc-mods-arrow.open { transform: rotate(90deg); } .mc-mods-pills { display: none; flex-wrap: wrap; gap: 0.3rem; padding-top: 0.45rem; } .mc-mods-pills.open { display: flex; } .mc-mod-pill { background: rgba(98, 114, 164, 0.25); color: #f8f8f2; font-size: 0.65rem; padding: 2px 7px; border-radius: 20px; border: 1px solid rgba(98, 114, 164, 0.35); white-space: nowrap; cursor: default; } .mc-mod-pill:hover { background: rgba(189, 147, 249, 0.2); border-color: rgba(189, 147, 249, 0.5); color: #bd93f9; } .mc-mods-error { font-size: 0.7rem; color: #ff5555; padding-top: 0.3rem; font-style: italic; } `; document.head.appendChild(modsStyle); async function fetchMods() { try { const res = await fetch(MODS_API, { cache: 'no-cache' }); if (!res.ok) throw new Error(`HTTP ${res.status}`); modsData = await res.json(); } catch (e) { console.warn('[mods-mc] Erreur fetch :', e); } } function appendMods(card, srv) { if (!modsData) return; // trouve l'entrée correspondant au nom du serveur Crafty const entry = modsData.find(m => m.label === srv?.name) ?? modsData[SERVERS.findIndex(s => s.cardId === card.id)]; if (!entry) return; const section = document.createElement('div'); section.className = 'mc-mods-section'; if (entry.error) { const err = document.createElement('div'); err.className = 'mc-mods-error'; err.textContent = entry.error; section.appendChild(err); card.appendChild(section); return; } const count = entry.mods.length; const toggle = document.createElement('div'); toggle.className = 'mc-mods-toggle'; toggle.innerHTML = ` Mods ${count} mod${count !== 1 ? 's' : ''} `; const pills = document.createElement('div'); pills.className = 'mc-mods-pills'; entry.mods.forEach(mod => { const pill = document.createElement('span'); pill.className = 'mc-mod-pill'; pill.title = mod.file; pill.textContent = mod.name; pills.appendChild(pill); }); toggle.addEventListener('click', () => { pills.classList.toggle('open'); toggle.querySelector('.mc-mods-arrow').classList.toggle('open'); }); section.appendChild(toggle); section.appendChild(pills); card.appendChild(section); } // ── Status ─────────────────────────────────────────────────────────────── async function fetchAll() { let data; try { const res = await fetch(API, { cache: 'no-cache' }); if (!res.ok) throw new Error(`HTTP ${res.status}`); data = await res.json(); } catch (e) { console.error('crafty-stats error:', e); return; } if (!data.success) return; SERVERS.forEach(({ key, cardId }) => { const card = document.getElementById(cardId); if (!card) return; renderCard(card, data.servers?.[key]); }); updateStatCards(data.servers); } function renderCard(card, srv) { if (!srv) { card.innerHTML = `
Serveur inconnu ● Hors ligne
`; appendMods(card, srv); return; } const online = srv.running; const players = online ? `${srv.players} / ${srv.max_players}` : '— / —'; let ramHtml = ''; if (online && srv.ram_used !== null) { const usedStr = ramFmt(srv.ram_used); const allocStr = srv.ram_alloc ? ramFmt(srv.ram_alloc) : '?'; const pct = srv.ram_alloc > 0 ? Math.round((srv.ram_used / srv.ram_alloc) * 100) : 0; const color = pct > 90 ? '#ff5555' : pct > 70 ? '#ffb86c' : '#50fa7b'; ramHtml = ` ${usedStr} / ${allocStr} `; } const cpuHtml = (online && srv.cpu !== null) ? `${srv.cpu}% CPU` : ''; card.innerHTML = `
${esc(srv.name)} ${online ? '● En ligne' : '● Hors ligne'}
${esc('nix.roulaise.net')}:${srv.port}
${players} joueurs ${ramHtml} ${cpuHtml}
`; appendMods(card, srv); } function updateStatCards(servers) { ['mc1', 'mc2'].forEach((key, i) => { const n = i + 1; const el = document.getElementById(`mc${n}-ram`); const sub = document.getElementById(`mc${n}-ram-subtitle`); if (!el) return; const srv = servers?.[key]; if (!srv || !srv.running) { el.textContent = 'Éteint'; if (sub) sub.textContent = '—'; el.style.color = '#6272a4'; return; } if (srv.ram_used !== null) { const pct = srv.ram_alloc > 0 ? Math.round((srv.ram_used / srv.ram_alloc) * 100) : 0; el.textContent = ramFmt(srv.ram_used); if (sub) sub.textContent = `alloué: ${srv.ram_alloc ? ramFmt(srv.ram_alloc) : '?'} (${pct}%)`; el.style.color = pct > 90 ? '#ff5555' : pct > 70 ? '#ffb86c' : '#50fa7b'; } else { el.textContent = '...'; if (sub) sub.textContent = '—'; } }); } function ramFmt(mio) { return mio >= 1024 ? (mio / 1024).toFixed(1) + ' Gio' : mio + ' Mio'; } function esc(s) { return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function start() { timer = setInterval(fetchAll, INTERVAL); } function stop() { clearInterval(timer); timer = null; } async function init() { if (!document.getElementById('mc-card-0')) return; await fetchMods(); // charge les mods une seule fois au démarrage fetchAll(); start(); window.addEventListener('beforeunload', stop); document.addEventListener('visibilitychange', () => document.hidden ? stop() : (fetchAll(), start()) ); } document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init(); })();