694 lines
23 KiB
HTML
Executable File
694 lines
23 KiB
HTML
Executable File
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Nixiews — Serveurs Minecraft</title>
|
|
<link rel="stylesheet" href="../data/style.loader.css">
|
|
<link rel="icon" type="image/webp" href="../data/media/icon_circle.webp">
|
|
<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=Comfortaa:wght@400;700&family=JetBrains+Mono:wght@400;600&display=swap');
|
|
|
|
/* ===== Reset & Base ===== */
|
|
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
:root {
|
|
--bg: #282a36;
|
|
--bg2: #1e2029;
|
|
--surface: rgba(68,71,90,0.95);
|
|
--surface2: rgba(40,42,54,0.8);
|
|
--border: rgba(98,114,164,0.25);
|
|
--purple: #bd93f9;
|
|
--pink: #ff79c6;
|
|
--green: #50fa7b;
|
|
--cyan: #8be9fd;
|
|
--yellow: #f1fa8c;
|
|
--orange: #ffb86c;
|
|
--red: #ff5555;
|
|
--comment: #6272a4;
|
|
--fg: #f8f8f2;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Comfortaa', cursive;
|
|
background: var(--bg2);
|
|
color: var(--fg);
|
|
min-height: 100vh;
|
|
padding: 2rem;
|
|
}
|
|
|
|
/* ===== Background ===== */
|
|
body::before {
|
|
content: '';
|
|
position: fixed;
|
|
inset: 0;
|
|
background:
|
|
radial-gradient(ellipse 60% 40% at 20% 20%, rgba(189,147,249,0.06) 0%, transparent 70%),
|
|
radial-gradient(ellipse 50% 60% at 80% 80%, rgba(80,250,123,0.04) 0%, transparent 70%),
|
|
var(--bg2);
|
|
z-index: -1;
|
|
}
|
|
|
|
/* ===== Layout ===== */
|
|
.page-wrap {
|
|
max-width: 1300px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* ===== Header ===== */
|
|
.page-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 2rem;
|
|
padding-bottom: 1rem;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.page-header h1 {
|
|
font-size: 2rem;
|
|
color: var(--purple);
|
|
}
|
|
|
|
.back-link {
|
|
font-size: 0.85rem;
|
|
color: var(--comment);
|
|
text-decoration: none;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
transition: color 0.2s;
|
|
}
|
|
.back-link:hover { color: var(--purple); }
|
|
|
|
/* ===== Server tabs ===== */
|
|
.server-tabs {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.tab-btn {
|
|
font-family: inherit;
|
|
font-size: 0.9rem;
|
|
font-weight: 700;
|
|
padding: 0.5rem 1.2rem;
|
|
border-radius: 8px;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface2);
|
|
color: var(--comment);
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
.tab-btn.active {
|
|
background: rgba(189,147,249,0.15);
|
|
border-color: var(--purple);
|
|
color: var(--purple);
|
|
}
|
|
.tab-btn:hover:not(.active) { color: var(--fg); border-color: var(--comment); }
|
|
|
|
/* ===== Main grid ===== */
|
|
.mc-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
grid-template-rows: auto auto;
|
|
gap: 1.2rem;
|
|
}
|
|
|
|
.mc-grid .log-panel { grid-column: 1 / -1; }
|
|
|
|
/* ===== Panels ===== */
|
|
.panel {
|
|
background: var(--surface);
|
|
border-radius: 14px;
|
|
border: 1px solid var(--border);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.panel-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0.85rem 1.1rem;
|
|
background: rgba(40,42,54,0.5);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.panel-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.45rem;
|
|
font-size: 0.78rem;
|
|
font-weight: 700;
|
|
color: var(--purple);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.1em;
|
|
}
|
|
|
|
.panel-count {
|
|
font-size: 0.7rem;
|
|
color: var(--comment);
|
|
font-family: 'JetBrains Mono', monospace;
|
|
}
|
|
|
|
.panel-body {
|
|
padding: 0.8rem;
|
|
}
|
|
|
|
/* ===== Log feed ===== */
|
|
.log-feed {
|
|
height: 340px;
|
|
overflow-y: auto;
|
|
display: flex;
|
|
flex-direction: column-reverse;
|
|
gap: 0.3rem;
|
|
padding: 0.8rem;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 0.78rem;
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--comment) transparent;
|
|
}
|
|
|
|
.log-entry {
|
|
display: grid;
|
|
grid-template-columns: auto auto 1fr;
|
|
gap: 0.5rem;
|
|
align-items: baseline;
|
|
padding: 0.3rem 0.5rem;
|
|
border-radius: 6px;
|
|
background: var(--surface2);
|
|
border-left: 3px solid transparent;
|
|
}
|
|
|
|
.log-entry.log-new {
|
|
animation: logIn 0.4s cubic-bezier(0.22, 1, 0.36, 1);
|
|
}
|
|
|
|
@keyframes logIn {
|
|
from { opacity: 0; transform: translateX(-10px); background: rgba(189,147,249,0.12); }
|
|
to { opacity: 1; transform: translateX(0); background: var(--surface2); }
|
|
}
|
|
|
|
.log-entry.join { border-color: var(--green); }
|
|
.log-entry.leave { border-color: var(--comment); }
|
|
.log-entry.death { border-color: var(--red); }
|
|
.log-entry.chat { border-color: var(--cyan); }
|
|
.log-entry.advancement { border-color: var(--yellow); }
|
|
.log-entry.start { border-color: var(--purple); }
|
|
.log-entry.stop { border-color: var(--orange); }
|
|
|
|
.log-time {
|
|
color: var(--comment);
|
|
font-size: 0.7rem;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.log-server-badge {
|
|
font-size: 0.62rem;
|
|
font-weight: 600;
|
|
padding: 0.05rem 0.35rem;
|
|
border-radius: 4px;
|
|
white-space: nowrap;
|
|
}
|
|
.log-server-badge.MC1 { background: rgba(189,147,249,0.15); color: var(--purple); }
|
|
.log-server-badge.MC2 { background: rgba(80,250,123,0.12); color: var(--green); }
|
|
|
|
.log-icon { font-size: 0.85rem; }
|
|
.log-msg { color: var(--fg); line-height: 1.4; }
|
|
.log-empty { color: var(--comment); font-size: 0.8rem; text-align: center; padding: 2rem; }
|
|
|
|
/* ===== Log filter ===== */
|
|
.log-filters {
|
|
display: flex;
|
|
gap: 0.4rem;
|
|
padding: 0.6rem 0.8rem;
|
|
border-bottom: 1px solid var(--border);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.filter-btn {
|
|
font-family: inherit;
|
|
font-size: 0.68rem;
|
|
font-weight: 700;
|
|
padding: 0.2rem 0.6rem;
|
|
border-radius: 20px;
|
|
border: 1px solid var(--border);
|
|
background: transparent;
|
|
color: var(--comment);
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
}
|
|
.filter-btn.active { color: var(--bg); font-weight: 700; }
|
|
.filter-btn[data-type="all"].active { background: var(--comment); border-color: var(--comment); }
|
|
.filter-btn[data-type="join"].active { background: var(--green); border-color: var(--green); }
|
|
.filter-btn[data-type="leave"].active { background: var(--comment); border-color: var(--comment); }
|
|
.filter-btn[data-type="death"].active { background: var(--red); border-color: var(--red); }
|
|
.filter-btn[data-type="chat"].active { background: var(--cyan); border-color: var(--cyan); }
|
|
.filter-btn[data-type="advancement"].active { background: var(--yellow); border-color: var(--yellow); }
|
|
|
|
/* ===== Player lists ===== */
|
|
.player-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.4rem;
|
|
max-height: 260px;
|
|
overflow-y: auto;
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--comment) transparent;
|
|
}
|
|
|
|
.player-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.6rem;
|
|
padding: 0.5rem 0.7rem;
|
|
background: var(--surface2);
|
|
border-radius: 8px;
|
|
border: 1px solid var(--border);
|
|
transition: border-color 0.2s;
|
|
}
|
|
.player-item:hover { border-color: var(--purple); }
|
|
|
|
.player-avatar {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 4px;
|
|
background: var(--border);
|
|
flex-shrink: 0;
|
|
overflow: hidden;
|
|
}
|
|
.player-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
|
|
|
.player-name {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
color: var(--fg);
|
|
flex: 1;
|
|
}
|
|
|
|
.player-uuid {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 0.6rem;
|
|
color: var(--comment);
|
|
}
|
|
|
|
.whitelist-disabled {
|
|
color: var(--comment);
|
|
font-size: 0.82rem;
|
|
text-align: center;
|
|
padding: 1.5rem;
|
|
font-style: italic;
|
|
}
|
|
|
|
/* ===== Mods list ===== */
|
|
.mods-list {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 0.4rem;
|
|
max-height: 260px;
|
|
overflow-y: auto;
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--comment) transparent;
|
|
}
|
|
|
|
.mod-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.15rem;
|
|
padding: 0.55rem 0.75rem;
|
|
background: var(--surface2);
|
|
border-radius: 8px;
|
|
border: 1px solid var(--border);
|
|
transition: border-color 0.2s;
|
|
}
|
|
.mod-item:hover { border-color: var(--cyan); }
|
|
|
|
.mod-name {
|
|
font-size: 0.82rem;
|
|
font-weight: 700;
|
|
color: var(--fg);
|
|
}
|
|
|
|
.mod-version {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 0.65rem;
|
|
color: var(--cyan);
|
|
}
|
|
|
|
.mod-desc {
|
|
font-size: 0.68rem;
|
|
color: var(--comment);
|
|
line-height: 1.3;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
/* ===== Status indicator ===== */
|
|
.refresh-info {
|
|
font-size: 0.68rem;
|
|
color: var(--comment);
|
|
font-family: 'JetBrains Mono', monospace;
|
|
}
|
|
|
|
.dot-live {
|
|
display: inline-block;
|
|
width: 6px;
|
|
height: 6px;
|
|
background: var(--green);
|
|
border-radius: 50%;
|
|
margin-right: 0.3rem;
|
|
animation: livePulse 2s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes livePulse {
|
|
0%, 100% { box-shadow: 0 0 0 0 rgba(80,250,123,0); }
|
|
50% { box-shadow: 0 0 0 4px rgba(80,250,123,0.2); }
|
|
}
|
|
|
|
/* ===== Empty states ===== */
|
|
.empty {
|
|
color: var(--comment);
|
|
font-size: 0.8rem;
|
|
text-align: center;
|
|
padding: 2rem 1rem;
|
|
}
|
|
|
|
/* ===== Responsive ===== */
|
|
@media (max-width: 900px) {
|
|
.mc-grid { grid-template-columns: 1fr; }
|
|
.mc-grid .log-panel { grid-column: 1; }
|
|
.mods-list { grid-template-columns: 1fr; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="page-wrap">
|
|
|
|
<header class="page-header">
|
|
<h1>⛏ Serveurs Minecraft</h1>
|
|
<a class="back-link" href="/">
|
|
← Retour au dashboard
|
|
</a>
|
|
</header>
|
|
|
|
<!-- Tabs serveurs -->
|
|
<div class="server-tabs">
|
|
<button class="tab-btn active" data-server="all">Tous</button>
|
|
<button class="tab-btn" data-server="MC1">MC #1 :9191</button>
|
|
<button class="tab-btn" data-server="MC2">MC #2 :9292</button>
|
|
</div>
|
|
|
|
<div class="mc-grid">
|
|
|
|
<!-- ===== LOG FEED ===== -->
|
|
<div class="panel log-panel">
|
|
<div class="panel-header">
|
|
<div class="panel-title">
|
|
<span class="dot-live"></span>
|
|
Activité en direct
|
|
</div>
|
|
<span class="refresh-info" id="last-update">—</span>
|
|
</div>
|
|
|
|
<!-- Filtres -->
|
|
<div class="log-filters">
|
|
<button class="filter-btn active" data-type="all">Tout</button>
|
|
<button class="filter-btn" data-type="join">🟢 Connexion</button>
|
|
<button class="filter-btn" data-type="leave">⚫ Déconnexion</button>
|
|
<button class="filter-btn" data-type="death">💀 Mort</button>
|
|
<button class="filter-btn" data-type="chat">💬 Chat</button>
|
|
<button class="filter-btn" data-type="advancement">🏆 Avancement</button>
|
|
</div>
|
|
|
|
<div class="log-feed" id="log-feed">
|
|
<div class="log-empty">Chargement des logs…</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===== WHITELIST ===== -->
|
|
<div class="panel">
|
|
<div class="panel-header">
|
|
<div class="panel-title">🔒 Whitelist</div>
|
|
<span class="panel-count" id="whitelist-count">—</span>
|
|
</div>
|
|
<div class="panel-body">
|
|
<div class="player-list" id="whitelist-list">
|
|
<div class="empty">Chargement…</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===== MODS ===== -->
|
|
<div class="panel">
|
|
<div class="panel-header">
|
|
<div class="panel-title">🧩 Mods Fabric (MC #2)</div>
|
|
<span class="panel-count" id="mods-count">—</span>
|
|
</div>
|
|
<div class="panel-body">
|
|
<div class="mods-list" id="mods-list">
|
|
<div class="empty" style="grid-column:1/-1">Chargement…</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
'use strict';
|
|
|
|
const API = '/data/api/mc-info.php';
|
|
const ICONS = {
|
|
join: '🟢',
|
|
leave: '⚫',
|
|
death: '💀',
|
|
chat: '💬',
|
|
advancement: '🏆',
|
|
start: '🚀',
|
|
stop: '🛑',
|
|
};
|
|
|
|
let activeServer = 'all';
|
|
let activeFilter = 'all';
|
|
let lastData = null;
|
|
let knownEventKey = null; // clé du dernier event connu
|
|
let firstRender = true; // premier chargement = pas d'animation
|
|
|
|
function eventKey(e) { return e.ts + '|' + e.server + '|' + e.msg; }
|
|
|
|
function buildEntry(e, animate) {
|
|
const time = new Date(e.ts).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
const icon = ICONS[e.type] || '•';
|
|
const el = document.createElement('div');
|
|
el.className = 'log-entry ' + esc(e.type) + (animate ? ' log-new' : '');
|
|
el.innerHTML =
|
|
'<span class="log-time">' + time + '</span>' +
|
|
'<span class="log-server-badge ' + esc(e.server) + '">' + esc(e.server) + '</span>' +
|
|
'<span class="log-msg">' + icon + ' ' + esc(e.msg) + '</span>';
|
|
return el;
|
|
}
|
|
|
|
function filterEvents(events) {
|
|
if (!events) return [];
|
|
return events.filter(e => {
|
|
return (activeServer === 'all' || e.server === activeServer) &&
|
|
(activeFilter === 'all' || e.type === activeFilter);
|
|
});
|
|
}
|
|
|
|
// Rendu complet sans animation (premier chargement, changement filtre/tab)
|
|
function renderLogsFull(events) {
|
|
const feed = document.getElementById('log-feed');
|
|
const filtered = filterEvents(events);
|
|
if (!filtered.length) {
|
|
feed.innerHTML = events && events.length
|
|
? '<div class="log-empty">Aucun événement correspondant.</div>'
|
|
: '<div class="log-empty">Aucun événement pour l\'instant.<br><small>Le watcher est-il bien actif ?</small></div>';
|
|
knownEventKey = null;
|
|
return;
|
|
}
|
|
feed.innerHTML = '';
|
|
filtered.forEach(e => feed.appendChild(buildEntry(e, false)));
|
|
knownEventKey = eventKey(filtered[0]);
|
|
}
|
|
|
|
// Rendu diff : insère seulement les nouvelles lignes avec animation
|
|
function renderLogsDiff(events) {
|
|
const feed = document.getElementById('log-feed');
|
|
const filtered = filterEvents(events);
|
|
if (!filtered.length) return;
|
|
|
|
// Trouver les events plus récents que le dernier connu
|
|
let newEvents = [];
|
|
if (knownEventKey === null) {
|
|
newEvents = filtered;
|
|
} else {
|
|
for (const e of filtered) {
|
|
if (eventKey(e) === knownEventKey) break;
|
|
newEvents.push(e);
|
|
}
|
|
}
|
|
if (!newEvents.length) return;
|
|
|
|
// Vider le placeholder si présent
|
|
if (feed.querySelector('.log-empty')) feed.innerHTML = '';
|
|
|
|
// Insérer en haut dans le bon ordre (feed est flex column-reverse)
|
|
for (let i = newEvents.length - 1; i >= 0; i--) {
|
|
const el = buildEntry(newEvents[i], true);
|
|
feed.insertBefore(el, feed.firstChild);
|
|
el.addEventListener('animationend', () => el.classList.remove('log-new'), { once: true });
|
|
}
|
|
|
|
// Limiter le DOM à 100 entrées
|
|
const all = feed.querySelectorAll('.log-entry');
|
|
for (let i = 100; i < all.length; i++) all[i].remove();
|
|
|
|
knownEventKey = eventKey(filtered[0]);
|
|
}
|
|
|
|
// ===== Tabs =====
|
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
activeServer = btn.dataset.server;
|
|
if (lastData) { firstRender = true; renderLogsFull(lastData.events); firstRender = false; }
|
|
});
|
|
});
|
|
|
|
// ===== Filtres log =====
|
|
document.querySelectorAll('.filter-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
activeFilter = btn.dataset.type;
|
|
if (lastData) renderLogsFull(lastData.events);
|
|
});
|
|
});
|
|
|
|
// ===== Fetch =====
|
|
async function fetchData() {
|
|
try {
|
|
const res = await fetch(API, { cache: 'no-cache' });
|
|
if (!res.ok) throw new Error(res.status);
|
|
const json = await res.json();
|
|
if (json.success) {
|
|
const data = json.data;
|
|
if (firstRender) {
|
|
renderLogsFull(data.events);
|
|
renderMods(data.mods);
|
|
firstRender = false;
|
|
} else {
|
|
renderLogsDiff(data.events);
|
|
}
|
|
renderWhitelist(data.whitelist, data.whitelist_enabled);
|
|
lastData = data;
|
|
document.getElementById('last-update').textContent =
|
|
'MAJ ' + new Date().toLocaleTimeString('fr-FR');
|
|
}
|
|
} catch (e) {
|
|
console.error('[mc-info]', e);
|
|
}
|
|
}
|
|
|
|
function renderAll(data) {
|
|
renderLogsFull(data.events);
|
|
renderWhitelist(data.whitelist, data.whitelist_enabled);
|
|
renderMods(data.mods);
|
|
}
|
|
|
|
// alias pour compat
|
|
function renderLogs(events) { renderLogsFull(events); }
|
|
|
|
// ===== Whitelist =====
|
|
function renderWhitelist(whitelist, enabled) {
|
|
const list = document.getElementById('whitelist-list');
|
|
const count = document.getElementById('whitelist-count');
|
|
|
|
// Fusionner les deux serveurs selon le tab actif
|
|
let players = [];
|
|
const keys = activeServer === 'all' ? ['mc1', 'mc2']
|
|
: activeServer === 'MC1' ? ['mc1'] : ['mc2'];
|
|
|
|
for (const key of keys) {
|
|
if (!enabled[key]) continue; // whitelist désactivée
|
|
if (whitelist[key]) players.push(...whitelist[key]);
|
|
}
|
|
|
|
// Dédoublonner par UUID
|
|
const seen = new Set();
|
|
players = players.filter(p => {
|
|
if (seen.has(p.uuid)) return false;
|
|
seen.add(p.uuid); return true;
|
|
});
|
|
|
|
// Vérifier si whitelist désactivée sur tous les serveurs visibles
|
|
const allDisabled = keys.every(k => !enabled[k]);
|
|
if (allDisabled) {
|
|
count.textContent = 'désactivée';
|
|
list.innerHTML = '<div class="whitelist-disabled">La whitelist est désactivée sur ce serveur.</div>';
|
|
return;
|
|
}
|
|
|
|
count.textContent = players.length + ' joueur' + (players.length > 1 ? 's' : '');
|
|
|
|
if (players.length === 0) {
|
|
list.innerHTML = '<div class="empty">Whitelist vide.</div>';
|
|
return;
|
|
}
|
|
|
|
list.innerHTML = players.map(p => `
|
|
<div class="player-item">
|
|
<div class="player-avatar">
|
|
<img src="https://mc-heads.net/avatar/${esc(p.uuid)}/28" alt="${esc(p.name)}" loading="lazy">
|
|
</div>
|
|
<span class="player-name">${esc(p.name)}</span>
|
|
<span class="player-uuid">${esc(p.uuid).substring(0, 8)}…</span>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// ===== Mods =====
|
|
function renderMods(mods) {
|
|
const list = document.getElementById('mods-list');
|
|
const count = document.getElementById('mods-count');
|
|
|
|
if (!mods || mods.length === 0) {
|
|
count.textContent = '0 mod';
|
|
list.innerHTML = '<div class="empty" style="grid-column:1/-1">Aucun mod trouvé.<br><small>Vérifiez /srv/Fabric/mods/</small></div>';
|
|
return;
|
|
}
|
|
|
|
count.textContent = mods.length + ' mod' + (mods.length > 1 ? 's' : '');
|
|
list.innerHTML = mods.map(m => `
|
|
<div class="mod-item">
|
|
<span class="mod-name">${esc(m.name || m.filename)}</span>
|
|
${m.version ? `<span class="mod-version">v${esc(m.version)}</span>` : ''}
|
|
${m.description ? `<span class="mod-desc">${esc(m.description)}</span>` : ''}
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function esc(s) {
|
|
if (s == null) return '';
|
|
return String(s)
|
|
.replace(/&/g,'&').replace(/</g,'<')
|
|
.replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
|
|
// ===== Init =====
|
|
fetchData();
|
|
setInterval(fetchData, 5000); // refresh toutes les 5s
|
|
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (!document.hidden) fetchData();
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|