diff --git a/client/src/components/RoomsTable.vue b/client/src/components/RoomsTable.vue
index dc6d296..61d4aad 100644
--- a/client/src/components/RoomsTable.vue
+++ b/client/src/components/RoomsTable.vue
@@ -96,6 +96,27 @@
>
📊 Details
+
+
+
+
@@ -145,6 +166,8 @@ const props = defineProps<{
defineEmits<{
refresh: [];
viewRoomModal: [roomId: string];
+ closeRoom: [roomId: string];
+ changeVariant: [roomId: string, variant: string];
}>();
function getRoomDetails(roomId: string) {
@@ -243,6 +266,47 @@ function getReadableTextColor(hex?: string): string {
transform: translateY(-1px);
}
+.btn-close {
+ background: #f44336;
+ color: white;
+ padding: 6px 12px;
+ font-size: 12px;
+ margin-left: 4px;
+}
+
+.btn-close:hover {
+ background: #d32f2f;
+ transform: translateY(-1px);
+}
+
+.variant-selector-container {
+ display: inline-block;
+ margin: 0 4px;
+}
+
+.variant-select {
+ padding: 4px 8px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ background: white;
+ font-size: 12px;
+ font-weight: 600;
+ color: #333;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.variant-select:hover {
+ border-color: #9c27b0;
+ background: #f8f9fa;
+}
+
+.variant-select:focus {
+ outline: none;
+ border-color: #9c27b0;
+ box-shadow: 0 0 0 2px rgba(156, 39, 176, 0.2);
+}
+
.no-rooms {
text-align: center;
padding: 40px;
diff --git a/client/src/router/index.ts b/client/src/router/index.ts
index a2c3646..d141b37 100644
--- a/client/src/router/index.ts
+++ b/client/src/router/index.ts
@@ -3,6 +3,7 @@ import Lobby from '../views/Lobby.vue';
import Game from '../views/Game.vue';
import Dashboard from '../views/Dashboard.vue';
import DemoGame from '../views/DemoGame.vue';
+import UuidSelector from '../views/UuidSelector.vue';
const router = createRouter({
history: createWebHistory(),
@@ -29,14 +30,13 @@ const router = createRouter({
},
{
path: '/',
- redirect: '/missing-uuid'
+ name: 'UuidSelector',
+ component: UuidSelector
},
{
- // simple fallback for users hitting root without UUID
+ // Redirect old missing-uuid path to the new selector
path: '/missing-uuid',
- component: {
- template: `Falta UUID
Abre el juego escaneando tu código QR: snatchgame.nucleoriofrio.com/{uuid}
`
- }
+ redirect: '/'
}
]
});
diff --git a/client/src/views/Dashboard.vue b/client/src/views/Dashboard.vue
index cd003ef..9980b81 100644
--- a/client/src/views/Dashboard.vue
+++ b/client/src/views/Dashboard.vue
@@ -37,6 +37,10 @@
+
+
+
+
@@ -154,6 +158,8 @@
:room-details="roomDetails"
@refresh="fetchData"
@view-room-modal="openRoomModal"
+ @close-room="closeRoom"
+ @change-variant="changeRoomVariant"
/>
@@ -238,7 +244,7 @@ const isLoadingGlobal = ref(false);
// Open tabs UI state
const allowedUuids = ref([]);
-const openCount = ref<1|2|6|10>(1);
+const openCount = ref<1|2|6|10|'50-1'|'50-2'|'50-3'|'50-4'>(1);
const selectedUuid = ref('');
const gameRooms = computed(() => rooms.value.filter(r => r.name === 'game'));
@@ -248,14 +254,17 @@ const totalPlayers = computed(() => rooms.value.reduce((sum, room) => sum + room
onMounted(async () => {
// Try SSE first, fallback to polling if it fails
initSSE();
- // Load allowed UUIDs
+ // Load allowed UUIDs from API
try {
- const list = await colyseusService.fetchAllowedUuids();
- allowedUuids.value = list || [];
+ const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3000/api'}/admin/uuids`);
+ const data = await response.json();
+ allowedUuids.value = data.uuids || [];
if (!selectedUuid.value && allowedUuids.value.length > 0) {
selectedUuid.value = allowedUuids.value[0];
}
- } catch {}
+ } catch {
+ console.error('Failed to load UUIDs from API');
+ }
});
onUnmounted(() => {
@@ -329,6 +338,43 @@ async function kickPlayer(roomId: string, playerId: string) {
}
}
+async function closeRoom(roomId: string) {
+ if (!confirm('¿Estás seguro de que quieres expulsar a todos los jugadores y cerrar esta sala?')) return;
+
+ try {
+ const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3000/api'}/rooms/${roomId}/close`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' }
+ });
+
+ if (!response.ok) throw new Error('Failed to close room');
+
+ console.log(`Room ${roomId} closed successfully`);
+ await fetchData();
+ } catch (error) {
+ console.error('Failed to close room:', error);
+ alert('Failed to close room. Check console for details.');
+ }
+}
+
+async function changeRoomVariant(roomId: string, variant: string) {
+ try {
+ const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3000/api'}/rooms/${roomId}/variant`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ variant })
+ });
+
+ if (!response.ok) throw new Error('Failed to change room variant');
+
+ console.log(`Room ${roomId} variant changed to ${variant} successfully`);
+ await fetchData();
+ } catch (error) {
+ console.error('Failed to change room variant:', error);
+ alert('Failed to change room variant. Check console for details.');
+ }
+}
+
function refreshData() {
fetchData();
@@ -351,10 +397,24 @@ function openTabs() {
return;
}
- // Randomly select N distinct UUIDs
- const N = openCount.value as number;
const pool = [...allowedUuids.value];
if (pool.length === 0) return;
+
+ // Handle deterministic 50-UUID batches
+ if (typeof openCount.value === 'string' && openCount.value.startsWith('50-')) {
+ const batchNumber = parseInt(openCount.value.split('-')[1]);
+ const startIndex = (batchNumber - 1) * 50;
+ const endIndex = startIndex + 50;
+ const batchUuids = pool.slice(startIndex, Math.min(endIndex, pool.length));
+
+ batchUuids.forEach(u => openOne(u));
+ return;
+ }
+
+ // For numeric counts, randomly select N distinct UUIDs
+ const N = openCount.value as number;
+
+ // For other counts, use distinct UUIDs
for (let i = pool.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[pool[i], pool[j]] = [pool[j], pool[i]];
diff --git a/client/src/views/UuidSelector.vue b/client/src/views/UuidSelector.vue
new file mode 100644
index 0000000..19754cd
--- /dev/null
+++ b/client/src/views/UuidSelector.vue
@@ -0,0 +1,381 @@
+
+
+
+
+
+
+
+ 🔍
+
+
+
+
+
{{ getUuidIndex(uuidInfo.uuid) }}
+
{{ uuidInfo.name }}
+
{{ formatUuid(uuidInfo.uuid) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/server/src/adminApi.ts b/server/src/adminApi.ts
index ec94bb6..db0c512 100644
--- a/server/src/adminApi.ts
+++ b/server/src/adminApi.ts
@@ -114,6 +114,48 @@ adminRouter.post("/rooms/:roomId/kick/:playerId", async (req: Request, res: Resp
}
});
+adminRouter.post("/rooms/:roomId/close", async (req: Request, res: Response) => {
+ try {
+ const { roomId } = req.params;
+ const rooms = await matchMaker.query({ roomId });
+
+ if (rooms.length === 0) {
+ return res.status(404).json({ error: "Room not found" });
+ }
+
+ await matchMaker.remoteRoomCall(roomId, "executeAdminCommand", ["sendToLobby"]);
+
+ res.json({ success: true, message: `Room ${roomId} closed and players sent to lobby` });
+ } catch (error) {
+ console.error(`[AdminAPI] Error closing room ${req.params.roomId}:`, error);
+ res.status(500).json({ error: "Failed to close room" });
+ }
+});
+
+adminRouter.post("/rooms/:roomId/variant", async (req: Request, res: Response) => {
+ try {
+ const { roomId } = req.params;
+ 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 rooms = await matchMaker.query({ roomId });
+
+ if (rooms.length === 0) {
+ return res.status(404).json({ error: "Room not found" });
+ }
+
+ await matchMaker.remoteRoomCall(roomId, "executeAdminCommand", ["setVariant", variant]);
+
+ res.json({ success: true, message: `Room ${roomId} variant changed to ${variant}` });
+ } catch (error) {
+ console.error(`[AdminAPI] Error changing variant for room ${req.params.roomId}:`, error);
+ res.status(500).json({ error: "Failed to change room variant" });
+ }
+});
+
adminRouter.get("/stats", async (req: Request, res: Response) => {
try {
const stats = await matchMaker.stats.fetchAll();
@@ -461,6 +503,28 @@ adminRouter.get("/admin/uuids", async (req: Request, res: Response) => {
}
});
+// UUID with names endpoint
+adminRouter.get("/admin/uuids-with-names", async (req: Request, res: Response) => {
+ try {
+ const uuids = listAllowedUuids();
+ const nameManager = NameManager.getInstance();
+
+ const uuidsWithInfo = uuids.map(uuid => {
+ const name = nameManager.getPlayerName(uuid);
+ return {
+ uuid,
+ name: name || null,
+ hasName: !!name
+ };
+ });
+
+ res.json({ uuids: uuidsWithInfo });
+ } catch (error) {
+ console.error("[AdminAPI] Error fetching UUIDs with names:", error);
+ res.status(500).json({ error: "Failed to fetch UUIDs with names" });
+ }
+});
+
// SSE endpoint for real-time dashboard updates
adminRouter.get("/dashboard-stream", (req: Request, res: Response) => {
// Set SSE headers