Dashboard real-time updates con SSE y tabla de rooms
- Implementación de Server-Sent Events (SSE) para actualizaciones en tiempo real - Nuevo componente RoomsTable para vista de águila con animaciones de tokens - Componente RoomCard extraído para reutilización - Modal para ver detalles de rooms - SystemMessageDisplay usando AnimatedNumber creativamente - Indicador de conexión SSE con fallback a polling - Colores dinámicos de texto basados en brillo del fondo - Backend: broadcast de actualizaciones del dashboard - Backend: mensajes del sistema (excepto cambios de ronda) visibles en dashboard - Configuración de Vite para acceso desde red local
This commit is contained in:
@@ -2,6 +2,9 @@ import { Request, Response, Router } from "express";
|
||||
import { matchMaker } from "colyseus";
|
||||
import { GameRoom } from "./rooms/GameRoom";
|
||||
|
||||
// SSE connections storage
|
||||
const sseClients = new Set<Response>();
|
||||
|
||||
const adminRouter = Router();
|
||||
|
||||
adminRouter.get("/rooms", async (req: Request, res: Response) => {
|
||||
@@ -126,4 +129,130 @@ adminRouter.get("/stats", async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
export { adminRouter };
|
||||
// SSE endpoint for real-time dashboard updates
|
||||
adminRouter.get("/dashboard-stream", (req: Request, res: Response) => {
|
||||
// Set SSE headers
|
||||
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'
|
||||
});
|
||||
|
||||
// Add client to our set
|
||||
sseClients.add(res);
|
||||
console.log(`[AdminAPI] SSE client connected. Total clients: ${sseClients.size}`);
|
||||
|
||||
// Send initial data
|
||||
sendDashboardUpdate(res);
|
||||
|
||||
// Handle client disconnect
|
||||
req.on('close', () => {
|
||||
sseClients.delete(res);
|
||||
console.log(`[AdminAPI] SSE client disconnected. Total clients: ${sseClients.size}`);
|
||||
});
|
||||
|
||||
// Keep connection alive with periodic heartbeat
|
||||
const heartbeat = setInterval(() => {
|
||||
if (res.destroyed) {
|
||||
clearInterval(heartbeat);
|
||||
sseClients.delete(res);
|
||||
return;
|
||||
}
|
||||
res.write(':heartbeat\n\n');
|
||||
}, 30000); // 30 seconds
|
||||
|
||||
req.on('close', () => {
|
||||
clearInterval(heartbeat);
|
||||
});
|
||||
});
|
||||
|
||||
// Function to send dashboard data to SSE clients
|
||||
async function sendDashboardUpdate(client?: Response) {
|
||||
try {
|
||||
const rooms = await matchMaker.query({});
|
||||
const roomStats = rooms.map(room => ({
|
||||
roomId: room.roomId,
|
||||
name: room.name,
|
||||
clients: room.clients,
|
||||
maxClients: room.maxClients,
|
||||
metadata: room.metadata,
|
||||
locked: room.locked,
|
||||
private: room.private,
|
||||
createdAt: room.createdAt
|
||||
}));
|
||||
|
||||
// Get detailed stats for all game rooms
|
||||
const roomDetails: { [key: string]: any } = {};
|
||||
|
||||
for (const room of rooms) {
|
||||
if (room.name === 'game') {
|
||||
try {
|
||||
const detailData = await matchMaker.remoteRoomCall(room.roomId, "getState");
|
||||
roomDetails[room.roomId] = detailData;
|
||||
} catch (error) {
|
||||
console.warn(`[AdminAPI] Failed to get details for room ${room.roomId}:`, error);
|
||||
// Set empty details if room call fails
|
||||
roomDetails[room.roomId] = {
|
||||
players: [],
|
||||
gameStatus: room.metadata?.gameStatus || 'waiting',
|
||||
variant: room.metadata?.currentVariant || 'G1',
|
||||
round: room.metadata?.currentRound || 1
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const stats = await matchMaker.stats.fetchAll();
|
||||
const globalCCU = await matchMaker.stats.getGlobalCCU();
|
||||
|
||||
const dashboardData = {
|
||||
rooms: roomStats,
|
||||
roomDetails: roomDetails,
|
||||
globalStats: {
|
||||
processes: stats,
|
||||
globalCCU,
|
||||
localCCU: matchMaker.stats.local.ccu,
|
||||
localRoomCount: matchMaker.stats.local.roomCount
|
||||
}
|
||||
};
|
||||
|
||||
const message = `data: ${JSON.stringify(dashboardData)}\n\n`;
|
||||
|
||||
if (client) {
|
||||
// Send to specific client (for initial connection)
|
||||
if (!client.destroyed) {
|
||||
client.write(message);
|
||||
}
|
||||
} else {
|
||||
// Broadcast to all clients
|
||||
const deadClients: Response[] = [];
|
||||
|
||||
sseClients.forEach(client => {
|
||||
if (client.destroyed) {
|
||||
deadClients.push(client);
|
||||
} else {
|
||||
try {
|
||||
client.write(message);
|
||||
} catch (error) {
|
||||
console.error('[AdminAPI] Error writing to SSE client:', error);
|
||||
deadClients.push(client);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up dead connections
|
||||
deadClients.forEach(client => sseClients.delete(client));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[AdminAPI] Error sending dashboard update:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to broadcast dashboard updates (called from room events)
|
||||
function broadcastDashboardUpdate() {
|
||||
sendDashboardUpdate();
|
||||
}
|
||||
|
||||
export { adminRouter, broadcastDashboardUpdate };
|
||||
Reference in New Issue
Block a user