first commit

This commit is contained in:
nix
2026-05-16 11:10:19 +02:00
commit 509c9b3737
172 changed files with 14496 additions and 0 deletions
+693
View File
@@ -0,0 +1,693 @@
<!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,'&amp;').replace(/</g,'&lt;')
.replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ===== Init =====
fetchData();
setInterval(fetchData, 5000); // refresh toutes les 5s
document.addEventListener('visibilitychange', () => {
if (!document.hidden) fetchData();
});
})();
</script>
</body>
</html>