feat: Complete Admin Dashboard with game control and player management (v0.0.8-alpha)

## Major Features Added
  - **🎛️ Complete Admin Dashboard**: Real-time player monitoring with detailed stats
  - **👥 Player Management**: Individual and mass player kicking with proper notifications
  - **🎯 Global Round Control**: Advance/retreat rounds across all rooms simultaneously
  - **⏸️ Game Control**: Pause/resume games from admin interface
  - **🔔 Client Notifications**: Players receive alerts for kicks and round changes

  ## Technical Improvements
  - **🏗️ Official Colyseus API**: Replaced global variable hacks with `matchMaker.query()` and `matchMaker.remoteRoomCall()`
  - **📡 Proper Client Communication**: Implemented broadcast messages for `adminKicked`, `gamePaused`, `gameResumed`, `roundChanged`
  - **🎮 Enhanced GameRoom Methods**: Added `pauseGame()`, `resumeGame()`, `advanceRound()`, `previousRound()`, `_forceClientDisconnect()`, `_forceDisconnectAllClients()`, `getInspectData()`

  ## UI/UX Enhancements
  - **📊 Detailed Player Info**: Name, room, role, producer type, and current tokens (🦃🌽)
  - **🚫 Proper Kick Notifications**: Clients auto-redirect to home with clear messaging
  - **🎨 Improved Admin Interface**: Better organized controls for non-technical commentators
  - **📱 Responsive Design**: Works well on different screen sizes

  ## Bug Fixes
  - **🔧 Fixed Admin Service URLs**: Now correctly calls Colyseus server (port 2567) instead of admin server (port 3001)
  - ** Mass Kick Notifications**: All players receive proper notifications when expelled en masse
  - **🔄 Auto-redirect**: Kicked clients properly return to home screen

  ## Architecture
  - **🏗️ Clean API Design**: All admin endpoints use official Colyseus patterns
  - **🔒 Type Safety**: Maintained TypeScript sync between server and clients
  - **📦 Microservices Ready**: Separated concerns between game server and admin interface

  **Breaking Changes:** None - fully backward compatible
  **Migration:** No migration needed
This commit is contained in:
2025-07-04 17:43:28 -06:00
parent 656cf7988e
commit eb6d19906b
26 changed files with 6123 additions and 1000 deletions

172
admin/server.js Normal file
View File

@@ -0,0 +1,172 @@
const express = require('express');
const path = require('path');
const dotenv = require('dotenv');
// Load environment variables
const ENV = process.env.NODE_ENV || 'development';
dotenv.config({ path: `.env.${ENV}` });
const app = express();
const PORT = process.env.PORT || 3002;
// Parse JSON bodies
app.use(express.json());
// Health check endpoint
app.get('/health', (req, res) => {
console.log('que pedos');
res.json({
status: 'healthy',
service: 'snatchgame-admin',
environment: ENV,
serverUrl: process.env.SERVER_URL
});
});
// API endpoint to get environment config for client
app.get('/api/config', (req, res) => {
res.json({
serverUrl: process.env.SERVER_URL,
environment: ENV
});
});
// SSE endpoint for real-time updates
app.get('/api/sse', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Cache-Control'
});
// Send initial connection message
res.write('data: {"type": "connected", "message": "SSE connection established"}\n\n');
// Set up polling interval for game state updates
const pollInterval = setInterval(async () => {
try {
// Fetch game state from Colyseus server
const gameServerUrl = process.env.SERVER_URL || 'http://localhost:2567';
const response = await fetch(`${gameServerUrl}/api/admin/stats`);
if (response.ok) {
const gameStats = await response.json();
res.write(`data: ${JSON.stringify({
type: 'gameStats',
timestamp: new Date().toISOString(),
data: gameStats
})}\n\n`);
} else {
// Send error status if server is not reachable
res.write(`data: ${JSON.stringify({
type: 'error',
timestamp: new Date().toISOString(),
message: 'Cannot connect to game server'
})}\n\n`);
}
} catch (error) {
console.error('Error fetching game stats:', error);
res.write(`data: ${JSON.stringify({
type: 'error',
timestamp: new Date().toISOString(),
message: 'Error fetching game stats'
})}\n\n`);
}
}, 500); // Poll every 500ms
// Clean up on client disconnect
req.on('close', () => {
clearInterval(pollInterval);
});
});
// Admin control endpoints - proxy to Colyseus server
// Kick player endpoint
app.post('/api/admin/kick-player', async (req, res) => {
try {
const gameServerUrl = process.env.SERVER_URL || 'http://localhost:2567';
const response = await fetch(`${gameServerUrl}/api/admin/kick-player`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(req.body)
});
const result = await response.json();
res.json(result);
} catch (error) {
res.status(500).json({ success: false, message: 'Error communicating with game server' });
}
});
// Pause game endpoint
app.post('/api/admin/pause-game', async (req, res) => {
try {
const gameServerUrl = process.env.SERVER_URL || 'http://localhost:2567';
const response = await fetch(`${gameServerUrl}/api/admin/pause-game`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(req.body)
});
const result = await response.json();
res.json(result);
} catch (error) {
res.status(500).json({ success: false, message: 'Error communicating with game server' });
}
});
// Resume game endpoint
app.post('/api/admin/resume-game', async (req, res) => {
try {
const gameServerUrl = process.env.SERVER_URL || 'http://localhost:2567';
const response = await fetch(`${gameServerUrl}/api/admin/resume-game`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(req.body)
});
const result = await response.json();
res.json(result);
} catch (error) {
res.status(500).json({ success: false, message: 'Error communicating with game server' });
}
});
// Cancel game endpoint
app.post('/api/admin/cancel-game', async (req, res) => {
try {
const gameServerUrl = process.env.SERVER_URL || 'http://localhost:2567';
const response = await fetch(`${gameServerUrl}/api/admin/cancel-game`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(req.body)
});
const result = await response.json();
res.json(result);
} catch (error) {
res.status(500).json({ success: false, message: 'Error communicating with game server' });
}
});
// Serve static files from current directory (AFTER API routes)
app.use(express.static('.'));
// Serve main HTML file for SPA routes
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'));
});
app.listen(PORT, () => {
console.log(`
📊 SnatchGame Admin Dashboard
📱 Environment: ${ENV}
🌐 Server URL: http://localhost:${PORT}
🔗 Game Server: ${process.env.SERVER_URL}
📡 SSE Endpoint: http://localhost:${PORT}/api/sse
`);
});