diff --git a/client/src/components/RoomCard.vue b/client/src/components/RoomCard.vue new file mode 100644 index 0000000..47537d4 --- /dev/null +++ b/client/src/components/RoomCard.vue @@ -0,0 +1,353 @@ + + + + + \ No newline at end of file diff --git a/client/src/components/RoomModal.vue b/client/src/components/RoomModal.vue new file mode 100644 index 0000000..25f886c --- /dev/null +++ b/client/src/components/RoomModal.vue @@ -0,0 +1,203 @@ + + + + + \ No newline at end of file diff --git a/client/src/components/RoomsTable.vue b/client/src/components/RoomsTable.vue new file mode 100644 index 0000000..a61ff5f --- /dev/null +++ b/client/src/components/RoomsTable.vue @@ -0,0 +1,467 @@ + + + + + \ No newline at end of file diff --git a/client/src/components/SystemMessageDisplay.vue b/client/src/components/SystemMessageDisplay.vue new file mode 100644 index 0000000..60270c7 --- /dev/null +++ b/client/src/components/SystemMessageDisplay.vue @@ -0,0 +1,150 @@ + + + + + \ No newline at end of file diff --git a/client/src/views/Dashboard.vue b/client/src/views/Dashboard.vue index e2faadc..98a7c92 100644 --- a/client/src/views/Dashboard.vue +++ b/client/src/views/Dashboard.vue @@ -1,7 +1,13 @@ @@ -136,26 +118,34 @@ import { ref, onMounted, onUnmounted, computed } from 'vue'; import { useRouter } from 'vue-router'; import { colyseusService } from '../services/colyseus'; +import RoomCard from '../components/RoomCard.vue'; +import RoomsTable from '../components/RoomsTable.vue'; +import RoomModal from '../components/RoomModal.vue'; const router = useRouter(); const rooms = ref([]); const roomDetails = ref<{ [key: string]: any }>({}); const globalStats = ref(null); const refreshInterval = ref(); +const selectedRoomId = ref(''); +const isModalOpen = ref(false); +const viewMode = ref<'cards' | 'table'>('table'); +const eventSource = ref(null); +const isSSEConnected = ref(false); +const reconnectAttempts = ref(0); +const maxReconnectAttempts = 5; const gameRooms = computed(() => rooms.value.filter(r => r.name === 'game')); const lobbyRooms = computed(() => rooms.value.filter(r => r.name === 'lobby')); const totalPlayers = computed(() => rooms.value.reduce((sum, room) => sum + room.clients, 0)); onMounted(() => { - fetchData(); - refreshInterval.value = setInterval(fetchData, 3000); + // Try SSE first, fallback to polling if it fails + initSSE(); }); onUnmounted(() => { - if (refreshInterval.value) { - clearInterval(refreshInterval.value); - } + cleanup(); }); async function fetchData() { @@ -173,7 +163,15 @@ async function fetchData() { } async function viewRoomDetails(roomId: string) { + // If we have SSE connection, details are already coming in real-time + if (isSSEConnected.value && roomDetails.value[roomId]) { + console.log('[Dashboard] Room details already available via SSE'); + return; + } + + // Fallback to fetch if SSE is not connected or details are missing try { + console.log('[Dashboard] Fetching room details via API'); const stats = await colyseusService.fetchRoomStats(roomId); roomDetails.value[roomId] = stats; } catch (error) { @@ -217,16 +215,6 @@ async function kickPlayer(roomId: string, playerId: string) { } } -function formatTime(timestamp: number): string { - const date = new Date(timestamp); - return date.toLocaleTimeString(); -} - -function formatSeconds(seconds: number): string { - const mins = Math.floor(seconds / 60); - const secs = Math.floor(seconds % 60); - return `${mins}:${secs.toString().padStart(2, '0')}`; -} function refreshData() { fetchData(); @@ -235,6 +223,116 @@ function refreshData() { function goToLobby() { router.push('/'); } + +function initSSE() { + try { + console.log('[Dashboard] Initializing SSE connection...'); + eventSource.value = new EventSource(`${import.meta.env.VITE_API_URL || 'http://localhost:3000/api'}/dashboard-stream`); + + eventSource.value.onopen = () => { + console.log('[Dashboard] SSE connection opened'); + isSSEConnected.value = true; + reconnectAttempts.value = 0; + // Clear any existing polling interval + if (refreshInterval.value) { + clearInterval(refreshInterval.value); + refreshInterval.value = undefined; + } + }; + + eventSource.value.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + console.log('[Dashboard] Received SSE data:', data); + + // Update rooms, room details, and global stats from SSE + rooms.value = data.rooms || []; + roomDetails.value = data.roomDetails || {}; + globalStats.value = data.globalStats || null; + } catch (error) { + console.error('[Dashboard] Error parsing SSE data:', error); + } + }; + + eventSource.value.onerror = (error) => { + console.error('[Dashboard] SSE connection error:', error); + isSSEConnected.value = false; + + // Close the current connection + if (eventSource.value) { + eventSource.value.close(); + eventSource.value = null; + } + + // Attempt reconnection with exponential backoff + if (reconnectAttempts.value < maxReconnectAttempts) { + reconnectAttempts.value++; + const delay = Math.pow(2, reconnectAttempts.value) * 1000; // 2s, 4s, 8s, 16s, 32s + console.log(`[Dashboard] Attempting SSE reconnection in ${delay}ms (attempt ${reconnectAttempts.value})`); + + setTimeout(() => { + if (!isSSEConnected.value) { + initSSE(); + } + }, delay); + } else { + console.log('[Dashboard] Max SSE reconnection attempts reached, falling back to polling'); + fallbackToPolling(); + } + }; + + } catch (error) { + console.error('[Dashboard] Failed to initialize SSE, falling back to polling:', error); + fallbackToPolling(); + } +} + +function fallbackToPolling() { + console.log('[Dashboard] Using polling fallback'); + isSSEConnected.value = false; + + // Initial fetch + fetchData(); + + // Set up polling interval + if (refreshInterval.value) { + clearInterval(refreshInterval.value); + } + refreshInterval.value = setInterval(fetchData, 3000); +} + +function cleanup() { + // Close SSE connection + if (eventSource.value) { + eventSource.value.close(); + eventSource.value = null; + } + + // Clear polling interval + if (refreshInterval.value) { + clearInterval(refreshInterval.value); + } + + isSSEConnected.value = false; +} + +function openRoomModal(roomId: string) { + selectedRoomId.value = roomId; + // Auto-fetch room details if not already loaded and SSE is not connected + if (!roomDetails.value[roomId] && !isSSEConnected.value) { + viewRoomDetails(roomId); + } + isModalOpen.value = true; +} + +function closeRoomModal() { + isModalOpen.value = false; + selectedRoomId.value = ''; +} + +const selectedRoom = computed(() => { + return gameRooms.value.find(room => room.roomId === selectedRoomId.value); +});