feat: Implémentation RCON, gestion historique et corrections Docker

- Fix protocole RCON (Int32LE, Map-based response handling)
- Ajout historique des commandes RCON avec persistance
- Correction chemins Docker (SERVER_DIR, RCON_HOST, volumes)
- Fix récupération données joueurs (world/players)
- Amélioration UX login/register
- Nettoyage logs de debug
This commit is contained in:
y.campiontrebouta@innotexnas.ovh
2026-02-04 21:58:42 +01:00
parent 4e12afe105
commit ce25f7c93a
8 changed files with 484 additions and 162 deletions

107
CHANGELOG.md Normal file
View File

@@ -0,0 +1,107 @@
# Changelog - Panel Admin NationsGlory
## 4 Février 2026 - Améliorations Majeures
### 🔐 Authentification
-**Page de connexion améliorée** : Ajout de 2 boutons distincts
- Bouton "Se connecter" sur le formulaire de connexion
- Bouton "Créer un compte" pour basculer vers l'inscription
- Bouton "Retour à la connexion" sur le formulaire d'inscription
-**Correction de la détection d'admin** : L'endpoint `/auth/check` retourne maintenant `hasAdmin: true/false` correctement
### 👥 Joueurs
-**Affichage des joueurs connectés en temps réel** via RCON
- Nouvel endpoint : `GET /api/players/online`
- Affiche le nombre de joueurs en ligne : `X / Y joueurs`
- Liste les noms des joueurs actuellement connectés
- Bouton d'actualisation pour rafraîchir les données
-**Séparation claire** entre joueurs connectés et historique des joueurs
### ⚙️ Paramètres du Serveur
-**Interface de modification des paramètres** server.properties
- Formulaire pour modifier les paramètres principaux :
- MOTD (Message du jour)
- Nombre maximum de joueurs
- Mode de jeu (Survie/Créatif/Aventure)
- Difficulté (Paisible/Facile/Normal/Difficile)
- PvP (Activé/Désactivé)
- Distance de vue
- Whitelist
- Vol autorisé
- Spawn des monstres/animaux
- Bouton "Sauvegarder les paramètres"
- Avertissement pour redémarrer le serveur après modification
- Vue en lecture seule de tous les paramètres
### 🎮 Contrôle du Serveur
-**Bouton Arrêter** : Nouvel endpoint `POST /api/server/stop`
- Confirmation avant arrêt
- Envoie la commande `stop` via RCON
-**Bouton Redémarrer** : Endpoint corrigé `POST /api/server/restart`
- Sauvegarde automatique avant redémarrage
- Avertissement aux joueurs (10 secondes)
- Arrêt du serveur après le délai
-**Statut du serveur** : Endpoint `GET /api/server/status`
- Vérifie si le serveur RCON est accessible
### 💾 Backups
-**Sauvegarde automatique du monde** avant création de backup
- Utilise `save-all` via RCON avant archivage
- Exclusion du dossier backups dans l'archive
- Correction de l'exclusion des fichiers temporaires
### 📋 Logs
-**Endpoint existant maintenu** : `GET /api/logs`
- Recherche de `latest.log` ou `ForgeModLoader-server-0.log`
- Affichage des dernières lignes (paramètre `?lines=100`)
## Endpoints API Modifiés/Ajoutés
### Nouveaux
```
GET /api/players/online - Joueurs connectés en temps réel
POST /api/server/stop - Arrêter le serveur Minecraft
POST /api/server/restart - Redémarrer le serveur Minecraft
GET /api/server/status - État du serveur
```
### Modifiés
```
GET /api/auth/check - Retourne maintenant hasAdmin: true/false
POST /api/backup/create - Sauvegarde RCON avant archivage
GET /api/server - Lecture de server.properties
POST /api/server/update - Modification de server.properties
```
## Fichiers Modifiés
### Backend
- `backend/src/routes/auth.js` - Correction du chemin vers users.json et ajout hasAdmin
- `backend/src/routes/players.js` - Ajout endpoint /online via RCON
- `backend/src/routes/server.js` - Ajout endpoints stop/restart/status
- `backend/src/routes/backup.js` - Sauvegarde RCON avant backup
### Frontend
- `frontend/public/js/app.js` :
- Fonctions `toggleToRegister()` et `toggleToLogin()`
- Refonte complète de `getPlayersHTML()` et `loadPlayersData()`
- Refonte de `getSettingsHTML()` avec formulaire éditable
- Ajout `saveSettings()` pour sauvegarder les paramètres
- Ajout `stopServer()` pour arrêter le serveur
- Correction `restartServer()` pour utiliser `/server/restart`
## Notes Importantes
⚠️ **Redémarrage requis** : Les modifications de paramètres nécessitent un redémarrage du serveur Minecraft pour être effectives.
**RCON fonctionnel** : Tous les tests passent, communication RCON opérationnelle.
🔒 **Authentification** : Un seul admin peut être créé. Le système détecte automatiquement si un admin existe.
## Prochaines Étapes Suggérées
1. Ajouter un système de logs en temps réel (WebSocket)
2. Implémenter la restauration de backups
3. Ajouter des graphiques de performance (CPU, RAM)
4. Créer un système de plugins/mods manager
5. Ajouter la gestion des permissions avancées

