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:
@@ -25,6 +25,75 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dashboard-content">
|
<div class="dashboard-content">
|
||||||
|
<!-- Global Controls Section -->
|
||||||
|
<div class="global-controls-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>🌐 Global Controls</h2>
|
||||||
|
</div>
|
||||||
|
<div class="global-controls-grid">
|
||||||
|
<div class="control-group">
|
||||||
|
<h3>Game State</h3>
|
||||||
|
<div class="control-buttons">
|
||||||
|
<button
|
||||||
|
@click="pauseAllGames"
|
||||||
|
class="btn btn-pause"
|
||||||
|
:disabled="isLoadingGlobal"
|
||||||
|
>
|
||||||
|
⏸️ Pause All Games
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="resumeAllGames"
|
||||||
|
class="btn btn-resume"
|
||||||
|
:disabled="isLoadingGlobal"
|
||||||
|
>
|
||||||
|
▶️ Resume All Games
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="restartAllGames"
|
||||||
|
class="btn btn-restart"
|
||||||
|
:disabled="isLoadingGlobal"
|
||||||
|
>
|
||||||
|
🔄 Restart All Games
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<h3>Game Variant</h3>
|
||||||
|
<div class="variant-controls">
|
||||||
|
<select v-model="selectedGlobalVariant" class="variant-selector">
|
||||||
|
<option value="">Select Variant</option>
|
||||||
|
<option value="G1">G1 - Basic Game</option>
|
||||||
|
<option value="G2">G2 - Forced Offers</option>
|
||||||
|
<option value="G3">G3 - Shame Tokens</option>
|
||||||
|
<option value="G4">G4 - Judge System</option>
|
||||||
|
<option value="G5">G5 - Advanced</option>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
@click="changeGlobalVariant"
|
||||||
|
class="btn btn-variant"
|
||||||
|
:disabled="!selectedGlobalVariant || isLoadingGlobal"
|
||||||
|
>
|
||||||
|
🎮 Change All to {{ selectedGlobalVariant }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<h3>Player Management</h3>
|
||||||
|
<div class="control-buttons">
|
||||||
|
<button
|
||||||
|
@click="sendAllToLobby"
|
||||||
|
class="btn btn-lobby-all"
|
||||||
|
:disabled="isLoadingGlobal"
|
||||||
|
>
|
||||||
|
🏠 Send All to Lobby
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="rooms-section">
|
<div class="rooms-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>Active Game Rooms</h2>
|
<h2>Active Game Rooms</h2>
|
||||||
@@ -134,6 +203,8 @@ const eventSource = ref<EventSource | null>(null);
|
|||||||
const isSSEConnected = ref(false);
|
const isSSEConnected = ref(false);
|
||||||
const reconnectAttempts = ref(0);
|
const reconnectAttempts = ref(0);
|
||||||
const maxReconnectAttempts = 5;
|
const maxReconnectAttempts = 5;
|
||||||
|
const selectedGlobalVariant = ref('');
|
||||||
|
const isLoadingGlobal = ref(false);
|
||||||
|
|
||||||
const gameRooms = computed(() => rooms.value.filter(r => r.name === 'game'));
|
const gameRooms = computed(() => rooms.value.filter(r => r.name === 'game'));
|
||||||
const lobbyRooms = computed(() => rooms.value.filter(r => r.name === 'lobby'));
|
const lobbyRooms = computed(() => rooms.value.filter(r => r.name === 'lobby'));
|
||||||
@@ -224,6 +295,119 @@ function goToLobby() {
|
|||||||
router.push('/');
|
router.push('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Global control functions
|
||||||
|
async function pauseAllGames() {
|
||||||
|
if (!confirm('Are you sure you want to pause ALL active games?')) return;
|
||||||
|
|
||||||
|
isLoadingGlobal.value = true;
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3000/api'}/admin/pause-all`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Failed to pause all games');
|
||||||
|
|
||||||
|
console.log('All games paused successfully');
|
||||||
|
await fetchData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to pause all games:', error);
|
||||||
|
alert('Failed to pause all games. Check console for details.');
|
||||||
|
} finally {
|
||||||
|
isLoadingGlobal.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resumeAllGames() {
|
||||||
|
if (!confirm('Are you sure you want to resume ALL paused games?')) return;
|
||||||
|
|
||||||
|
isLoadingGlobal.value = true;
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3000/api'}/admin/resume-all`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Failed to resume all games');
|
||||||
|
|
||||||
|
console.log('All games resumed successfully');
|
||||||
|
await fetchData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to resume all games:', error);
|
||||||
|
alert('Failed to resume all games. Check console for details.');
|
||||||
|
} finally {
|
||||||
|
isLoadingGlobal.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restartAllGames() {
|
||||||
|
if (!confirm('Are you sure you want to RESTART ALL active games? This will reset all progress!')) return;
|
||||||
|
|
||||||
|
isLoadingGlobal.value = true;
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3000/api'}/admin/restart-all`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Failed to restart all games');
|
||||||
|
|
||||||
|
console.log('All games restarted successfully');
|
||||||
|
await fetchData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to restart all games:', error);
|
||||||
|
alert('Failed to restart all games. Check console for details.');
|
||||||
|
} finally {
|
||||||
|
isLoadingGlobal.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changeGlobalVariant() {
|
||||||
|
if (!selectedGlobalVariant.value) return;
|
||||||
|
if (!confirm(`Are you sure you want to change ALL games to variant ${selectedGlobalVariant.value}?`)) return;
|
||||||
|
|
||||||
|
isLoadingGlobal.value = true;
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3000/api'}/admin/change-variant`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ variant: selectedGlobalVariant.value })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Failed to change global variant');
|
||||||
|
|
||||||
|
console.log(`All games changed to variant ${selectedGlobalVariant.value} successfully`);
|
||||||
|
await fetchData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to change global variant:', error);
|
||||||
|
alert('Failed to change global variant. Check console for details.');
|
||||||
|
} finally {
|
||||||
|
isLoadingGlobal.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendAllToLobby() {
|
||||||
|
if (!confirm('Are you sure you want to send ALL players back to the lobby? This will end all active games!')) return;
|
||||||
|
|
||||||
|
isLoadingGlobal.value = true;
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3000/api'}/admin/send-all-to-lobby`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Failed to send all players to lobby');
|
||||||
|
|
||||||
|
console.log('All players sent to lobby successfully');
|
||||||
|
await fetchData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to send all players to lobby:', error);
|
||||||
|
alert('Failed to send all players to lobby. Check console for details.');
|
||||||
|
} finally {
|
||||||
|
isLoadingGlobal.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function initSSE() {
|
function initSSE() {
|
||||||
try {
|
try {
|
||||||
console.log('[Dashboard] Initializing SSE connection...');
|
console.log('[Dashboard] Initializing SSE connection...');
|
||||||
@@ -434,6 +618,83 @@ const selectedRoom = computed(() => {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.global-controls-section {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-controls-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 15px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group h3 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.variant-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.variant-selector {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-pause {
|
||||||
|
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-resume {
|
||||||
|
background: linear-gradient(135deg, #4caf50 0%, #388e3c 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-restart {
|
||||||
|
background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-variant {
|
||||||
|
background: linear-gradient(135deg, #9c27b0 0%, #7b1fa2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-lobby-all {
|
||||||
|
background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.rooms-section,
|
.rooms-section,
|
||||||
.lobby-section {
|
.lobby-section {
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
|
|||||||
@@ -200,9 +200,43 @@ onMounted(() => {
|
|||||||
// Game paused, could update UI state if needed
|
// Game paused, could update UI state if needed
|
||||||
});
|
});
|
||||||
|
|
||||||
|
room.onMessage("gameRestart", () => {
|
||||||
|
// Game restarted, could update UI state if needed
|
||||||
|
});
|
||||||
|
|
||||||
room.onMessage("variantChanged", (data: { variant: string }) => {
|
room.onMessage("variantChanged", (data: { variant: string }) => {
|
||||||
currentVariant.value = data.variant as any;
|
currentVariant.value = data.variant as any;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle room closure/disconnection
|
||||||
|
room.onLeave((code) => {
|
||||||
|
console.log('[DemoGame] Room disconnected with code:', code);
|
||||||
|
// Always clean up local storage when room closes
|
||||||
|
try {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.localStorage.removeItem('snatch.game.roomId');
|
||||||
|
window.localStorage.removeItem('snatch.game.sessionId');
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// If not on lobby page, redirect there
|
||||||
|
if (router.currentRoute.value.path !== '/') {
|
||||||
|
console.log('[DemoGame] Room closed, redirecting to lobby');
|
||||||
|
router.push('/');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
room.onError((code, message) => {
|
||||||
|
console.error('[DemoGame] Room error:', code, message);
|
||||||
|
// On error, also redirect to lobby
|
||||||
|
try {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.localStorage.removeItem('snatch.game.roomId');
|
||||||
|
window.localStorage.removeItem('snatch.game.sessionId');
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
router.push('/');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -223,6 +257,7 @@ function onReport(val: boolean) { colyseusService.report(val); }
|
|||||||
function onAssignShame(val: boolean) { colyseusService.assignShame(val); }
|
function onAssignShame(val: boolean) { colyseusService.assignShame(val); }
|
||||||
|
|
||||||
function leaveGame() {
|
function leaveGame() {
|
||||||
|
console.log('[DemoGame] User manually leaving game');
|
||||||
colyseusService.leaveGame();
|
colyseusService.leaveGame();
|
||||||
try { if (typeof window !== 'undefined') { window.localStorage.removeItem('snatch.game.roomId'); window.localStorage.removeItem('snatch.game.sessionId'); } } catch {}
|
try { if (typeof window !== 'undefined') { window.localStorage.removeItem('snatch.game.roomId'); window.localStorage.removeItem('snatch.game.sessionId'); } } catch {}
|
||||||
router.push('/');
|
router.push('/');
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ adminRouter.post("/rooms/:roomId/pause", async (req: Request, res: Response) =>
|
|||||||
return res.status(404).json({ error: "Room not found" });
|
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" });
|
res.json({ success: true, message: "Room paused" });
|
||||||
} catch (error) {
|
} 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" });
|
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" });
|
res.json({ success: true, message: "Room resumed" });
|
||||||
} catch (error) {
|
} 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" });
|
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" });
|
res.json({ success: true, message: "Room restarted" });
|
||||||
} catch (error) {
|
} 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" });
|
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` });
|
res.json({ success: true, message: `Player ${playerId} kicked` });
|
||||||
} catch (error) {
|
} 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
|
// SSE endpoint for real-time dashboard updates
|
||||||
adminRouter.get("/dashboard-stream", (req: Request, res: Response) => {
|
adminRouter.get("/dashboard-stream", (req: Request, res: Response) => {
|
||||||
// Set SSE headers
|
// Set SSE headers
|
||||||
|
|||||||
@@ -221,18 +221,6 @@ export class GameRoom extends Room<GameState> {
|
|||||||
|
|
||||||
// Removed nextRound handler - rounds now auto-advance
|
// 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.onMessage("admin:kick", (client, playerId: string) => {
|
||||||
this.handleKick(playerId);
|
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 {
|
private getConnectedPlayersCount(): number {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
this.state.players.forEach(player => {
|
this.state.players.forEach(player => {
|
||||||
|
|||||||
Reference in New Issue
Block a user