Implementar controles globales de admin en dashboard
- Agregados botones de control global: pausar, reanudar, reiniciar, cambiar variante, enviar al lobby - Implementados endpoints API para operaciones masivas en todas las game rooms - Agregado método executeAdminCommand en GameRoom para manejo unificado de comandos admin - Mejorado manejo de desconexión automática al lobby cuando admin cierra rooms - Interfaz responsiva con confirmaciones de usuario y estados de carga - Sistema robusto de limpieza de rooms con fallback de forzado
This commit is contained in:
@@ -49,7 +49,7 @@ adminRouter.post("/rooms/:roomId/pause", async (req: Request, res: Response) =>
|
||||
return res.status(404).json({ error: "Room not found" });
|
||||
}
|
||||
|
||||
await matchMaker.remoteRoomCall(roomId, "broadcast", ["admin:pause"]);
|
||||
await matchMaker.remoteRoomCall(roomId, "executeAdminCommand", ["pause"]);
|
||||
|
||||
res.json({ success: true, message: "Room paused" });
|
||||
} catch (error) {
|
||||
@@ -67,7 +67,7 @@ adminRouter.post("/rooms/:roomId/resume", async (req: Request, res: Response) =>
|
||||
return res.status(404).json({ error: "Room not found" });
|
||||
}
|
||||
|
||||
await matchMaker.remoteRoomCall(roomId, "broadcast", ["admin:resume"]);
|
||||
await matchMaker.remoteRoomCall(roomId, "executeAdminCommand", ["resume"]);
|
||||
|
||||
res.json({ success: true, message: "Room resumed" });
|
||||
} catch (error) {
|
||||
@@ -85,7 +85,7 @@ adminRouter.post("/rooms/:roomId/restart", async (req: Request, res: Response) =
|
||||
return res.status(404).json({ error: "Room not found" });
|
||||
}
|
||||
|
||||
await matchMaker.remoteRoomCall(roomId, "broadcast", ["admin:restart"]);
|
||||
await matchMaker.remoteRoomCall(roomId, "executeAdminCommand", ["restart"]);
|
||||
|
||||
res.json({ success: true, message: "Room restarted" });
|
||||
} catch (error) {
|
||||
@@ -103,7 +103,7 @@ adminRouter.post("/rooms/:roomId/kick/:playerId", async (req: Request, res: Resp
|
||||
return res.status(404).json({ error: "Room not found" });
|
||||
}
|
||||
|
||||
await matchMaker.remoteRoomCall(roomId, "broadcast", ["admin:kick", playerId]);
|
||||
await matchMaker.remoteRoomCall(roomId, "executeAdminCommand", ["kick", playerId]);
|
||||
|
||||
res.json({ success: true, message: `Player ${playerId} kicked` });
|
||||
} catch (error) {
|
||||
@@ -129,6 +129,169 @@ adminRouter.get("/stats", async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Global admin endpoints
|
||||
adminRouter.post("/admin/pause-all", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const gameRooms = await matchMaker.query({ name: "game" });
|
||||
|
||||
if (gameRooms.length === 0) {
|
||||
return res.json({ success: true, message: "No game rooms to pause" });
|
||||
}
|
||||
|
||||
const promises = gameRooms.map(room =>
|
||||
matchMaker.remoteRoomCall(room.roomId, "executeAdminCommand", ["pause"])
|
||||
.catch(error => console.error(`Failed to pause room ${room.roomId}:`, error))
|
||||
);
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Pause command sent to ${gameRooms.length} game rooms`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[AdminAPI] Error pausing all games:", error);
|
||||
res.status(500).json({ error: "Failed to pause all games" });
|
||||
}
|
||||
});
|
||||
|
||||
adminRouter.post("/admin/resume-all", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const gameRooms = await matchMaker.query({ name: "game" });
|
||||
|
||||
if (gameRooms.length === 0) {
|
||||
return res.json({ success: true, message: "No game rooms to resume" });
|
||||
}
|
||||
|
||||
const promises = gameRooms.map(room =>
|
||||
matchMaker.remoteRoomCall(room.roomId, "executeAdminCommand", ["resume"])
|
||||
.catch(error => console.error(`Failed to resume room ${room.roomId}:`, error))
|
||||
);
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Resume command sent to ${gameRooms.length} game rooms`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[AdminAPI] Error resuming all games:", error);
|
||||
res.status(500).json({ error: "Failed to resume all games" });
|
||||
}
|
||||
});
|
||||
|
||||
adminRouter.post("/admin/restart-all", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const gameRooms = await matchMaker.query({ name: "game" });
|
||||
|
||||
if (gameRooms.length === 0) {
|
||||
return res.json({ success: true, message: "No game rooms to restart" });
|
||||
}
|
||||
|
||||
const promises = gameRooms.map(room =>
|
||||
matchMaker.remoteRoomCall(room.roomId, "executeAdminCommand", ["restart"])
|
||||
.catch(error => console.error(`Failed to restart room ${room.roomId}:`, error))
|
||||
);
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Restart command sent to ${gameRooms.length} game rooms`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[AdminAPI] Error restarting all games:", error);
|
||||
res.status(500).json({ error: "Failed to restart all games" });
|
||||
}
|
||||
});
|
||||
|
||||
adminRouter.post("/admin/change-variant", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { variant } = req.body;
|
||||
|
||||
if (!variant || !['G1', 'G2', 'G3', 'G4', 'G5'].includes(variant)) {
|
||||
return res.status(400).json({ error: "Invalid variant. Must be one of: G1, G2, G3, G4, G5" });
|
||||
}
|
||||
|
||||
const gameRooms = await matchMaker.query({ name: "game" });
|
||||
|
||||
if (gameRooms.length === 0) {
|
||||
return res.json({ success: true, message: "No game rooms to change variant" });
|
||||
}
|
||||
|
||||
const promises = gameRooms.map(room =>
|
||||
matchMaker.remoteRoomCall(room.roomId, "executeAdminCommand", ["setVariant", variant])
|
||||
.catch(error => console.error(`Failed to change variant in room ${room.roomId}:`, error))
|
||||
);
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Variant change to ${variant} sent to ${gameRooms.length} game rooms`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[AdminAPI] Error changing global variant:", error);
|
||||
res.status(500).json({ error: "Failed to change global variant" });
|
||||
}
|
||||
});
|
||||
|
||||
adminRouter.post("/admin/send-all-to-lobby", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const gameRooms = await matchMaker.query({ name: "game" });
|
||||
|
||||
if (gameRooms.length === 0) {
|
||||
return res.json({ success: true, message: "No game rooms to close" });
|
||||
}
|
||||
|
||||
console.log(`[AdminAPI] Sending ${gameRooms.length} game rooms to lobby and disposing them`);
|
||||
|
||||
// Send command to all game rooms
|
||||
const promises = gameRooms.map(room =>
|
||||
matchMaker.remoteRoomCall(room.roomId, "executeAdminCommand", ["sendToLobby"])
|
||||
.catch(error => console.error(`Failed to send room ${room.roomId} to lobby:`, error))
|
||||
);
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
|
||||
// Wait a bit for rooms to dispose themselves, then force dispose any remaining
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const remainingGameRooms = await matchMaker.query({ name: "game" });
|
||||
|
||||
if (remainingGameRooms.length > 0) {
|
||||
console.log(`[AdminAPI] Force disposing ${remainingGameRooms.length} remaining game rooms`);
|
||||
|
||||
const disposePromises = remainingGameRooms.map(room =>
|
||||
matchMaker.remoteRoomCall(room.roomId, "disconnect").catch(() => {
|
||||
// If remote call fails, try direct disposal
|
||||
console.log(`[AdminAPI] Force disposing room ${room.roomId} directly`);
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.allSettled(disposePromises);
|
||||
}
|
||||
|
||||
// Broadcast dashboard update after cleanup
|
||||
setTimeout(() => {
|
||||
broadcastDashboardUpdate();
|
||||
}, 500);
|
||||
|
||||
} catch (error) {
|
||||
console.error("[AdminAPI] Error in cleanup phase:", error);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Send to lobby command sent to ${gameRooms.length} game rooms. Rooms will be disposed.`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[AdminAPI] Error sending all to lobby:", error);
|
||||
res.status(500).json({ error: "Failed to send all players to lobby" });
|
||||
}
|
||||
});
|
||||
|
||||
// SSE endpoint for real-time dashboard updates
|
||||
adminRouter.get("/dashboard-stream", (req: Request, res: Response) => {
|
||||
// Set SSE headers
|
||||
|
||||
@@ -221,18 +221,6 @@ export class GameRoom extends Room<GameState> {
|
||||
|
||||
// Removed nextRound handler - rounds now auto-advance
|
||||
|
||||
this.onMessage("admin:pause", () => {
|
||||
this.state.pauseGame();
|
||||
});
|
||||
|
||||
this.onMessage("admin:resume", () => {
|
||||
this.state.resumeGame();
|
||||
});
|
||||
|
||||
this.onMessage("admin:restart", () => {
|
||||
this.handleRestart();
|
||||
});
|
||||
|
||||
this.onMessage("admin:kick", (client, playerId: string) => {
|
||||
this.handleKick(playerId);
|
||||
});
|
||||
@@ -480,6 +468,111 @@ export class GameRoom extends Room<GameState> {
|
||||
}
|
||||
}
|
||||
|
||||
private handleSetVariant(variant: string) {
|
||||
console.log(`[GameRoom] Admin set variant to ${variant} in room ${this.roomId}`);
|
||||
|
||||
this.state.currentVariant = variant;
|
||||
this.state.currentRound = 1;
|
||||
this.state.resetRound();
|
||||
|
||||
if (this.state.gameStatus === GameStatus.FINISHED) {
|
||||
this.state.gameStatus = GameStatus.PLAYING;
|
||||
}
|
||||
|
||||
this.setMetadata({
|
||||
gameStatus: this.state.gameStatus === GameStatus.WAITING ? 'waiting' : 'playing',
|
||||
currentRound: this.state.currentRound,
|
||||
currentVariant: this.state.currentVariant
|
||||
});
|
||||
|
||||
if (variant === 'G2') {
|
||||
this.state.forcedByP2 = true;
|
||||
}
|
||||
|
||||
this.broadcast("variantChanged", { variant });
|
||||
this.sysChat(`🔄 Admin cambió variante a ${variant}`, 'admin_variant_change');
|
||||
|
||||
broadcastDashboardUpdate();
|
||||
}
|
||||
|
||||
private handleSendToLobby() {
|
||||
console.log(`[GameRoom] Admin send all players to lobby from room ${this.roomId}`);
|
||||
|
||||
this.sysChat('👋 Admin envía a todos al lobby', 'admin_send_lobby');
|
||||
|
||||
// Give players a moment to see the message
|
||||
setTimeout(() => {
|
||||
// Disconnect all clients, which will send them back to lobby
|
||||
this.clients.forEach(client => {
|
||||
try {
|
||||
client.leave(1000);
|
||||
} catch (error) {
|
||||
console.error(`Failed to disconnect client ${client.sessionId}:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
// Dispose the room
|
||||
setTimeout(() => {
|
||||
this.disconnect();
|
||||
}, 500);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Public method for admin API calls
|
||||
executeAdminCommand(command: string, ...args: any[]) {
|
||||
console.log(`[GameRoom] Executing admin command: ${command} with args:`, args);
|
||||
|
||||
switch (command) {
|
||||
case 'pause':
|
||||
this.state.pauseGame();
|
||||
this.broadcast("gamePaused");
|
||||
this.setMetadata({
|
||||
gameStatus: 'paused',
|
||||
currentRound: this.state.currentRound,
|
||||
currentVariant: this.state.currentVariant
|
||||
});
|
||||
this.sysChat('⏸️ Admin pausó el juego', 'admin_pause');
|
||||
broadcastDashboardUpdate();
|
||||
break;
|
||||
|
||||
case 'resume':
|
||||
this.state.resumeGame();
|
||||
this.setMetadata({
|
||||
gameStatus: 'playing',
|
||||
currentRound: this.state.currentRound,
|
||||
currentVariant: this.state.currentVariant
|
||||
});
|
||||
this.sysChat('▶️ Admin reanudó el juego', 'admin_resume');
|
||||
broadcastDashboardUpdate();
|
||||
break;
|
||||
|
||||
case 'restart':
|
||||
this.handleRestart();
|
||||
break;
|
||||
|
||||
case 'setVariant':
|
||||
const variant = args[0];
|
||||
if (variant) {
|
||||
this.handleSetVariant(variant);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'sendToLobby':
|
||||
this.handleSendToLobby();
|
||||
break;
|
||||
|
||||
case 'kick':
|
||||
const playerId = args[0];
|
||||
if (playerId) {
|
||||
this.handleKick(playerId);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`[GameRoom] Unknown admin command: ${command}`);
|
||||
}
|
||||
}
|
||||
|
||||
private getConnectedPlayersCount(): number {
|
||||
let count = 0;
|
||||
this.state.players.forEach(player => {
|
||||
|
||||
Reference in New Issue
Block a user