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:
2025-08-11 23:07:09 -06:00
parent deb63d4e38
commit 32f69805f0
9 changed files with 1689 additions and 265 deletions

View File

@@ -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 };