Files
Site-Web/data/js/minecraft-status.js
T
2026-05-16 11:10:19 +02:00

283 lines
9.9 KiB
JavaScript
Executable File

/**
* 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 = `
<span class="mc-mods-toggle-label">Mods</span>
<span class="mc-mods-toggle-meta">
<span>${count} mod${count !== 1 ? 's' : ''}</span>
<span class="mc-mods-arrow">&#9656;</span>
</span>
`;
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 = `
<div class="mc-header">
<span class="mc-name">Serveur inconnu</span>
<span class="mc-badge mc-offline">&#9679; Hors ligne</span>
</div>`;
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 = `
<span class="mc-ram" style="color:${color}">
<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor" style="color:#bd93f9;flex-shrink:0">
<path d="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z"/>
</svg>
${usedStr}<span style="color:#6272a4;font-weight:400"> / ${allocStr}</span>
</span>`;
}
const cpuHtml = (online && srv.cpu !== null)
? `<span class="mc-version">${srv.cpu}% CPU</span>`
: '';
card.innerHTML = `
<div class="mc-header">
<span class="mc-name">${esc(srv.name)}</span>
<span class="mc-badge ${online ? 'mc-online' : 'mc-offline'}">${online ? '&#9679; En ligne' : '&#9679; Hors ligne'}</span>
</div>
<div class="mc-address">${esc('nix.roulaise.net')}:${srv.port}</div>
<div class="mc-info">
<span class="mc-players">
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
<path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/>
</svg>
${players} joueurs
</span>
${ramHtml}
${cpuHtml}
</div>`;
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
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();
})();