first commit
This commit is contained in:
Executable
+693
@@ -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,'&').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>
|
||||
Executable
+124
@@ -0,0 +1,124 @@
|
||||
#!/bin/bash
|
||||
# mc-log-watcher.sh - Parse les logs MC en temps réel + historique récent
|
||||
|
||||
OUTPUT="/var/cache/mc-logs.json"
|
||||
MAX_EVENTS=200
|
||||
LOG_MC1="/srv/Minecraft/logs/latest.log"
|
||||
LOG_MC2="/srv/Fabric/logs/latest.log"
|
||||
TMP="$OUTPUT.tmp"
|
||||
HISTORY_LINES=500 # Lignes historiques à lire au démarrage
|
||||
|
||||
declare -a EVENTS=()
|
||||
|
||||
touch "$OUTPUT"
|
||||
chmod 644 "$OUTPUT"
|
||||
|
||||
write_json() {
|
||||
local json='{"events":['
|
||||
local first=true
|
||||
for event in "${EVENTS[@]}"; do
|
||||
$first || json+=','
|
||||
json+="$event"
|
||||
first=false
|
||||
done
|
||||
json+=']}'
|
||||
echo "$json" > "$TMP"
|
||||
mv "$TMP" "$OUTPUT"
|
||||
}
|
||||
|
||||
add_event() {
|
||||
local server="$1"
|
||||
local type="$2"
|
||||
local message="$3"
|
||||
local ts="$4" # optionnel: timestamp extrait du log
|
||||
|
||||
# Si pas de timestamp fourni, utiliser l'heure actuelle
|
||||
if [ -z "$ts" ]; then
|
||||
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
fi
|
||||
|
||||
message="${message//\\/\\\\}"
|
||||
message="${message//\"/\\\"}"
|
||||
|
||||
local entry="{\"ts\":\"$ts\",\"server\":\"$server\",\"type\":\"$type\",\"msg\":\"$message\"}"
|
||||
EVENTS+=("$entry")
|
||||
|
||||
if [ "${#EVENTS[@]}" -gt "$MAX_EVENTS" ]; then
|
||||
EVENTS=("${EVENTS[@]:1}")
|
||||
fi
|
||||
}
|
||||
|
||||
# Extraire le timestamp ISO depuis une ligne de log MC
|
||||
# Format: [HH:MM:SS] ... → on utilise la date du jour + l'heure du log
|
||||
extract_ts() {
|
||||
local line="$1"
|
||||
local hour
|
||||
if [[ "$line" =~ ^\[([0-9]{2}):([0-9]{2}):([0-9]{2})\] ]]; then
|
||||
hour="${BASH_REMATCH[1]}:${BASH_REMATCH[2]}:${BASH_REMATCH[3]}"
|
||||
echo "$(date -u +%Y-%m-%d)T${hour}Z"
|
||||
else
|
||||
date -u +"%Y-%m-%dT%H:%M:%SZ"
|
||||
fi
|
||||
}
|
||||
|
||||
parse_line() {
|
||||
local server="$1"
|
||||
local line="$2"
|
||||
local write="$3" # "write" pour écrire le JSON après
|
||||
|
||||
[[ -z "$line" ]] && return
|
||||
|
||||
local ts
|
||||
ts=$(extract_ts "$line")
|
||||
|
||||
if [[ "$line" =~ ([A-Za-z0-9_]+)\ joined\ the\ game ]]; then
|
||||
add_event "$server" "join" "${BASH_REMATCH[1]} a rejoint le serveur" "$ts"
|
||||
elif [[ "$line" =~ ([A-Za-z0-9_]+)\ left\ the\ game ]]; then
|
||||
add_event "$server" "leave" "${BASH_REMATCH[1]} a quitté le serveur" "$ts"
|
||||
elif [[ "$line" =~ ([A-Za-z0-9_]+)\ (was\ slain|was\ shot|drowned|fell|burned|tried\ to\ swim|walked\ into|was\ killed|was\ blown|suffocated|hit\ the\ ground|starved|was\ poked|was\ impaled|was\ fireballed|was\ stung) ]]; then
|
||||
local msg="${line#*]: }"
|
||||
add_event "$server" "death" "$msg" "$ts"
|
||||
elif [[ "$line" =~ ([A-Za-z0-9_]+)\ has\ (made\ the\ advancement|completed\ the\ challenge|reached\ the\ goal)\ \[(.+)\] ]]; then
|
||||
add_event "$server" "advancement" "${BASH_REMATCH[1]} : ${BASH_REMATCH[3]}" "$ts"
|
||||
elif [[ "$line" =~ \[Server\ thread/INFO\]:\ \<([A-Za-z0-9_]+)\>\ (.+) ]]; then
|
||||
add_event "$server" "chat" "<${BASH_REMATCH[1]}> ${BASH_REMATCH[2]}" "$ts"
|
||||
elif [[ "$line" =~ Done\ \([0-9.]+s\)\!\ For\ help ]]; then
|
||||
add_event "$server" "start" "Serveur démarré" "$ts"
|
||||
elif [[ "$line" =~ Stopping\ the\ server ]]; then
|
||||
add_event "$server" "stop" "Serveur arrêté" "$ts"
|
||||
fi
|
||||
|
||||
[ "$write" = "write" ] && write_json
|
||||
}
|
||||
|
||||
# ===== Lire l'historique récent des deux logs =====
|
||||
echo "[mc-log-watcher] Lecture historique ($HISTORY_LINES lignes)..."
|
||||
|
||||
if [ -f "$LOG_MC1" ]; then
|
||||
while IFS= read -r line; do
|
||||
parse_line "MC1" "$line"
|
||||
done < <(tail -n "$HISTORY_LINES" "$LOG_MC1")
|
||||
fi
|
||||
|
||||
if [ -f "$LOG_MC2" ]; then
|
||||
while IFS= read -r line; do
|
||||
parse_line "MC2" "$line"
|
||||
done < <(tail -n "$HISTORY_LINES" "$LOG_MC2")
|
||||
fi
|
||||
|
||||
write_json
|
||||
echo "[mc-log-watcher] Historique chargé (${#EVENTS[@]} events). Surveillance en cours..."
|
||||
|
||||
# ===== Surveiller en temps réel =====
|
||||
FIFO=$(mktemp -u /tmp/mc-watcher-XXXXXX)
|
||||
mkfifo "$FIFO"
|
||||
trap "rm -f $FIFO; kill 0" EXIT INT TERM
|
||||
|
||||
tail -F "$LOG_MC1" 2>/dev/null | sed 's/^/MC1|/' > "$FIFO" &
|
||||
tail -F "$LOG_MC2" 2>/dev/null | sed 's/^/MC2|/' > "$FIFO" &
|
||||
|
||||
while IFS= read -r rawline; do
|
||||
server="${rawline%%|*}"
|
||||
line="${rawline#*|}"
|
||||
parse_line "$server" "$line" "write"
|
||||
done < "$FIFO"
|
||||
Reference in New Issue
Block a user