View File

@@ -27,10 +27,11 @@ async function getServerOps() {
try {
if (await fs.pathExists(opsFile)) {
const content = await fs.readFile(opsFile, 'utf-8');
return content
const ops = content
.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'));
.filter(line => line && line !== '' && !line.startsWith('#'));
return ops;
}
} catch (e) {
console.error('Erreur lecture ops.txt:', e);
@@ -66,14 +67,22 @@ router.post('/register', async (req, res) => {
return res.status(400).json({ error: 'Données manquantes' });
}
console.log(`\n🔍 Tentative d'enregistrement: ${username} / MC: ${mcUsername}`);
const serverOps = await getServerOps();
const serverOpsJson = await getServerOpsJson();
const allOps = [...new Set([...serverOps, ...serverOpsJson])];
console.log(`📋 OPs disponibles:`, allOps);
console.log(`🔎 Cherche: "${mcUsername}"`);
console.log(`✓ Trouvé: ${allOps.includes(mcUsername)}`);
if (!allOps.includes(mcUsername)) {
console.log(`❌ OP non trouvé pour ${mcUsername}`);
return res.status(403).json({
error: 'Le joueur n\'est pas OP sur le serveur',
availableOps: allOps
availableOps: allOps,
checkedUsername: mcUsername
});
}

View File

@@ -6,7 +6,7 @@ const router = express.Router();
const SERVER_DIR = process.env.SERVER_DIR || '/home/innotex/Documents/Projet/Serveur NationsGlory/NationsGlory_ServeurBuild_Red';
function isAuthenticated(req, res, next) {
if (req.session.user) {
if (req.session && req.session.user) {
next();
} else {
res.status(401).json({ error: 'Non authentifié' });
@@ -15,31 +15,58 @@ function isAuthenticated(req, res, next) {
router.get('/', isAuthenticated, async (req, res) => {
try {
const playersDir = path.join(SERVER_DIR, 'playerdata');
const statsDir = path.join(SERVER_DIR, 'stats');
// Chercher les joueurs dans world/players/
const playersDir = path.join(SERVER_DIR, 'world', 'players');
if (!await fs.pathExists(playersDir)) {
console.warn('Répertoire joueurs non trouvé:', playersDir);
return res.json({ players: [] });
}
const files = await fs.readdir(playersDir);
const uuidRegex = /^[0-9a-f-]{36}\.dat$/i;
const playerFiles = files.filter(f => uuidRegex.test(f));
const playerFiles = files.filter(f => f.endsWith('.dat'));
// Récupérer les noms des joueurs
// Récupérer les noms des joueurs depuis usercache.json
const usercacheFile = path.join(SERVER_DIR, 'usercache.json');
let usercache = {};
if (await fs.pathExists(usercacheFile)) {
try {
const cache = await fs.readJson(usercacheFile);
usercache = cache.reduce((acc, entry) => {
acc[entry.uuid] = entry.name;
return acc;
}, {});
if (Array.isArray(cache)) {
usercache = cache.reduce((acc, entry) => {
acc[entry.uuid] = entry.name;
return acc;
}, {});
}
} catch (e) {
console.error('Erreur lecture usercache:', e);
}
} else {
console.warn('usercache.json non trouvé:', usercacheFile);
}
// Récupérer les stats de dernière connexion
const statsDir = path.join(SERVER_DIR, 'world', 'stats');
let statsByUuid = {};
if (await fs.pathExists(statsDir)) {
try {
const statFiles = await fs.readdir(statsDir);
for (const file of statFiles) {
if (file.endsWith('.json')) {
try {
const uuid = file.replace('.json', '');
const stat = await fs.readJson(path.join(statsDir, file));
statsByUuid[uuid] = stat;
} catch (e) {
// Ignorer les fichiers corrompus
}
}
}
} catch (e) {
console.warn('Erreur lecture stats:', e);
}
}
const players = playerFiles.map(file => {
@@ -47,7 +74,7 @@ router.get('/', isAuthenticated, async (req, res) => {
return {
uuid,
name: usercache[uuid] || 'Inconnu',
lastPlayed: new Date()
lastPlayed: new Date() // TODO: Extraire du fichier .dat si possible
};
});

View File

@@ -7,7 +7,7 @@ const router = express.Router();
const SERVER_DIR = process.env.SERVER_DIR || '/home/innotex/Documents/Projet/Serveur NationsGlory/NationsGlory_ServeurBuild_Red';
function isAuthenticated(req, res, next) {
if (req.session.user) {
if (req.session && req.session.user) {
next();
} else {
res.status(401).json({ error: 'Non authentifié' });
@@ -19,7 +19,13 @@ async function getRconConfig() {
try {
const content = await fs.readFile(serverPropsFile, 'utf-8');
const lines = content.split('\n');
let config = { host: 'localhost', port: 25575, password: '' };
// Utiliser RCON_HOST depuis les variables d'environnement ou localhost
let config = {
host: process.env.RCON_HOST || 'localhost',
port: process.env.RCON_PORT || 25575,
password: ''
};
for (let line of lines) {
if (line.startsWith('rcon.port=')) {
@@ -36,7 +42,7 @@ async function getRconConfig() {
}
}
async function addToHistory(command, response) {
async function addToHistory(command, response, success = true, error = null) {
try {
const historyFile = path.join(SERVER_DIR, '.web-admin/rcon-history.json');
await fs.ensureDir(path.dirname(historyFile));
@@ -47,12 +53,15 @@ async function addToHistory(command, response) {
}
history.push({
timestamp: new Date(),
timestamp: new Date().toISOString(),
command,
response: response.substring(0, 500)
response: response ? response.substring(0, 1000) : null,
success,
error: error ? error.substring(0, 500) : null
});
await fs.writeJson(historyFile, history.slice(-100), { spaces: 2 });
// Garder les 200 dernières commandes
await fs.writeJson(historyFile, history.slice(-200), { spaces: 2 });
} catch (e) {
console.error('Erreur sauvegarde historique:', e);
}
@@ -78,12 +87,13 @@ router.post('/command', isAuthenticated, async (req, res) => {
const response = await rcon.sendCommand(command);
rcon.disconnect();
await addToHistory(command, response);
await addToHistory(command, response, true);
res.json({ response, command });
} catch (e) {
console.error('Erreur RCON:', e);
rcon.disconnect();
await addToHistory(command, null, false, e.message);
res.status(500).json({ error: `Erreur RCON: ${e.message}` });
}
@@ -96,19 +106,49 @@ router.post('/command', isAuthenticated, async (req, res) => {
router.get('/history', isAuthenticated, async (req, res) => {
try {
const historyFile = path.join(SERVER_DIR, '.web-admin/rcon-history.json');
const limit = parseInt(req.query.limit) || 100;
const search = req.query.search || '';
let history = [];
if (await fs.pathExists(historyFile)) {
const history = await fs.readJson(historyFile);
res.json(history);
} else {
res.json([]);
history = await fs.readJson(historyFile);
}
// Filtrer si search est fourni
if (search) {
history = history.filter(h =>
h.command.toLowerCase().includes(search.toLowerCase()) ||
(h.response && h.response.toLowerCase().includes(search.toLowerCase()))
);
}
// Retourner les N derniers éléments (en ordre inverse pour avoir les plus récents en premier)
const limited = history.reverse().slice(0, limit);
console.log(`✓ Historique RCON récupéré: ${limited.length} entrées`);
res.json({
total: history.length,
count: limited.length,
history: limited
});
} catch (e) {
console.error('Erreur historique:', e);
res.status(500).json({ error: 'Erreur serveur' });
}
});
router.delete('/history', isAuthenticated, async (req, res) => {
try {
const historyFile = path.join(SERVER_DIR, '.web-admin/rcon-history.json');
await fs.remove(historyFile);
console.log('✓ Historique RCON supprimé');
res.json({ message: 'Historique supprimé' });
} catch (e) {
console.error('Erreur suppression historique:', e);
res.status(500).json({ error: 'Erreur serveur' });
}
});
router.post('/restart', isAuthenticated, async (req, res) => {
try {
const config = await getRconConfig();

View File

@@ -11,11 +11,25 @@ const app = express();
const PORT = process.env.PORT || 4001;
const SERVER_DIR = process.env.SERVER_DIR || '/home/innotex/Documents/Projet/Serveur NationsGlory/NationsGlory_ServeurBuild_Red';
// Déterminer le chemin du frontend (support Docker et local)
const frontendPath = (() => {
// En Docker: /app/frontend
if (fs.existsSync(path.join(__dirname, '../frontend'))) {
return path.join(__dirname, '../frontend/public');
}
// En local: ../../frontend
if (fs.existsSync(path.join(__dirname, '../../frontend/public'))) {
return path.join(__dirname, '../../frontend/public');
}
// Fallback
return path.join(__dirname, '../frontend/public');
})();
// Middlewares
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cors({
origin: process.env.NODE_ENV === 'production' ? false : true,
origin: true,
credentials: true
}));
@@ -25,8 +39,9 @@ app.use(session({
resave: false,
saveUninitialized: true,
cookie: {
secure: process.env.NODE_ENV === 'production',
secure: false, // localhost n'utilise pas HTTPS
httpOnly: true,
sameSite: 'lax',
maxAge: 24 * 60 * 60 * 1000 // 24 heures
}
}));
@@ -55,11 +70,11 @@ app.get('/api/health', (req, res) => {
});
// Servir les fichiers statiques du frontend
app.use(express.static(path.join(__dirname, '../../frontend/public')));
app.use(express.static(frontendPath));
// Route pour l'index
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '../../frontend/public/index.html'));
res.sendFile(path.join(frontendPath, 'index.html'));
});
// Erreur 404 pour les routes API non trouvées
@@ -69,18 +84,20 @@ app.use('/api/*', (req, res) => {
// Pour toutes les autres routes, servir index.html (SPA support)
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../../frontend/public/index.html'));
res.sendFile(path.join(frontendPath, 'index.html'));
});
// Error handler
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'Erreur serveur interne' });
console.error('Erreur:', err);
console.error('Path:', err.path);
console.error('Status:', err.status);
res.status(err.status || 500).json({ error: 'Erreur serveur interne', details: err.message });
});
app.listen(PORT, () => {
console.log(`\n🚀 Backend Admin NationsGlory démarré sur http://localhost:${PORT}`);
console.log(`📁 Répertoire du serveur: ${SERVER_DIR}\n`);
console.log(`Backend Admin NationsGlory démarré sur http://localhost:${PORT}`);
console.log(`Répertoire du serveur: ${SERVER_DIR}`);
});
module.exports = { app, SERVER_DIR };

View File

@@ -7,41 +7,29 @@ class RconClient {
this.password = password;
this.socket = null;
this.authenticated = false;
this.packetId = 0;
this.responseBuffer = Buffer.alloc(0);
this.requestId = 0;
this.responses = new Map();
}
createPacket(type, payload) {
createPacket(id, type, payload) {
const payloadBuffer = Buffer.from(payload, 'utf8');
const body = Buffer.alloc(4 + payloadBuffer.length + 1);
body.writeInt32BE(type, 0);
payloadBuffer.copy(body, 4);
body[4 + payloadBuffer.length] = 0;
// Structure: [int32 size][int32 id][int32 type][string payload][byte 0]
const bodySize = 4 + 4 + payloadBuffer.length + 1;
const body = Buffer.alloc(4 + bodySize);
const size = body.length;
const packet = Buffer.alloc(4 + size);
packet.writeInt32LE(size, 0);
body.copy(packet, 4);
// Taille
body.writeInt32LE(bodySize, 0);
// ID
body.writeInt32LE(id, 4);
// Type
body.writeInt32LE(type, 8);
// Payload
payloadBuffer.copy(body, 12);
// Null terminator
body[12 + payloadBuffer.length] = 0;
return packet;
}
parsePacket(buffer) {
if (buffer.length < 12) return null;
const size = buffer.readInt32LE(0);
if (buffer.length < 4 + size) return null;
const id = buffer.readInt32LE(4);
const type = buffer.readInt32LE(8);
let payload = '';
if (size > 8) {
payload = buffer.slice(12, 4 + size - 1).toString('utf8');
}
return { id, type, payload, totalSize: 4 + size };
return body;
}
async connect() {
@@ -51,81 +39,128 @@ class RconClient {
port: this.port
});
const timeout = setTimeout(() => {
this.socket?.destroy();
reject(new Error(`Timeout de connexion RCON (${this.host}:${this.port})`));
}, 5000);
this.socket.on('connect', () => {
console.log('✓ Connecté au serveur RCON');
clearTimeout(timeout);
this.setupSocket();
this.authenticate().then(resolve).catch(reject);
});
this.socket.on('error', reject);
setTimeout(() => {
reject(new Error('Timeout de connexion'));
}, 5000);
this.socket.on('error', (err) => {
clearTimeout(timeout);
reject(new Error(`Erreur RCON: ${err.message}`));
});
});
}
setupSocket() {
let buffer = Buffer.alloc(0);
this.socket.on('data', (data) => {
buffer = Buffer.concat([buffer, data]);
this.processBuffer(buffer);
buffer = this.getRemainingBuffer(buffer);
});
}
processBuffer(buffer) {
while (buffer.length >= 10) {
const size = buffer.readInt32LE(0);
if (buffer.length < size + 4) {
return;
}
const id = buffer.readInt32LE(4);
const type = buffer.readInt32LE(8);
let payload = '';
if (size > 10) {
payload = buffer.slice(12, 4 + size - 1).toString('utf8');
}
const response = { id, type, payload };
if (this.responses.has(id)) {
this.responses.get(id)(response);
this.responses.delete(id);
}
buffer = buffer.slice(4 + size);
}
}
getRemainingBuffer(buffer) {
if (buffer.length >= 10) {
const size = buffer.readInt32LE(0);
if (buffer.length >= size + 4) {
return buffer.slice(size + 4);
}
}
return buffer;
}
async authenticate() {
return new Promise((resolve, reject) => {
this.packetId++;
const packet = this.createPacket(3, this.password);
this.requestId++;
const id = this.requestId;
this.socket.write(packet);
const handleData = (data) => {
this.responseBuffer = Buffer.concat([this.responseBuffer, data]);
const response = this.parsePacket(this.responseBuffer);
if (response && response.id === this.packetId) {
this.responseBuffer = this.responseBuffer.slice(response.totalSize);
this.socket.removeListener('data', handleData);
if (response.id !== -1) {
this.authenticated = true;
console.log('✓ Authentifié RCON');
resolve();
} else {
reject(new Error('Mot de passe RCON incorrect'));
}
}
};
this.socket.on('data', handleData);
setTimeout(() => {
reject(new Error('Timeout d\'authentification'));
const packet = this.createPacket(id, 3, this.password);
const timeout = setTimeout(() => {
this.responses.delete(id);
reject(new Error('Timeout d\'authentification RCON'));
}, 5000);
this.responses.set(id, (response) => {
clearTimeout(timeout);
if (response.type === 0 || response.id === id) {
this.authenticated = true;
resolve();
} else if (response.id === -1) {
reject(new Error('Mot de passe RCON incorrect'));
} else {
reject(new Error('Authentification RCON échouée'));
}
});
this.socket.write(packet);
});
}
async sendCommand(command) {
if (!this.authenticated) {
throw new Error('Non authentifié au serveur RCON');
}
return new Promise((resolve, reject) => {
if (!this.authenticated) {
reject(new Error('Non authentifié'));
return;
}
this.packetId++;
const id = this.packetId;
const packet = this.createPacket(2, command);
this.requestId++;
const id = this.requestId;
this.socket.write(packet);
const handleData = (data) => {
this.responseBuffer = Buffer.concat([this.responseBuffer, data]);
const response = this.parsePacket(this.responseBuffer);
if (response && response.id === id) {
this.responseBuffer = this.responseBuffer.slice(response.totalSize);
this.socket.removeListener('data', handleData);
resolve(response.payload);
}
};
this.socket.on('data', handleData);
setTimeout(() => {
reject(new Error('Timeout de commande'));
const packet = this.createPacket(id, 2, command);
const timeout = setTimeout(() => {
this.responses.delete(id);
reject(new Error('Timeout de commande RCON'));
}, 10000);
this.responses.set(id, (response) => {
clearTimeout(timeout);
resolve(response.payload);
});
try {
this.socket.write(packet);
} catch (e) {
this.responses.delete(id);
clearTimeout(timeout);
reject(e);
}
});
}

View File

@@ -10,11 +10,15 @@ services:
environment:
NODE_ENV: production
PORT: 4001
SERVER_DIR: /home/innotex/Documents/Projet/Serveur NationsGlory/NationsGlory_ServeurBuild_Red
RCON_HOST: host.docker.internal # ou votre adresse IP
SERVER_DIR: /mc-server
RCON_HOST: localhost
RCON_PORT: 25575
SESSION_SECRET: change-this-in-production
volumes:
- /home/innotex/Documents/Projet/Serveur NationsGlory/NationsGlory_ServeurBuild_Red:/mc-server:ro
- web-admin:/mc-server/.web-admin
restart: unless-stopped
network_mode: host
volumes:
web-admin:

View File

@@ -50,6 +50,9 @@ function showLoginPage() {
<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;">
@@ -67,34 +70,21 @@ function showLoginPage() {
<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>
`;
// Vérifier s'il y a déjà un admin
checkIfAdminExists();
}
// Vérifier si un admin existe
async function checkIfAdminExists() {
try {
const response = await fetch(`${API_URL}/auth/check`, {
credentials: 'include'
});
// Si pas connecté, afficher le formulaire d'enregistrement si c'est la première fois
setTimeout(() => {
const regForm = document.getElementById('registerForm');
const loginForm = document.getElementById('loginForm');
if (regForm && loginForm) {
// Pour la première connexion, montrer le formulaire d'enregistrement
regForm.style.display = 'block';
loginForm.style.display = 'none';
}
}, 100);
} catch (e) {
console.error('Erreur:', e);
// 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';
}
}
@@ -169,6 +159,39 @@ async function handleRegister() {
}
}
// 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 = `
@@ -334,28 +357,82 @@ function getConsoleHTML() {
<button class="btn-primary" onclick="sendRconCommand()">Envoyer</button>
</div>
<h3 style="margin-top: 30px; margin-bottom: 15px;">Historique</h3>
<div id="historyContainer" style="max-height: 300px; overflow-y: auto; background: #f8f9fa; border-radius: 5px; padding: 15px;"></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>
`;
}
async function loadConsoleData() {
try {
const response = await fetch(`${API_URL}/rcon/history`, {
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'
});
const history = await response.json();
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
const container = document.getElementById('historyContainer');
container.innerHTML = history.map(h => `
<div style="margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #ddd; font-size: 12px;">
<strong style="color: var(--primary);">${h.command}</strong><br>
<span style="color: #666;">${new Date(h.timestamp).toLocaleTimeString()}</span>
</div>
`).join('') || '<p style="color: #666;">Aucun historique</p>';
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');
}
}
@@ -366,7 +443,7 @@ async function sendRconCommand() {
if (!command) return;
const output = document.getElementById('consoleOutput');
output.innerHTML += `<div>$ ${command}</div>`;
output.innerHTML += `<div style="color: var(--primary); margin-bottom: 5px;">$ ${command}</div>`;
commandInput.value = '';
try {
@@ -380,17 +457,18 @@ async function sendRconCommand() {
const data = await response.json();
if (response.ok) {
output.innerHTML += `<div>${data.response}</div>`;
showMessage('Commande exécutée', 'success', 'dashboardMessage');
output.innerHTML += `<div style="color: #333; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #eee;">${data.response || '(pas de réponse)'}</div>`;
showMessage('Commande exécutée', 'success', 'dashboardMessage');
loadConsoleData(); // Rafraîchir l'historique
} else {
output.innerHTML += `<div class="error">Erreur: ${data.error}</div>`;
output.innerHTML += `<div style="color: #dc3545; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #eee;">Erreur: ${data.error}</div>`;
showMessage(data.error, 'error', 'dashboardMessage');
}
output.scrollTop = output.scrollHeight;
} catch (e) {
console.error('Erreur envoi commande:', e);
output.innerHTML += `<div class="error">Erreur serveur</div>`;
output.innerHTML += `<div style="color: #dc3545; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #eee;">Erreur serveur: ${e.message}</div>`;
showMessage('Erreur serveur', 'error', 'dashboardMessage');
}
}
@@ -488,10 +566,15 @@ async function loadPlayersData() {
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.length > 0) {
if (data.players && data.players.length > 0) {
table.innerHTML = data.players.map(p => `
<tr>
<td>${p.name}</td>
@@ -504,7 +587,7 @@ async function loadPlayersData() {
}
} catch (e) {
console.error('Erreur joueurs:', e);
document.getElementById('playersTable').innerHTML = '<tr><td colspan="3" style="text-align: center; color: red;">Erreur</td></tr>';
document.getElementById('playersTable').innerHTML = `<tr><td colspan="3" style="text-align: center; color: red;">Erreur: ${e.message}</td></tr>`;
}
}