- Suppression de 10 fichiers obsolètes (scripts, tests, docs temporaires) - Suppression des dossiers vides (frontend/src, backend/src/middlewares) - Réecriture complète de 7 fichiers de documentation - README.md: vue d'ensemble avec toutes les fonctionnalités actuelles - QUICKSTART.md: guide de démarrage rapide en 3 étapes - CONFIGURATION.md: guide de configuration complète (Docker, RCON, sécurité) - DEPLOYMENT.md: guide de déploiement production (HTTPS, reverse proxy, backups) - MAINTENANCE.md: guide de maintenance avec dépannage exhaustif - INDEX.md: index de navigation simplifié - CHANGELOG.md: historique complet v1.0.0 - Optimisation docker-compose.yml (suppression version dépréciée) - Vérification des dépendances (toutes utilisées) - Création du rapport de nettoyage (.cleanup-report.md) - Documentation cohérente avec le code actuel - Projet 100% prêt pour la production
1466 lines
58 KiB
JavaScript
1466 lines
58 KiB
JavaScript
// Configuration API
|
||
const API_URL = 'http://localhost:4001/api';
|
||
|
||
// State
|
||
let currentUser = null;
|
||
let isAuthenticated = false;
|
||
|
||
// Initialisation
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
checkAuth();
|
||
});
|
||
|
||
// Vérifier l'authentification
|
||
async function checkAuth() {
|
||
try {
|
||
const response = await fetch(`${API_URL}/auth/check`, {
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.authenticated) {
|
||
currentUser = data.user;
|
||
isAuthenticated = true;
|
||
showDashboard();
|
||
} else {
|
||
showLoginPage();
|
||
}
|
||
} catch (e) {
|
||
console.error('Erreur vérification auth:', e);
|
||
showLoginPage();
|
||
}
|
||
}
|
||
|
||
// Afficher la page de connexion
|
||
function showLoginPage() {
|
||
document.getElementById('app').innerHTML = `
|
||
<div class="login-container">
|
||
<div class="login-box">
|
||
<h1>🎮 NationsGlory Admin</h1>
|
||
<div class="message" id="loginMessage"></div>
|
||
|
||
<div id="loginForm">
|
||
<h2 style="text-align: center; margin-bottom: 20px; color: var(--primary); font-size: 18px;">Connexion</h2>
|
||
<div class="form-group">
|
||
<label>Nom d'utilisateur</label>
|
||
<input type="text" id="username" placeholder="admin">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Mot de passe</label>
|
||
<input type="password" id="password" placeholder="">
|
||
</div>
|
||
<button class="btn-primary" onclick="handleLogin()">Se connecter</button>
|
||
<div style="text-align: center; margin-top: 15px;">
|
||
<small>Pas encore de compte? <a href="#" onclick="toggleToRegister(event)" style="color: var(--primary); text-decoration: underline; cursor: pointer;">Créer un compte</a></small>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="registerForm" style="display: none;">
|
||
<h2 style="text-align: center; margin-bottom: 20px; color: var(--primary); font-size: 18px;">Créer le compte Admin</h2>
|
||
<div class="form-group">
|
||
<label>Nom d'utilisateur</label>
|
||
<input type="text" id="regUsername" placeholder="admin">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Mot de passe</label>
|
||
<input type="password" id="regPassword" placeholder="">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Pseudo Minecraft (doit être OP)</label>
|
||
<input type="text" id="mcUsername" placeholder="VotreNomMC">
|
||
</div>
|
||
<button class="btn-primary" onclick="handleRegister()">Créer le compte</button>
|
||
<div style="text-align: center; margin-top: 15px;">
|
||
<small>Vous avez déjà un compte? <a href="#" onclick="toggleToLogin(event)" style="color: var(--primary); text-decoration: underline; cursor: pointer;">Se connecter</a></small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Par défaut, montrer le formulaire de connexion
|
||
const regForm = document.getElementById('registerForm');
|
||
const loginForm = document.getElementById('loginForm');
|
||
|
||
if (regForm && loginForm) {
|
||
regForm.style.display = 'none';
|
||
loginForm.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
// Connexion
|
||
async function handleLogin() {
|
||
const username = document.getElementById('username').value;
|
||
const password = document.getElementById('password').value;
|
||
|
||
if (!username || !password) {
|
||
showMessage('Veuillez remplir tous les champs', 'error', 'loginMessage');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${API_URL}/auth/login`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({ username, password })
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (response.ok) {
|
||
currentUser = data.user;
|
||
isAuthenticated = true;
|
||
showDashboard();
|
||
} else {
|
||
showMessage(data.error || 'Erreur de connexion', 'error', 'loginMessage');
|
||
}
|
||
} catch (e) {
|
||
console.error('Erreur connexion:', e);
|
||
showMessage('Erreur serveur', 'error', 'loginMessage');
|
||
}
|
||
}
|
||
|
||
// Enregistrement
|
||
async function handleRegister() {
|
||
const username = document.getElementById('regUsername').value;
|
||
const password = document.getElementById('regPassword').value;
|
||
const mcUsername = document.getElementById('mcUsername').value;
|
||
|
||
if (!username || !password || !mcUsername) {
|
||
showMessage('Veuillez remplir tous les champs', 'error', 'loginMessage');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${API_URL}/auth/register`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({ username, password, mcUsername })
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (response.ok) {
|
||
showMessage('Compte créé! Connectez-vous maintenant.', 'success', 'loginMessage');
|
||
setTimeout(() => {
|
||
document.getElementById('registerForm').style.display = 'none';
|
||
document.getElementById('loginForm').style.display = 'block';
|
||
document.getElementById('username').value = username;
|
||
document.getElementById('password').value = '';
|
||
}, 1000);
|
||
} else {
|
||
showMessage(data.error || 'Erreur d\'enregistrement', 'error', 'loginMessage');
|
||
}
|
||
} catch (e) {
|
||
console.error('Erreur enregistrement:', e);
|
||
showMessage('Erreur serveur', 'error', 'loginMessage');
|
||
}
|
||
}
|
||
|
||
// Basculer vers le formulaire de connexion
|
||
function toggleToLogin(event) {
|
||
event.preventDefault();
|
||
const regForm = document.getElementById('registerForm');
|
||
const loginForm = document.getElementById('loginForm');
|
||
|
||
if (regForm && loginForm) {
|
||
regForm.style.display = 'none';
|
||
loginForm.style.display = 'block';
|
||
document.getElementById('loginMessage').innerHTML = '';
|
||
// Effacer les champs
|
||
document.getElementById('username').value = '';
|
||
document.getElementById('password').value = '';
|
||
}
|
||
}
|
||
|
||
// Basculer vers le formulaire d'enregistrement
|
||
function toggleToRegister(event) {
|
||
event.preventDefault();
|
||
const regForm = document.getElementById('registerForm');
|
||
const loginForm = document.getElementById('loginForm');
|
||
|
||
if (regForm && loginForm) {
|
||
loginForm.style.display = 'none';
|
||
regForm.style.display = 'block';
|
||
document.getElementById('loginMessage').innerHTML = '';
|
||
// Effacer les champs
|
||
document.getElementById('regUsername').value = '';
|
||
document.getElementById('regPassword').value = '';
|
||
document.getElementById('mcUsername').value = '';
|
||
}
|
||
}
|
||
|
||
// Afficher le dashboard
|
||
function showDashboard() {
|
||
document.getElementById('app').innerHTML = `
|
||
<div class="container">
|
||
<div class="header">
|
||
<div>
|
||
<h1>🎮 NationsGlory Admin Panel</h1>
|
||
</div>
|
||
<div class="user-info">
|
||
<p>👤 <strong>${currentUser.username}</strong> (${currentUser.mcUsername})</p>
|
||
<button class="btn-danger" onclick="handleLogout()">Déconnexion</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="message" id="dashboardMessage"></div>
|
||
|
||
<div class="nav-tabs">
|
||
<button class="active" onclick="switchTab('dashboard')">📊 Dashboard</button>
|
||
<button onclick="switchTab('console')">⌨️ Console RCON</button>
|
||
<button onclick="switchTab('logs')">📜 Logs</button>
|
||
<button onclick="switchTab('players')">👥 Joueurs</button>
|
||
<button onclick="switchTab('whitelist')">✅ Whitelist</button>
|
||
<button onclick="switchTab('backups')">💾 Backups</button>
|
||
<button onclick="switchTab('settings')">⚙️ Paramètres</button>
|
||
</div>
|
||
|
||
<!-- Dashboard -->
|
||
<div id="dashboard" class="content-section active">
|
||
${getDashboardHTML()}
|
||
</div>
|
||
|
||
<!-- Console RCON -->
|
||
<div id="console" class="content-section">
|
||
${getConsoleHTML()}
|
||
</div>
|
||
|
||
<!-- Logs -->
|
||
<div id="logs" class="content-section">
|
||
${getLogsHTML()}
|
||
</div>
|
||
|
||
<!-- Joueurs -->
|
||
<div id="players" class="content-section">
|
||
${getPlayersHTML()}
|
||
</div>
|
||
|
||
<!-- Whitelist -->
|
||
<div id="whitelist" class="content-section">
|
||
${getWhitelistHTML()}
|
||
</div>
|
||
|
||
<!-- Backups -->
|
||
<div id="backups" class="content-section">
|
||
${getBackupsHTML()}
|
||
</div>
|
||
|
||
<!-- Paramètres -->
|
||
<div id="settings" class="content-section">
|
||
${getSettingsHTML()}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Charger les données
|
||
loadDashboardData();
|
||
}
|
||
|
||
// Changer d'onglet
|
||
function switchTab(tabName) {
|
||
// Masquer tous les onglets
|
||
document.querySelectorAll('.content-section').forEach(el => {
|
||
el.classList.remove('active');
|
||
});
|
||
|
||
// Afficher l'onglet sélectionné
|
||
document.getElementById(tabName).classList.add('active');
|
||
|
||
// Mettre à jour les boutons
|
||
document.querySelectorAll('.nav-tabs button').forEach(btn => {
|
||
btn.classList.remove('active');
|
||
});
|
||
event.target.classList.add('active');
|
||
|
||
// Charger les données spécifiques
|
||
if (tabName === 'console') loadConsoleData();
|
||
if (tabName === 'logs') loadLogsData();
|
||
if (tabName === 'players') loadPlayersData();
|
||
if (tabName === 'whitelist') loadWhitelistData();
|
||
if (tabName === 'backups') loadBackupsData();
|
||
if (tabName === 'settings') loadSettingsData();
|
||
}
|
||
|
||
// ========== DASHBOARD ==========
|
||
function getDashboardHTML() {
|
||
return `
|
||
<div class="section-card">
|
||
<h2>🎮 Contrôle du Serveur</h2>
|
||
<div class="btn-group">
|
||
<button class="btn-primary" onclick="saveServer()">💾 Sauvegarder</button>
|
||
<button class="btn-warning" onclick="openChangeRconPasswordModal()">🔐 Changer RCON</button>
|
||
<button class="btn-danger" id="cancelRestartBtn" style="display: none;" onclick="cancelRestart()">❌ Annuler Redémarrage</button>
|
||
<button class="btn-danger" id="cancelShutdownBtnDash" style="display: none;" onclick="cancelShutdown()">❌ Annuler Arrêt</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid">
|
||
<div class="grid-card">
|
||
<h3>État Serveur</h3>
|
||
<p class="value"><span class="status online"></span>En ligne</p>
|
||
</div>
|
||
<div class="grid-card">
|
||
<h3>Joueurs Connectés</h3>
|
||
<p class="value" id="playerCount">0/20</p>
|
||
</div>
|
||
<div class="grid-card">
|
||
<h3>Version Forge</h3>
|
||
<p class="value" id="forgeVersion">1.6.4</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section-card">
|
||
<h2>📊 Informations Rapides</h2>
|
||
<table>
|
||
<tbody id="quickInfoTable">
|
||
<tr><td>Chargement...</td><td></td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
async function loadDashboardData() {
|
||
try {
|
||
// Charger les joueurs en ligne
|
||
await window.loadOnlinePlayers();
|
||
|
||
const response = await fetch(`${API_URL}/server`, {
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.properties) {
|
||
const table = document.getElementById('quickInfoTable');
|
||
table.innerHTML = `
|
||
<tr><td><strong>Nom Serveur</strong></td><td>${data.properties['motd'] || 'NationsGlory'}</td></tr>
|
||
<tr><td><strong>Mode Jeu</strong></td><td>${data.properties['gamemode'] || 'Survival'}</td></tr>
|
||
<tr><td><strong>Difficulté</strong></td><td>${data.properties['difficulty'] || '2'}</td></tr>
|
||
<tr><td><strong>PvP</strong></td><td>${data.properties['pvp'] === 'true' ? 'Activé' : 'Désactivé'}</td></tr>
|
||
<tr><td><strong>Port</strong></td><td>${data.properties['server-port'] || '25565'}</td></tr>
|
||
<tr><td><strong>Max Joueurs</strong></td><td>${data.properties['max-players'] || '20'}</td></tr>
|
||
`;
|
||
}
|
||
|
||
// Mettre à jour le nombre de joueurs
|
||
const onlineResponse = await fetch(`${API_URL}/rcon/command`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({ command: 'list' })
|
||
});
|
||
|
||
if (onlineResponse.ok) {
|
||
const rconData = await onlineResponse.json();
|
||
const output = rconData.response || '';
|
||
const cleanOutput = output.replace(/§[0-9a-fk-or]/gi, '');
|
||
const match = cleanOutput.match(/There are (\d+) out of maximum (\d+) players online/i);
|
||
|
||
if (match) {
|
||
const onlineCount = parseInt(match[1]);
|
||
const maxPlayers = parseInt(match[2]);
|
||
document.getElementById('playerCount').textContent = `${onlineCount}/${maxPlayers}`;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('Erreur chargement dashboard:', e);
|
||
}
|
||
}
|
||
|
||
// ========== CONSOLE RCON ==========
|
||
function getConsoleHTML() {
|
||
return `
|
||
<div class="section-card">
|
||
<h2>⌨️ Console RCON</h2>
|
||
<p style="margin-bottom: 15px; color: #666;">Exécutez des commandes directement sur le serveur</p>
|
||
<div style="background: #1e1e1e; border-radius: 8px; padding: 15px; margin-bottom: 15px; font-family: 'Courier New', monospace; border: 1px solid #333;">
|
||
<div id="consoleOutput" style="color: #0f0; min-height: 300px; max-height: 400px; overflow-y: auto; margin-bottom: 15px; line-height: 1.5; font-size: 13px; white-space: pre-wrap; word-wrap: break-word;"></div>
|
||
<div style="display: flex; gap: 5px;">
|
||
<span style="color: #0f0;">admin@nationsglory:~$</span>
|
||
<input type="text" id="commandInput" placeholder="" style="background: #1e1e1e; color: #0f0; border: none; outline: none; flex: 1; font-family: 'Courier New', monospace; font-size: 13px;" onkeypress="if(event.key==='Enter') sendRconCommand();">
|
||
</div>
|
||
</div>
|
||
|
||
<h3 style="margin-top: 30px; margin-bottom: 15px;"><3E>️ Actions Rapides</h3>
|
||
<div class="btn-group" style="margin-bottom: 15px; flex-wrap: wrap; gap: 10px;">
|
||
<button class="btn-primary" onclick="openSayModal()">💬 Message Chat</button>
|
||
<button class="btn-success" onclick="openRestartModal()">🔄 Redémarrer</button>
|
||
<button class="btn-warning" onclick="openShutdownModal()" style="background: #ff9800;">⏹️ Arrêter Serveur</button>
|
||
<button class="btn-danger" onclick="cancelRestart()" id="cancelRestartBtn" style="display: none;">❌ Annuler Redémarrage</button>
|
||
<button class="btn-danger" onclick="cancelShutdown()" id="cancelShutdownBtn" style="display: none;">❌ Annuler Arrêt</button>
|
||
</div>
|
||
|
||
<h3 style="margin-top: 30px; margin-bottom: 15px;">📜 Historique des Commandes</h3>
|
||
<div class="btn-group" style="margin-bottom: 15px;">
|
||
<input type="text" id="historySearch" placeholder="Chercher dans l'historique..." style="padding: 8px 12px; border-radius: 4px; border: 1px solid #ddd; flex: 1;" onchange="loadConsoleData()">
|
||
<button class="btn-secondary" onclick="loadConsoleData()">🔄 Rafraîchir</button>
|
||
<button class="btn-danger" onclick="clearRconHistory()">🗑️ Vider</button>
|
||
</div>
|
||
<div id="historyContainer" style="max-height: 400px; overflow-y: auto; background: #f8f9fa; border-radius: 5px; padding: 15px; border: 1px solid #ddd;"></div>
|
||
</div>
|
||
|
||
<!-- Modal Message Chat -->
|
||
<div id="sayModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center;">
|
||
<div style="background: white; padding: 30px; border-radius: 8px; width: 90%; max-width: 500px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
|
||
<h3 style="margin-top: 0; margin-bottom: 20px;">💬 Envoyer un Message</h3>
|
||
<textarea id="sayInput" placeholder="Entrez votre message..." style="width: 100%; height: 100px; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-family: Arial, sans-serif; resize: vertical; box-sizing: border-box;"></textarea>
|
||
<div style="margin-top: 15px; display: flex; gap: 10px; justify-content: flex-end;">
|
||
<button class="btn-secondary" onclick="document.getElementById('sayModal').style.display='none'">Annuler</button>
|
||
<button class="btn-primary" onclick="sendSayCommand()">Envoyer</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal Arrêt Serveur -->
|
||
<div id="shutdownModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center;">
|
||
<div style="background: white; padding: 30px; border-radius: 8px; width: 90%; max-width: 500px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
|
||
<h3 style="margin-top: 0; margin-bottom: 20px;">⏹️ Arrêter le Serveur</h3>
|
||
<div style="margin-bottom: 20px;">
|
||
<label style="display: block; margin-bottom: 10px; font-weight: bold;">Cooldown avant arrêt (secondes):</label>
|
||
<input type="number" id="shutdownCooldown" value="60" min="10" max="600" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box;">
|
||
</div>
|
||
<div style="margin-bottom: 20px;">
|
||
<label style="display: block; margin-bottom: 10px; font-weight: bold;">Message d'avertissement:</label>
|
||
<textarea id="shutdownMessage" placeholder="Message à envoyer aux joueurs..." style="width: 100%; height: 80px; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-family: Arial, sans-serif; resize: vertical; box-sizing: border-box;">Le serveur redémarre dans {cooldown} secondes!</textarea>
|
||
</div>
|
||
<p style="color: #ff9800; background: #fff3e0; padding: 10px; border-radius: 4px; margin-bottom: 15px; font-size: 13px;">⚠️ Un message d'avertissement sera envoyé tous les 10 secondes</p>
|
||
<div style="display: flex; gap: 10px; justify-content: flex-end;">
|
||
<button class="btn-secondary" onclick="document.getElementById('shutdownModal').style.display='none'">Annuler</button>
|
||
<button class="btn-danger" style="background: #dc3545;" onclick="shutdownServer()">Confirmer l'Arrêt</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal Redémarrage Serveur -->
|
||
<div id="restartModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center;">
|
||
<div style="background: white; padding: 30px; border-radius: 8px; width: 90%; max-width: 500px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
|
||
<h3 style="margin-top: 0; margin-bottom: 20px;">🔄 Redémarrer le Serveur</h3>
|
||
<div style="margin-bottom: 20px;">
|
||
<label style="display: block; margin-bottom: 10px; font-weight: bold;">Cooldown avant redémarrage (secondes):</label>
|
||
<input type="number" id="restartCooldown" value="60" min="10" max="600" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box;">
|
||
</div>
|
||
<div style="margin-bottom: 20px;">
|
||
<label style="display: block; margin-bottom: 10px; font-weight: bold;">Message d'avertissement:</label>
|
||
<textarea id="restartMessage" placeholder="Message à envoyer aux joueurs..." style="width: 100%; height: 80px; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-family: Arial, sans-serif; resize: vertical; box-sizing: border-box;">Le serveur redémarre dans {cooldown} secondes!</textarea>
|
||
</div>
|
||
<p style="color: #2196F3; background: #e3f2fd; padding: 10px; border-radius: 4px; margin-bottom: 15px; font-size: 13px;">ℹ️ Un message d'avertissement sera envoyé tous les 10 secondes</p>
|
||
<div style="display: flex; gap: 10px; justify-content: flex-end;">
|
||
<button class="btn-secondary" onclick="document.getElementById('restartModal').style.display='none'">Annuler</button>
|
||
<button class="btn-success" onclick="restartServer()">Confirmer le Redémarrage</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
async function loadConsoleData() {
|
||
try {
|
||
const search = document.getElementById('historySearch')?.value || '';
|
||
const url = new URL(`${API_URL}/rcon/history`);
|
||
url.searchParams.set('limit', '50');
|
||
if (search) url.searchParams.set('search', search);
|
||
|
||
const response = await fetch(url, {
|
||
credentials: 'include'
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
const container = document.getElementById('historyContainer');
|
||
|
||
if (data.history && data.history.length > 0) {
|
||
container.innerHTML = data.history.map(h => {
|
||
const timestamp = new Date(h.timestamp).toLocaleString('fr-FR');
|
||
const statusIcon = h.success ? '✓' : '✗';
|
||
const statusColor = h.success ? '#28a745' : '#dc3545';
|
||
return `
|
||
<div style="margin-bottom: 12px; padding: 10px; background: white; border-left: 3px solid ${statusColor}; border-radius: 3px; font-size: 13px;">
|
||
<div style="display: flex; justify-content: space-between; align-items: start;">
|
||
<div style="flex: 1;">
|
||
<strong style="color: var(--primary); word-break: break-all;">${h.command}</strong><br>
|
||
<span style="color: #666; font-size: 11px;">${timestamp}</span>
|
||
</div>
|
||
<span style="color: ${statusColor}; font-weight: bold; margin-left: 10px;">${statusIcon}</span>
|
||
</div>
|
||
${h.response ? `<div style="color: #333; margin-top: 5px; padding-top: 5px; border-top: 1px solid #eee; font-size: 12px; max-height: 80px; overflow-y: auto; word-break: break-word;">${h.response}</div>` : ''}
|
||
${h.error ? `<div style="color: #dc3545; margin-top: 5px; padding-top: 5px; border-top: 1px solid #eee; font-size: 12px;">Erreur: ${h.error}</div>` : ''}
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
} else {
|
||
container.innerHTML = '<p style="color: #666; text-align: center; padding: 20px;">Aucun historique</p>';
|
||
}
|
||
} catch (e) {
|
||
console.error('Erreur historique:', e);
|
||
const container = document.getElementById('historyContainer');
|
||
container.innerHTML = `<p style="color: #dc3545;">Erreur lors du chargement: ${e.message}</p>`;
|
||
}
|
||
}
|
||
|
||
async function clearRconHistory() {
|
||
if (!confirm('Êtes-vous sûr de vouloir supprimer tout l\'historique?')) return;
|
||
|
||
try {
|
||
const response = await fetch(`${API_URL}/rcon/history`, {
|
||
method: 'DELETE',
|
||
credentials: 'include'
|
||
});
|
||
|
||
if (response.ok) {
|
||
showMessage('Historique supprimé', 'success', 'dashboardMessage');
|
||
loadConsoleData();
|
||
} else {
|
||
showMessage('Erreur lors de la suppression', 'error', 'dashboardMessage');
|
||
}
|
||
} catch (e) {
|
||
console.error('Erreur suppression historique:', e);
|
||
showMessage('Erreur serveur', 'error', 'dashboardMessage');
|
||
}
|
||
}
|
||
|
||
async function sendRconCommand() {
|
||
const commandInput = document.getElementById('commandInput');
|
||
const command = commandInput.value.trim();
|
||
|
||
if (!command) return;
|
||
|
||
const output = document.getElementById('consoleOutput');
|
||
output.textContent += `admin@nationsglory:~$ ${command}\n`;
|
||
commandInput.value = '';
|
||
|
||
try {
|
||
const response = await fetch(`${API_URL}/rcon/command`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({ command })
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (response.ok) {
|
||
const responseText = data.response || '(pas de réponse)';
|
||
output.textContent += `${responseText}\n`;
|
||
showMessage('Commande exécutée ✓', 'success', 'dashboardMessage');
|
||
loadConsoleData(); // Rafraîchir l'historique
|
||
} else {
|
||
output.textContent += `Erreur: ${data.error}\n`;
|
||
showMessage(data.error, 'error', 'dashboardMessage');
|
||
}
|
||
|
||
output.scrollTop = output.scrollHeight;
|
||
} catch (e) {
|
||
console.error('Erreur envoi commande:', e);
|
||
output.textContent += `Erreur serveur: ${e.message}\n`;
|
||
showMessage('Erreur serveur', 'error', 'dashboardMessage');
|
||
output.scrollTop = output.scrollHeight;
|
||
}
|
||
}
|
||
|
||
// Variables globales pour gérer l'arrêt du serveur
|
||
let shutdownInProgress = false;
|
||
let shutdownCancelToken = null;
|
||
|
||
window.openSayModal = function() {
|
||
document.getElementById('sayModal').style.display = 'flex';
|
||
};
|
||
|
||
window.sendSayCommand = async function() {
|
||
const message = document.getElementById('sayInput').value.trim();
|
||
if (!message) {
|
||
alert('Entrez un message');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${API_URL}/rcon/command`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({ command: `say ${message}` })
|
||
});
|
||
|
||
if (response.ok) {
|
||
showMessage('Message envoyé ✓', 'success', 'dashboardMessage');
|
||
document.getElementById('sayInput').value = '';
|
||
document.getElementById('sayModal').style.display = 'none';
|
||
loadConsoleData();
|
||
} else {
|
||
showMessage('Erreur lors de l\'envoi du message', 'error', 'dashboardMessage');
|
||
}
|
||
} catch (e) {
|
||
console.error('Erreur envoi message:', e);
|
||
showMessage('Erreur serveur', 'error', 'dashboardMessage');
|
||
}
|
||
};
|
||
|
||
window.openShutdownModal = function() {
|
||
document.getElementById('shutdownModal').style.display = 'flex';
|
||
};
|
||
|
||
window.shutdownServer = async function() {
|
||
const cooldown = parseInt(document.getElementById('shutdownCooldown').value);
|
||
const message = document.getElementById('shutdownMessage').value.trim();
|
||
|
||
if (cooldown < 10 || cooldown > 600) {
|
||
alert('Le cooldown doit être entre 10 et 600 secondes');
|
||
return;
|
||
}
|
||
|
||
if (!message) {
|
||
alert('Entrez un message d\'avertissement');
|
||
return;
|
||
}
|
||
|
||
if (!confirm(`Êtes-vous sûr de vouloir arrêter le serveur dans ${cooldown} secondes?`)) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
shutdownInProgress = true;
|
||
shutdownCancelToken = {};
|
||
document.getElementById('cancelShutdownBtn').style.display = 'block';
|
||
document.getElementById('shutdownModal').style.display = 'none';
|
||
|
||
showMessage(`⏳ Arrêt du serveur dans ${cooldown} secondes...`, 'warning', 'dashboardMessage');
|
||
|
||
let secondsRemaining = cooldown;
|
||
|
||
while (secondsRemaining > 0 && shutdownInProgress && shutdownCancelToken) {
|
||
if (secondsRemaining % 10 === 0 || secondsRemaining <= 10) {
|
||
// Envoyer un avertissement tous les 10 secondes (ou chaque seconde dans les 10 dernières)
|
||
const warningMsg = message.replace('{cooldown}', secondsRemaining);
|
||
await fetch(`${API_URL}/rcon/command`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({ command: `say ${warningMsg}` })
|
||
}).catch(() => {});
|
||
}
|
||
|
||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||
secondsRemaining--;
|
||
}
|
||
|
||
if (!shutdownInProgress || !shutdownCancelToken) {
|
||
// Envoyer un message dans le chat pour informer de l'annulation
|
||
await fetch(`${API_URL}/rcon/command`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({ command: 'say Arrêt du serveur annulé!' })
|
||
}).catch(() => {});
|
||
|
||
showMessage('❌ Arrêt annulé', 'warning', 'dashboardMessage');
|
||
document.getElementById('cancelShutdownBtn').style.display = 'none';
|
||
document.getElementById('cancelShutdownBtnDash').style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
// Envoyer la commande d'arrêt
|
||
const response = await fetch(`${API_URL}/rcon/command`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({ command: 'stop' })
|
||
});
|
||
|
||
if (response.ok) {
|
||
showMessage('✓ Serveur arrêté', 'success', 'dashboardMessage');
|
||
} else {
|
||
showMessage('Erreur lors de l\'arrêt du serveur', 'error', 'dashboardMessage');
|
||
}
|
||
|
||
shutdownInProgress = false;
|
||
shutdownCancelToken = null;
|
||
document.getElementById('cancelShutdownBtn').style.display = 'none';
|
||
document.getElementById('cancelShutdownBtnDash').style.display = 'none';
|
||
loadConsoleData();
|
||
} catch (e) {
|
||
console.error('Erreur arrêt serveur:', e);
|
||
showMessage('Erreur serveur', 'error', 'dashboardMessage');
|
||
shutdownInProgress = false;
|
||
shutdownCancelToken = null;
|
||
document.getElementById('cancelShutdownBtn').style.display = 'none';
|
||
}
|
||
};
|
||
|
||
window.cancelShutdown = function() {
|
||
if (shutdownInProgress) {
|
||
shutdownInProgress = false;
|
||
shutdownCancelToken = null;
|
||
document.getElementById('cancelShutdownBtn').style.display = 'none';
|
||
document.getElementById('cancelShutdownBtnDash').style.display = 'none';
|
||
showMessage('❌ Arrêt annulé', 'warning', 'dashboardMessage');
|
||
}
|
||
};
|
||
|
||
// Variables globales pour gérer le redémarrage du serveur
|
||
let restartInProgress = false;
|
||
let restartCancelToken = null;
|
||
|
||
window.openRestartModal = function() {
|
||
document.getElementById('restartModal').style.display = 'flex';
|
||
};
|
||
|
||
window.restartServer = async function() {
|
||
const cooldown = parseInt(document.getElementById('restartCooldown').value);
|
||
const message = document.getElementById('restartMessage').value.trim();
|
||
|
||
if (cooldown < 10 || cooldown > 600) {
|
||
alert('Le cooldown doit être entre 10 et 600 secondes');
|
||
return;
|
||
}
|
||
|
||
if (!message) {
|
||
alert('Entrez un message d\'avertissement');
|
||
return;
|
||
}
|
||
|
||
if (!confirm(`Êtes-vous sûr de vouloir redémarrer le serveur dans ${cooldown} secondes?`)) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
restartInProgress = true;
|
||
restartCancelToken = {};
|
||
document.getElementById('cancelRestartBtn').style.display = 'block';
|
||
document.getElementById('restartModal').style.display = 'none';
|
||
|
||
showMessage(`⏳ Redémarrage du serveur dans ${cooldown} secondes...`, 'warning', 'dashboardMessage');
|
||
|
||
let secondsRemaining = cooldown;
|
||
|
||
while (secondsRemaining > 0 && restartInProgress && restartCancelToken) {
|
||
if (secondsRemaining % 10 === 0 || secondsRemaining <= 10) {
|
||
// Envoyer un avertissement tous les 10 secondes (ou chaque seconde dans les 10 dernières)
|
||
const warningMsg = message.replace('{cooldown}', secondsRemaining);
|
||
await fetch(`${API_URL}/rcon/command`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({ command: `say ${warningMsg}` })
|
||
}).catch(() => {});
|
||
}
|
||
|
||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||
secondsRemaining--;
|
||
}
|
||
|
||
if (!restartInProgress || !restartCancelToken) {
|
||
// Envoyer un message dans le chat pour informer de l'annulation
|
||
await fetch(`${API_URL}/rcon/command`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({ command: 'say Redémarrage du serveur annulé!' })
|
||
}).catch(() => {});
|
||
|
||
showMessage('❌ Redémarrage annulé', 'warning', 'dashboardMessage');
|
||
document.getElementById('cancelRestartBtn').style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
// Envoyer la commande de redémarrage
|
||
const response = await fetch(`${API_URL}/rcon/command`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({ command: 'stop' })
|
||
});
|
||
|
||
if (response.ok) {
|
||
showMessage('✓ Serveur redémarré', 'success', 'dashboardMessage');
|
||
} else {
|
||
showMessage('Erreur lors du redémarrage du serveur', 'error', 'dashboardMessage');
|
||
}
|
||
|
||
restartInProgress = false;
|
||
restartCancelToken = null;
|
||
document.getElementById('cancelRestartBtn').style.display = 'none';
|
||
loadDashboardData();
|
||
} catch (e) {
|
||
console.error('Erreur redémarrage serveur:', e);
|
||
showMessage('Erreur serveur', 'error', 'dashboardMessage');
|
||
restartInProgress = false;
|
||
restartCancelToken = null;
|
||
document.getElementById('cancelRestartBtn').style.display = 'none';
|
||
}
|
||
};
|
||
|
||
window.cancelRestart = function() {
|
||
if (restartInProgress) {
|
||
restartInProgress = false;
|
||
restartCancelToken = null;
|
||
document.getElementById('cancelRestartBtn').style.display = 'none';
|
||
showMessage('❌ Redémarrage annulé', 'warning', 'dashboardMessage');
|
||
}
|
||
};
|
||
|
||
// ========== LOGS ==========
|
||
function getLogsHTML() {
|
||
return `
|
||
<div class="section-card">
|
||
<h2>📜 Logs</h2>
|
||
<div class="btn-group">
|
||
<button class="btn-primary" onclick="reloadLogs()">🔄 Rafraîchir</button>
|
||
<button class="btn-secondary" onclick="openSearchLogsModal()">🔍 Chercher</button>
|
||
</div>
|
||
<div class="logs-container" id="logsContainer"></div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
async function loadLogsData() {
|
||
try {
|
||
const response = await fetch(`${API_URL}/logs?lines=200`, {
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
|
||
const container = document.getElementById('logsContainer');
|
||
container.innerHTML = data.logs.map((line, idx) => {
|
||
let className = '';
|
||
if (line.includes('ERROR')) className = 'error';
|
||
else if (line.includes('WARN')) className = 'warning';
|
||
|
||
return `<div class="log-line ${className}">${escapeHtml(line)}</div>`;
|
||
}).join('');
|
||
|
||
container.scrollTop = container.scrollHeight;
|
||
} catch (e) {
|
||
console.error('Erreur chargement logs:', e);
|
||
document.getElementById('logsContainer').innerHTML = '<div class="error">Erreur chargement</div>';
|
||
}
|
||
}
|
||
|
||
async function reloadLogs() {
|
||
showMessage('Rechargement...', 'success', 'dashboardMessage');
|
||
await loadLogsData();
|
||
showMessage('Logs rechargés', 'success', 'dashboardMessage');
|
||
}
|
||
|
||
function openSearchLogsModal() {
|
||
const query = prompt('Chercher dans les logs:');
|
||
if (query) searchLogs(query);
|
||
}
|
||
|
||
async function searchLogs(query) {
|
||
try {
|
||
const response = await fetch(`${API_URL}/logs/search?query=${encodeURIComponent(query)}`, {
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
|
||
const container = document.getElementById('logsContainer');
|
||
container.innerHTML = data.results.map((r, idx) => `
|
||
<div class="log-line" style="background: ${idx % 2 === 0 ? '#f8f9fa' : 'white'}; padding: 5px;">
|
||
<strong style="color: var(--accent);">[${r.index}]</strong> ${escapeHtml(r.line)}
|
||
</div>
|
||
`).join('');
|
||
} catch (e) {
|
||
console.error('Erreur recherche:', e);
|
||
}
|
||
}
|
||
|
||
// ========== JOUEURS ==========
|
||
function getPlayersHTML() {
|
||
return `
|
||
<div class="section-card">
|
||
<h2>👥 Joueurs en Ligne</h2>
|
||
<div class="btn-group" style="margin-bottom: 15px;">
|
||
<button class="btn-secondary" onclick="loadOnlinePlayers()">🔄 Rafraîchir</button>
|
||
</div>
|
||
<div id="onlinePlayersInfo" style="padding: 15px; background: #f5f5f5; border-radius: 8px; margin-bottom: 20px;">
|
||
<p style="margin: 0; font-size: 16px; font-weight: bold;">Chargement...</p>
|
||
</div>
|
||
<div id="onlinePlayersList"></div>
|
||
</div>
|
||
<div class="section-card">
|
||
<h2>📋 Tous les Joueurs</h2>
|
||
<p style="margin-bottom: 15px; color: #666;">Liste des joueurs qui se sont connectés au serveur</p>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Nom</th>
|
||
<th>UUID</th>
|
||
<th>Dernière Connexion</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="playersTable">
|
||
<tr><td colspan="3" style="text-align: center;">Chargement...</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
async function loadPlayersData() {
|
||
// Charger les joueurs en ligne
|
||
await window.loadOnlinePlayers();
|
||
|
||
// Charger tous les joueurs
|
||
try {
|
||
const response = await fetch(`${API_URL}/players`, {
|
||
credentials: 'include'
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
const table = document.getElementById('playersTable');
|
||
if (data.players && data.players.length > 0) {
|
||
table.innerHTML = data.players.map(p => `
|
||
<tr>
|
||
<td>${p.name}</td>
|
||
<td><code style="font-size: 11px;">${p.uuid}</code></td>
|
||
<td>${new Date(p.lastPlayed).toLocaleString()}</td>
|
||
</tr>
|
||
`).join('');
|
||
} else {
|
||
table.innerHTML = '<tr><td colspan="3" style="text-align: center;">Aucun joueur</td></tr>';
|
||
}
|
||
} catch (e) {
|
||
console.error('Erreur joueurs:', e);
|
||
document.getElementById('playersTable').innerHTML = `<tr><td colspan="3" style="text-align: center; color: red;">Erreur: ${e.message}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
window.loadOnlinePlayers = async function() {
|
||
console.log('Chargement des joueurs en ligne...');
|
||
|
||
const infoDiv = document.getElementById('onlinePlayersInfo');
|
||
const listDiv = document.getElementById('onlinePlayersList');
|
||
|
||
if (!infoDiv || !listDiv) {
|
||
console.error('Éléments DOM non trouvés');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
infoDiv.innerHTML = '<p style="margin: 0;">⏳ Chargement...</p>';
|
||
|
||
const response = await fetch(`${API_URL}/rcon/command`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
credentials: 'include',
|
||
body: JSON.stringify({ command: 'list' })
|
||
});
|
||
|
||
console.log('Réponse RCON reçue:', response.status);
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
const output = data.response || data.output || '';
|
||
|
||
console.log('Données reçues:', data);
|
||
console.log('Output RCON:', output);
|
||
|
||
// Supprimer les codes de couleur Minecraft (§X)
|
||
const cleanOutput = output.replace(/§[0-9a-fk-or]/gi, '');
|
||
console.log('Output nettoyé:', cleanOutput);
|
||
|
||
// Parser différents formats possibles:
|
||
// Format moderne: "There are 1/20 players online: player1, player2"
|
||
// Format 1.6.4: "There are 1 out of maximum 20 players online. Connected players: player1"
|
||
let match = cleanOutput.match(/There are (\d+)\/(\d+) players online:?\s*(.*)/i);
|
||
|
||
if (!match) {
|
||
// Essayer le format 1.6.4
|
||
match = cleanOutput.match(/There are (\d+) out of maximum (\d+) players online/i);
|
||
if (match) {
|
||
const onlineCount = parseInt(match[1]);
|
||
const maxPlayers = parseInt(match[2]);
|
||
|
||
// Chercher les noms de joueurs après "Connected players:"
|
||
const playersMatch = cleanOutput.match(/Connected players[:\s]+(.+)/i);
|
||
const playerNames = playersMatch ?
|
||
playersMatch[1].split(',').map(n => n.trim().replace(/\[AFK\]/gi, '').trim()).filter(n => n) :
|
||
[];
|
||
|
||
match = [cleanOutput, onlineCount.toString(), maxPlayers.toString(), playerNames.join(', ')];
|
||
}
|
||
}
|
||
|
||
if (match) {
|
||
const onlineCount = parseInt(match[1]);
|
||
const maxPlayers = parseInt(match[2]);
|
||
const playerNames = match[3] ? match[3].split(',').map(n => n.trim()).filter(n => n) : [];
|
||
|
||
console.log(`Joueurs: ${onlineCount}/${maxPlayers}`, playerNames);
|
||
|
||
infoDiv.innerHTML = `
|
||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||
<p style="margin: 0; font-size: 16px;">
|
||
<span style="font-weight: bold; color: ${onlineCount > 0 ? '#4CAF50' : '#666'};">
|
||
${onlineCount} joueur${onlineCount !== 1 ? 's' : ''}
|
||
</span>
|
||
en ligne sur ${maxPlayers}
|
||
</p>
|
||
<span style="font-size: 24px;">${onlineCount > 0 ? '🟢' : '⚪'}</span>
|
||
</div>
|
||
`;
|
||
|
||
if (onlineCount > 0 && playerNames.length > 0) {
|
||
listDiv.innerHTML = `
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; margin-top: 15px;">
|
||
${playerNames.map(name => `
|
||
<div style="padding: 12px; background: white; border: 2px solid #4CAF50; border-radius: 8px; text-align: center;">
|
||
<span style="font-weight: bold; color: #333;">🎮 ${name}</span>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
`;
|
||
} else {
|
||
listDiv.innerHTML = '';
|
||
}
|
||
} else {
|
||
infoDiv.innerHTML = `<p style="margin: 0; color: #666;">Aucun joueur en ligne</p>`;
|
||
listDiv.innerHTML = '';
|
||
}
|
||
} catch (e) {
|
||
console.error('Erreur chargement joueurs en ligne:', e);
|
||
infoDiv.innerHTML = `
|
||
<p style="margin: 0; color: red;">Erreur: ${e.message}</p>
|
||
`;
|
||
listDiv.innerHTML = '';
|
||
}
|
||
}
|
||
|
||
// ========== WHITELIST ==========
|
||
function getWhitelistHTML() {
|
||
return `
|
||
<div class="section-card">
|
||
<h2>✅ Whitelist</h2>
|
||
<div class="btn-group">
|
||
<button class="btn-primary" onclick="openAddWhitelistModal()">➕ Ajouter</button>
|
||
<button class="btn-secondary" onclick="reloadWhitelist()">🔄 Rafraîchir</button>
|
||
</div>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Joueur</th>
|
||
<th>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="whitelistTable">
|
||
<tr><td colspan="2" style="text-align: center;">Chargement...</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
async function loadWhitelistData() {
|
||
try {
|
||
const response = await fetch(`${API_URL}/whitelist`, {
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
|
||
const table = document.getElementById('whitelistTable');
|
||
if (data.whitelist && data.whitelist.length > 0) {
|
||
const players = data.whitelist.map(w => typeof w === 'string' ? w : w.name);
|
||
table.innerHTML = players.map(p => `
|
||
<tr>
|
||
<td>${p}</td>
|
||
<td>
|
||
<button class="btn-danger" style="padding: 5px 10px; font-size: 12px;" onclick="removeFromWhitelist('${p}')">❌ Retirer</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
} else {
|
||
table.innerHTML = '<tr><td colspan="2" style="text-align: center;">Whitelist vide</td></tr>';
|
||
}
|
||
} catch (e) {
|
||
console.error('Erreur whitelist:', e);
|
||
document.getElementById('whitelistTable').innerHTML = '<tr><td colspan="2" style="text-align: center; color: red;">Erreur</td></tr>';
|
||
}
|
||
}
|
||
|
||
function openAddWhitelistModal() {
|
||
const username = prompt('Nom du joueur à ajouter:');
|
||
if (username) addToWhitelist(username);
|
||
}
|
||
|
||
async function addToWhitelist(username) {
|
||
try {
|
||
const response = await fetch(`${API_URL}/whitelist/add`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({ username })
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
showMessage(`${username} ajouté à la whitelist`, 'success', 'dashboardMessage');
|
||
loadWhitelistData();
|
||
} else {
|
||
showMessage(data.error, 'error', 'dashboardMessage');
|
||
}
|
||
} catch (e) {
|
||
console.error('Erreur:', e);
|
||
showMessage('Erreur serveur', 'error', 'dashboardMessage');
|
||
}
|
||
}
|
||
|
||
async function removeFromWhitelist(username) {
|
||
if (!confirm(`Confirmer la suppression de ${username}?`)) return;
|
||
|
||
try {
|
||
const response = await fetch(`${API_URL}/whitelist/remove`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({ username })
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
showMessage(`${username} retiré de la whitelist`, 'success', 'dashboardMessage');
|
||
loadWhitelistData();
|
||
} else {
|
||
showMessage(data.error, 'error', 'dashboardMessage');
|
||
}
|
||
} catch (e) {
|
||
console.error('Erreur:', e);
|
||
showMessage('Erreur serveur', 'error', 'dashboardMessage');
|
||
}
|
||
}
|
||
|
||
async function reloadWhitelist() {
|
||
showMessage('Rechargement...', 'success', 'dashboardMessage');
|
||
await loadWhitelistData();
|
||
showMessage('Whitelist rechargée', 'success', 'dashboardMessage');
|
||
}
|
||
|
||
// ========== BACKUPS ==========
|
||
function getBackupsHTML() {
|
||
return `
|
||
<div class="section-card">
|
||
<h2>💾 Backups</h2>
|
||
<div class="btn-group">
|
||
<button class="btn-success" onclick="createBackup()">➕ Créer Backup</button>
|
||
<button class="btn-secondary" onclick="reloadBackups()">🔄 Rafraîchir</button>
|
||
</div>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Nom</th>
|
||
<th>Taille</th>
|
||
<th>Date</th>
|
||
<th>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="backupsTable">
|
||
<tr><td colspan="4" style="text-align: center;">Chargement...</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
async function loadBackupsData() {
|
||
try {
|
||
const response = await fetch(`${API_URL}/backup`, {
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
|
||
const table = document.getElementById('backupsTable');
|
||
if (Array.isArray(data) && data.length > 0) {
|
||
table.innerHTML = data.map(b => `
|
||
<tr>
|
||
<td>${b.name}</td>
|
||
<td>${b.size}</td>
|
||
<td>${new Date(b.created).toLocaleString()}</td>
|
||
<td>
|
||
<button class="btn-danger" style="padding: 5px 10px; font-size: 12px;" onclick="deleteBackup('${b.name}')">❌ Supprimer</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
} else {
|
||
table.innerHTML = '<tr><td colspan="4" style="text-align: center;">Aucun backup</td></tr>';
|
||
}
|
||
} catch (e) {
|
||
console.error('Erreur backups:', e);
|
||
document.getElementById('backupsTable').innerHTML = '<tr><td colspan="4" style="text-align: center; color: red;">Erreur</td></tr>';
|
||
}
|
||
}
|
||
|
||
async function createBackup() {
|
||
if (!confirm('Créer un nouveau backup? (cela peut prendre du temps)')) return;
|
||
|
||
showMessage('Création du backup en cours...', 'success', 'dashboardMessage');
|
||
|
||
try {
|
||
const response = await fetch(`${API_URL}/backup/create`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({})
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
showMessage(data.message, 'success', 'dashboardMessage');
|
||
setTimeout(() => loadBackupsData(), 2000);
|
||
} else {
|
||
showMessage(data.error, 'error', 'dashboardMessage');
|
||
}
|
||
} catch (e) {
|
||
console.error('Erreur:', e);
|
||
showMessage('Erreur serveur', 'error', 'dashboardMessage');
|
||
}
|
||
}
|
||
|
||
async function deleteBackup(filename) {
|
||
if (!confirm(`Confirmer la suppression du backup ${filename}?`)) return;
|
||
|
||
try {
|
||
const response = await fetch(`${API_URL}/backup/delete/${filename}`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({})
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
showMessage('Backup supprimé', 'success', 'dashboardMessage');
|
||
loadBackupsData();
|
||
} else {
|
||
showMessage(data.error, 'error', 'dashboardMessage');
|
||
}
|
||
} catch (e) {
|
||
console.error('Erreur:', e);
|
||
showMessage('Erreur serveur', 'error', 'dashboardMessage');
|
||
}
|
||
}
|
||
|
||
async function reloadBackups() {
|
||
showMessage('Rechargement...', 'success', 'dashboardMessage');
|
||
await loadBackupsData();
|
||
showMessage('Backups rechargés', 'success', 'dashboardMessage');
|
||
}
|
||
|
||
// ========== PARAMÈTRES ==========
|
||
function getSettingsHTML() {
|
||
return `
|
||
<div class="section-card">
|
||
<h2>⚙️ Paramètres du Serveur</h2>
|
||
<p style="margin-bottom: 15px; color: #666;">Modifiez les paramètres du serveur en temps réel</p>
|
||
<div style="margin-bottom: 15px;">
|
||
<button class="btn-secondary" onclick="loadSettingsData()">🔄 Rafraîchir</button>
|
||
</div>
|
||
<table id="settingsTable" style="width: 100%;">
|
||
<thead>
|
||
<tr>
|
||
<th style="text-align: left; width: 30%; padding: 10px; border-bottom: 2px solid #ddd;">Paramètre</th>
|
||
<th style="text-align: left; width: 70%; padding: 10px; border-bottom: 2px solid #ddd;">Valeur</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr><td colspan="2" style="text-align: center; padding: 20px;">Chargement...</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
async function loadSettingsData() {
|
||
try {
|
||
const response = await fetch(`${API_URL}/server`, {
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
|
||
const table = document.getElementById('settingsTable');
|
||
const tbody = table.querySelector('tbody');
|
||
|
||
if (data.properties) {
|
||
const keys = Object.keys(data.properties).sort();
|
||
tbody.innerHTML = keys.map(k => {
|
||
const isPassword = k.toLowerCase().includes('password') || k.toLowerCase().includes('rcon');
|
||
const inputType = isPassword ? 'password' : 'text';
|
||
const value = data.properties[k];
|
||
|
||
return `
|
||
<tr style="border-bottom: 1px solid #eee;">
|
||
<td style="padding: 12px; font-weight: bold;">${k}</td>
|
||
<td style="padding: 12px;">
|
||
<input
|
||
type="${inputType}"
|
||
value="${escapeHtml(value)}"
|
||
data-key="${k}"
|
||
onchange="updateSetting(this)"
|
||
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; font-family: monospace;"
|
||
>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
}
|
||
} catch (e) {
|
||
console.error('Erreur paramètres:', e);
|
||
}
|
||
}
|
||
|
||
async function updateSetting(input) {
|
||
const key = input.getAttribute('data-key');
|
||
const newValue = input.value;
|
||
|
||
try {
|
||
const response = await fetch(`${API_URL}/server/settings`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({ key, value: newValue })
|
||
});
|
||
|
||
if (response.ok) {
|
||
showMessage(`✓ Paramètre ${key} modifié`, 'success', 'dashboardMessage');
|
||
} else {
|
||
const error = await response.json();
|
||
showMessage(`Erreur: ${error.error}`, 'error', 'dashboardMessage');
|
||
await loadSettingsData(); // Recharger pour annuler la modification
|
||
}
|
||
} catch (e) {
|
||
console.error('Erreur modification paramètre:', e);
|
||
showMessage('Erreur serveur', 'error', 'dashboardMessage');
|
||
}
|
||
}
|
||
|
||
// ========== ACTIONS ==========
|
||
|
||
async function restartServer() {
|
||
if (!confirm('⚠️ Redémarrer le serveur? Les joueurs seront déconnectés.')) return;
|
||
|
||
showMessage('Redémarrage du serveur...', 'success', 'dashboardMessage');
|
||
|
||
try {
|
||
const response = await fetch(`${API_URL}/rcon/restart`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({})
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
showMessage(data.message, 'success', 'dashboardMessage');
|
||
} else {
|
||
showMessage(data.error, 'error', 'dashboardMessage');
|
||
}
|
||
} catch (e) {
|
||
console.error('Erreur:', e);
|
||
showMessage('Erreur serveur', 'error', 'dashboardMessage');
|
||
}
|
||
}
|
||
|
||
async function saveServer() {
|
||
showMessage('Sauvegarde du serveur...', 'success', 'dashboardMessage');
|
||
|
||
try {
|
||
const response = await fetch(`${API_URL}/rcon/save`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({})
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
showMessage(data.message, 'success', 'dashboardMessage');
|
||
} else {
|
||
showMessage(data.error, 'error', 'dashboardMessage');
|
||
}
|
||
} catch (e) {
|
||
console.error('Erreur:', e);
|
||
showMessage('Erreur serveur', 'error', 'dashboardMessage');
|
||
}
|
||
}
|
||
|
||
function openChangeRconPasswordModal() {
|
||
const oldPassword = prompt('Ancien mot de passe RCON:');
|
||
if (!oldPassword) return;
|
||
|
||
const newPassword = prompt('Nouveau mot de passe RCON:');
|
||
if (!newPassword) return;
|
||
|
||
changeRconPassword(oldPassword, newPassword);
|
||
}
|
||
|
||
async function changeRconPassword(oldPassword, newPassword) {
|
||
try {
|
||
const response = await fetch(`${API_URL}/auth/change-rcon-password`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({ oldPassword, newPassword })
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
showMessage(data.message, 'success', 'dashboardMessage');
|
||
} else {
|
||
showMessage(data.error, 'error', 'dashboardMessage');
|
||
}
|
||
} catch (e) {
|
||
console.error('Erreur:', e);
|
||
showMessage('Erreur serveur', 'error', 'dashboardMessage');
|
||
}
|
||
}
|
||
|
||
// ========== UTILITAIRES ==========
|
||
|
||
function showMessage(text, type, elementId) {
|
||
const element = document.getElementById(elementId);
|
||
if (element) {
|
||
element.textContent = text;
|
||
element.className = `message show ${type}`;
|
||
setTimeout(() => {
|
||
element.classList.remove('show');
|
||
}, 4000);
|
||
}
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
const map = {
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": '''
|
||
};
|
||
return text.replace(/[&<>"']/g, m => map[m]);
|
||
}
|
||
|
||
// Déconnexion
|
||
async function handleLogout() {
|
||
try {
|
||
await fetch(`${API_URL}/auth/logout`, {
|
||
method: 'POST',
|
||
credentials: 'include'
|
||
});
|
||
checkAuth();
|
||
} catch (e) {
|
||
console.error('Erreur:', e);
|
||
}
|
||
}
|