Spaces:
Sleeping
Sleeping
| <template> | |
| <div class="socket-test-view"> | |
| <div class="test-header"> | |
| <h1>Socket API Test Page</h1> | |
| <p class="subtitle">Testing Backend Game Socket Handlers</p> | |
| </div> | |
| <div class="test-body"> | |
| <!-- Left Column: Controls and Status --> | |
| <div class="left-column"> | |
| <!-- Connection Status --> | |
| <div class="test-section"> | |
| <h2>Connection Status</h2> | |
| <div class="status-info"> | |
| <div class="status-item" :class="{ ready: connected, error: error }"> | |
| <span class="status-label">Socket:</span> | |
| <span class="status-value"> | |
| {{ connected ? "✓ Connected" : "⚪ Disconnected" }} | |
| </span> | |
| </div> | |
| <div v-if="socket" class="status-item"> | |
| <span class="status-label">Socket ID:</span> | |
| <span class="status-value code">{{ socket.id }}</span> | |
| </div> | |
| <div v-if="error" class="error-message">❌ Error: {{ error }}</div> | |
| </div> | |
| </div> | |
| <!-- Room Management --> | |
| <div class="test-section"> | |
| <h2>Room Management</h2> | |
| <div class="room-info"> | |
| <div class="info-item"> | |
| <span class="label">Current Room:</span> | |
| <span class="value code">{{ currentRoomId || "Not in room" }}</span> | |
| </div> | |
| <div v-if="playerColor" class="info-item"> | |
| <span class="label">Player Color:</span> | |
| <span class="value" :class="playerColor">{{ playerColor }}</span> | |
| </div> | |
| <div v-if="currentRoomId" class="info-item"> | |
| <span class="label">Room Admin:</span> | |
| <span class="value">{{ isAdmin ? "You (Admin)" : "Other player" }}</span> | |
| </div> | |
| </div> | |
| <div class="room-controls"> | |
| <div class="control-group"> | |
| <input | |
| v-model="nickname" | |
| type="text" | |
| placeholder="Nickname" | |
| class="input-text" | |
| /> | |
| <input | |
| v-model="roomIdInput" | |
| type="text" | |
| placeholder="Room ID (leave empty to create)" | |
| class="input-text" | |
| /> | |
| </div> | |
| <div class="button-group"> | |
| <button | |
| class="btn btn-primary" | |
| @click="testJoinRoom" | |
| :disabled="!connected || currentRoomId !== null" | |
| > | |
| Join/Create Room | |
| </button> | |
| <button | |
| class="btn btn-secondary" | |
| @click="testLeaveRoom" | |
| :disabled="!currentRoomId" | |
| > | |
| Leave Room | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Game State Display --> | |
| <div class="test-section"> | |
| <h2>Game State</h2> | |
| <div class="game-info"> | |
| <div class="info-item"> | |
| <span class="label">Game Status:</span> | |
| <span class="value">{{ gameState.gameStatus || "N/A" }}</span> | |
| </div> | |
| <div class="info-item"> | |
| <span class="label">Current Player:</span> | |
| <span class="value" :class="gameState.currentPlayer"> | |
| {{ gameState.currentPlayer || "N/A" }} | |
| </span> | |
| </div> | |
| <div class="info-item"> | |
| <span class="label">Current Index:</span> | |
| <span class="value">{{ gameState.currentMoveIndex }}</span> | |
| </div> | |
| <div class="info-item"> | |
| <span class="label">Captured (Black):</span> | |
| <span class="value">{{ gameState.capturedStones.black }}</span> | |
| </div> | |
| <div class="info-item"> | |
| <span class="label">Captured (White):</span> | |
| <span class="value">{{ gameState.capturedStones.white }}</span> | |
| </div> | |
| </div> | |
| <!-- TGN Display --> | |
| <div v-if="gameState.tgn" class="tgn-display"> | |
| <h3>Current TGN:</h3> | |
| <pre class="tgn-content">{{ gameState.tgn }}</pre> | |
| </div> | |
| </div> | |
| <!-- Game Actions --> | |
| <div class="test-section"> | |
| <h2>Game Actions</h2> | |
| <div class="game-controls"> | |
| <div class="control-group"> | |
| <input | |
| v-model.number="moveX" | |
| type="number" | |
| placeholder="X" | |
| class="input-number" | |
| min="0" | |
| max="4" | |
| /> | |
| <input | |
| v-model.number="moveY" | |
| type="number" | |
| placeholder="Y" | |
| class="input-number" | |
| min="0" | |
| max="4" | |
| /> | |
| <input | |
| v-model.number="moveZ" | |
| type="number" | |
| placeholder="Z" | |
| class="input-number" | |
| min="0" | |
| max="4" | |
| /> | |
| <button | |
| class="btn btn-primary" | |
| @click="testMakeMove" | |
| :disabled="!currentRoomId" | |
| > | |
| Make Move | |
| </button> | |
| </div> | |
| <div class="button-group"> | |
| <button | |
| class="btn btn-secondary" | |
| @click="testPass" | |
| :disabled="!currentRoomId" | |
| > | |
| Pass | |
| </button> | |
| <button | |
| class="btn btn-warning" | |
| @click="testResign" | |
| :disabled="!currentRoomId" | |
| > | |
| Resign | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Admin Actions --> | |
| <div class="test-section admin-section"> | |
| <h2>Admin Actions (Undo/Redo/Reset)</h2> | |
| <p class="section-desc">Test the new admin APIs</p> | |
| <div v-if="!isAdmin && currentRoomId" class="admin-warning"> | |
| ⚠️ Only the room admin can reset the game | |
| </div> | |
| <div class="button-group"> | |
| <button | |
| class="btn btn-admin" | |
| @click="testUndo" | |
| :disabled="!currentRoomId || gameState.currentMoveIndex === 0" | |
| > | |
| Undo Move | |
| </button> | |
| <button | |
| class="btn btn-admin" | |
| @click="testRedo" | |
| :disabled="!currentRoomId" | |
| > | |
| Redo Move | |
| </button> | |
| </div> | |
| <div class="reset-controls"> | |
| <div class="control-group"> | |
| <label class="input-label">Board Shape:</label> | |
| <input | |
| v-model.number="boardShapeX" | |
| type="number" | |
| placeholder="X" | |
| class="input-number" | |
| min="3" | |
| max="9" | |
| /> | |
| <input | |
| v-model.number="boardShapeY" | |
| type="number" | |
| placeholder="Y" | |
| class="input-number" | |
| min="3" | |
| max="9" | |
| /> | |
| <input | |
| v-model.number="boardShapeZ" | |
| type="number" | |
| placeholder="Z" | |
| class="input-number" | |
| min="3" | |
| max="9" | |
| /> | |
| </div> | |
| <div v-if="roomPlayers.length === 2" class="control-group"> | |
| <label class="checkbox-label"> | |
| <input type="checkbox" v-model="swapColors" /> | |
| Swap Player Colors (Black ↔ White) | |
| </label> | |
| </div> | |
| <button class="btn btn-danger" @click="testReset" :disabled="!currentRoomId || !isAdmin"> | |
| Reset Game (Admin Only) | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- End Left Column --> | |
| <!-- Right Column: Event Log --> | |
| <div class="right-column"> | |
| <!-- Event Log --> | |
| <div class="test-section event-log-section"> | |
| <h2>Event Log</h2> | |
| <div class="event-log-controls"> | |
| <button class="btn btn-secondary" @click="clearEventLog">Clear Log</button> | |
| <label class="checkbox-label"> | |
| <input type="checkbox" v-model="autoScroll" /> | |
| Auto-scroll | |
| </label> | |
| </div> | |
| <div class="event-log" ref="eventLogRef"> | |
| <div v-if="eventLog.length === 0" class="empty-state">No events yet</div> | |
| <div | |
| v-for="(event, index) in eventLog" | |
| :key="index" | |
| class="event-item" | |
| :class="event.type" | |
| > | |
| <span class="event-timestamp">{{ event.timestamp }}</span> | |
| <span class="event-name">{{ event.name }}</span> | |
| <span class="event-type-badge">{{ event.type }}</span> | |
| <pre class="event-data">{{ JSON.stringify(event.data, null, 2) }}</pre> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- End Right Column --> | |
| </div> | |
| </div> | |
| </template> | |
| <script setup lang="ts"> | |
| import { ref, reactive, onMounted, onUnmounted, nextTick, watch } from "vue"; | |
| import { useSocket } from "@/composables/useSocket"; | |
| // Socket composable | |
| const { socket, connected, error } = useSocket(); | |
| // Room state | |
| const currentRoomId = ref<string | null>(null); | |
| const playerColor = ref<string | null>(null); | |
| const nickname = ref("TestUser"); | |
| const roomIdInput = ref(""); | |
| const isAdmin = ref(false); | |
| const roomPlayers = ref<string[]>([]); | |
| const playerColorAssignments = reactive<{ [playerId: string]: "black" | "white" }>({}); | |
| const swapColors = ref(false); | |
| // Game state | |
| const gameState = reactive({ | |
| currentPlayer: null as string | null, | |
| currentMoveIndex: 0, | |
| capturedStones: { black: 0, white: 0 }, | |
| gameStatus: null as string | null, | |
| tgn: "" as string | |
| }); | |
| // Move inputs | |
| const moveX = ref(0); | |
| const moveY = ref(0); | |
| const moveZ = ref(0); | |
| // Board shape inputs for reset | |
| const boardShapeX = ref(5); | |
| const boardShapeY = ref(5); | |
| const boardShapeZ = ref(5); | |
| // Event log | |
| const eventLog = ref< | |
| Array<{ | |
| timestamp: string; | |
| name: string; | |
| type: "sent" | "received" | "error"; | |
| data: any; | |
| }> | |
| >([]); | |
| const eventLogRef = ref<HTMLElement | null>(null); | |
| const autoScroll = ref(true); | |
| // Helper to log events | |
| function logEvent(name: string, data: any, type: "sent" | "received" | "error" = "received") { | |
| const timestamp = new Date().toLocaleTimeString(); | |
| eventLog.value.push({ timestamp, name, type, data }); | |
| if (autoScroll.value) { | |
| nextTick(() => { | |
| if (eventLogRef.value) { | |
| eventLogRef.value.scrollTop = eventLogRef.value.scrollHeight; | |
| } | |
| }); | |
| } | |
| } | |
| function clearEventLog() { | |
| eventLog.value = []; | |
| } | |
| // Room management | |
| function testJoinRoom() { | |
| const data = { | |
| roomId: roomIdInput.value || undefined, | |
| nickname: nickname.value | |
| }; | |
| logEvent("joinRoom", data, "sent"); | |
| socket.emit("joinRoom", data, (response: any) => { | |
| logEvent("joinRoom:response", response, response.success ? "received" : "error"); | |
| if (response.success) { | |
| currentRoomId.value = response.roomId; | |
| playerColor.value = response.playerColor; | |
| isAdmin.value = response.isAdmin || false; | |
| // Initialize room players from response | |
| if (response.players) { | |
| roomPlayers.value = Object.keys(response.players); | |
| // Initialize player color assignments | |
| for (const [playerId, playerInfo] of Object.entries(response.players) as any) { | |
| playerColorAssignments[playerId] = playerInfo.color; | |
| } | |
| } | |
| // Update game state | |
| if (response.gameState) { | |
| Object.assign(gameState, response.gameState); | |
| // Update board shape inputs | |
| if (response.gameState.boardShape) { | |
| boardShapeX.value = response.gameState.boardShape.x; | |
| boardShapeY.value = response.gameState.boardShape.y; | |
| boardShapeZ.value = response.gameState.boardShape.z; | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| function testLeaveRoom() { | |
| logEvent("leaveRoom", {}, "sent"); | |
| socket.emit("leaveRoom"); | |
| // Reset local state | |
| currentRoomId.value = null; | |
| playerColor.value = null; | |
| Object.assign(gameState, { | |
| currentPlayer: null, | |
| currentMoveIndex: 0, | |
| capturedStones: { black: 0, white: 0 }, | |
| gameStatus: null, | |
| tgn: "" | |
| }); | |
| } | |
| // Game actions | |
| function testMakeMove() { | |
| const data = { x: moveX.value, y: moveY.value, z: moveZ.value }; | |
| logEvent("makeMove", data, "sent"); | |
| socket.emit("makeMove", data); | |
| } | |
| function testPass() { | |
| logEvent("pass", {}, "sent"); | |
| socket.emit("pass"); | |
| } | |
| function testResign() { | |
| if (!confirm("Are you sure you want to resign?")) return; | |
| logEvent("resign", {}, "sent"); | |
| socket.emit("resign"); | |
| } | |
| // Admin actions | |
| function testUndo() { | |
| logEvent("undoMove", {}, "sent"); | |
| socket.emit("undoMove", (response: any) => { | |
| logEvent("undoMove:response", response, response.success ? "received" : "error"); | |
| }); | |
| } | |
| function testRedo() { | |
| logEvent("redoMove", {}, "sent"); | |
| socket.emit("redoMove", (response: any) => { | |
| logEvent("redoMove:response", response, response.success ? "received" : "error"); | |
| }); | |
| } | |
| function testReset() { | |
| if (!confirm("Reset entire game?")) return; | |
| const data: any = { | |
| boardShape: { | |
| x: boardShapeX.value, | |
| y: boardShapeY.value, | |
| z: boardShapeZ.value | |
| } | |
| }; | |
| // Apply color swap if checkbox is checked | |
| if (swapColors.value && roomPlayers.value.length === 2) { | |
| const [player1, player2] = roomPlayers.value; | |
| const color1 = playerColorAssignments[player1]; | |
| const color2 = playerColorAssignments[player2]; | |
| // Swap the colors | |
| data.playerColors = { | |
| [player1]: color2, | |
| [player2]: color1 | |
| }; | |
| } | |
| logEvent("resetGame", data, "sent"); | |
| socket.emit("resetGame", data, (response: any) => { | |
| logEvent("resetGame:response", response, response.success ? "received" : "error"); | |
| }); | |
| } | |
| // Socket event listeners | |
| onMounted(() => { | |
| // Listen to gameUpdate | |
| socket.on("gameUpdate", (data: any) => { | |
| logEvent("gameUpdate", data, "received"); | |
| // Update local game state | |
| if (data.currentPlayer !== undefined) gameState.currentPlayer = data.currentPlayer; | |
| if (data.currentMoveIndex !== undefined) gameState.currentMoveIndex = data.currentMoveIndex; | |
| if (data.capturedStones !== undefined) gameState.capturedStones = data.capturedStones; | |
| if (data.tgn !== undefined) gameState.tgn = data.tgn; | |
| }); | |
| // Listen to gameReset | |
| socket.on("gameReset", (data: any) => { | |
| logEvent("gameReset", data, "received"); | |
| // Reset local game state | |
| gameState.currentPlayer = data.currentPlayer; | |
| gameState.currentMoveIndex = 0; | |
| gameState.capturedStones = { black: 0, white: 0 }; | |
| gameState.tgn = data.tgn || ""; | |
| // Update board shape inputs | |
| if (data.boardShape) { | |
| boardShapeX.value = data.boardShape.x; | |
| boardShapeY.value = data.boardShape.y; | |
| boardShapeZ.value = data.boardShape.z; | |
| } | |
| // Update player list and colors if provided | |
| if (data.players) { | |
| roomPlayers.value = Object.keys(data.players); | |
| // Initialize player color assignments | |
| for (const [playerId, playerInfo] of Object.entries(data.players) as any) { | |
| playerColorAssignments[playerId] = playerInfo.color; | |
| } | |
| // Update current player's color display | |
| if (data.players[socket.id]) { | |
| playerColor.value = data.players[socket.id].color; | |
| } | |
| } | |
| }); | |
| // Listen to gameEnded | |
| socket.on("gameEnded", (data: any) => { | |
| logEvent("gameEnded", data, "received"); | |
| gameState.gameStatus = "finished"; | |
| }); | |
| // Listen to playerJoined | |
| socket.on("playerJoined", (data: any) => { | |
| logEvent("playerJoined", data, "received"); | |
| // Add the new player to the room players list | |
| if (data.playerId && !roomPlayers.value.includes(data.playerId)) { | |
| roomPlayers.value.push(data.playerId); | |
| // Default the new player to white (second player) | |
| playerColorAssignments[data.playerId] = "white"; | |
| } | |
| }); | |
| // Listen to playerLeft | |
| socket.on("playerLeft", (data: any) => { | |
| logEvent("playerLeft", data, "received"); | |
| }); | |
| // Listen to nicknameChanged | |
| socket.on("nicknameChanged", (data: any) => { | |
| logEvent("nicknameChanged", data, "received"); | |
| }); | |
| // Listen to error events | |
| socket.on("error", (data: any) => { | |
| logEvent("error", data, "error"); | |
| }); | |
| }); | |
| onUnmounted(() => { | |
| // Clean up listeners | |
| socket.off("gameUpdate"); | |
| socket.off("gameReset"); | |
| socket.off("gameEnded"); | |
| socket.off("playerJoined"); | |
| socket.off("playerLeft"); | |
| socket.off("nicknameChanged"); | |
| socket.off("error"); | |
| }); | |
| </script> | |
| <style scoped> | |
| .socket-test-view { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | |
| } | |
| .test-header { | |
| text-align: center; | |
| margin-bottom: 30px; | |
| padding-bottom: 20px; | |
| border-bottom: 2px solid #e0e0e0; | |
| } | |
| .test-header h1 { | |
| margin: 0 0 10px 0; | |
| font-size: 2em; | |
| color: #333; | |
| } | |
| .subtitle { | |
| margin: 0; | |
| color: #666; | |
| font-size: 1.1em; | |
| } | |
| .test-body { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 20px; | |
| overflow: auto; | |
| max-height: calc(100vh - 200px); | |
| } | |
| .left-column { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| overflow-y: auto; | |
| } | |
| .right-column { | |
| display: flex; | |
| flex-direction: column; | |
| min-height: 0; | |
| } | |
| .event-log-section { | |
| display: flex; | |
| flex-direction: column; | |
| height: 100%; | |
| min-height: 0; | |
| } | |
| .test-section { | |
| background: #f9f9f9; | |
| border: 1px solid #ddd; | |
| border-radius: 8px; | |
| padding: 20px; | |
| } | |
| .test-section h2 { | |
| margin: 0 0 15px 0; | |
| font-size: 1.5em; | |
| color: #333; | |
| } | |
| .section-desc { | |
| margin: -10px 0 15px 0; | |
| color: #666; | |
| font-size: 0.9em; | |
| } | |
| .admin-section { | |
| background: #fff8e1; | |
| border-color: #ffd54f; | |
| } | |
| /* Status */ | |
| .status-info, | |
| .room-info, | |
| .game-info { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| margin-bottom: 15px; | |
| } | |
| .status-item, | |
| .info-item { | |
| display: flex; | |
| gap: 10px; | |
| align-items: center; | |
| padding: 8px; | |
| background: white; | |
| border-radius: 4px; | |
| } | |
| .status-item.ready { | |
| background: #e8f5e9; | |
| } | |
| .status-item.error { | |
| background: #ffebee; | |
| } | |
| .status-label, | |
| .label { | |
| font-weight: 600; | |
| min-width: 120px; | |
| } | |
| .status-value, | |
| .value { | |
| flex: 1; | |
| } | |
| .value.black { | |
| color: #000; | |
| font-weight: bold; | |
| } | |
| .value.white { | |
| color: #999; | |
| font-weight: bold; | |
| } | |
| .code { | |
| font-family: "Courier New", monospace; | |
| background: #f0f0f0; | |
| padding: 2px 6px; | |
| border-radius: 3px; | |
| font-size: 0.9em; | |
| } | |
| .error-message { | |
| padding: 10px; | |
| background: #ffebee; | |
| border: 1px solid #ef5350; | |
| border-radius: 4px; | |
| color: #c62828; | |
| } | |
| /* Controls */ | |
| .room-controls, | |
| .game-controls { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .control-group, | |
| .button-group { | |
| display: flex; | |
| gap: 10px; | |
| flex-wrap: wrap; | |
| align-items: center; | |
| } | |
| .reset-controls { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| margin-top: 10px; | |
| } | |
| .input-label { | |
| font-weight: 600; | |
| margin-right: 5px; | |
| } | |
| .input-text, | |
| .input-number, | |
| .input-select { | |
| padding: 8px 12px; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| font-size: 1em; | |
| flex: 1; | |
| min-width: 150px; | |
| } | |
| .input-number { | |
| min-width: 80px; | |
| max-width: 100px; | |
| } | |
| .input-select { | |
| min-width: 200px; | |
| cursor: pointer; | |
| background: white; | |
| } | |
| .admin-warning { | |
| padding: 10px; | |
| background: #fff8e1; | |
| border: 1px solid #ffd54f; | |
| border-radius: 4px; | |
| color: #f57c00; | |
| font-weight: 500; | |
| margin-bottom: 10px; | |
| } | |
| .btn { | |
| padding: 10px 20px; | |
| border: none; | |
| border-radius: 4px; | |
| font-size: 1em; | |
| cursor: pointer; | |
| transition: background 0.2s; | |
| font-weight: 500; | |
| } | |
| .btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| .btn-primary { | |
| background: #2196f3; | |
| color: white; | |
| } | |
| .btn-primary:hover:not(:disabled) { | |
| background: #1976d2; | |
| } | |
| .btn-secondary { | |
| background: #757575; | |
| color: white; | |
| } | |
| .btn-secondary:hover:not(:disabled) { | |
| background: #616161; | |
| } | |
| .btn-warning { | |
| background: #ff9800; | |
| color: white; | |
| } | |
| .btn-warning:hover:not(:disabled) { | |
| background: #f57c00; | |
| } | |
| .btn-danger { | |
| background: #f44336; | |
| color: white; | |
| } | |
| .btn-danger:hover:not(:disabled) { | |
| background: #d32f2f; | |
| } | |
| .btn-admin { | |
| background: #9c27b0; | |
| color: white; | |
| } | |
| .btn-admin:hover:not(:disabled) { | |
| background: #7b1fa2; | |
| } | |
| .checkbox-label { | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| cursor: pointer; | |
| } | |
| /* Event Log */ | |
| .event-log-controls { | |
| display: flex; | |
| gap: 10px; | |
| align-items: center; | |
| margin-bottom: 10px; | |
| } | |
| .event-log { | |
| flex: 1; | |
| overflow-y: auto; | |
| background: white; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| padding: 10px; | |
| min-height: 0; | |
| } | |
| .empty-state { | |
| text-align: center; | |
| color: #999; | |
| padding: 20px; | |
| } | |
| /* TGN Display */ | |
| .tgn-display { | |
| margin-top: 15px; | |
| } | |
| .tgn-display h3 { | |
| margin: 0 0 10px 0; | |
| font-size: 1.1em; | |
| color: #333; | |
| } | |
| .tgn-content { | |
| background: white; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| padding: 10px; | |
| margin: 0; | |
| font-family: "Courier New", monospace; | |
| font-size: 0.9em; | |
| overflow-x: auto; | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| max-height: 200px; | |
| overflow-y: auto; | |
| } | |
| .event-item { | |
| margin-bottom: 10px; | |
| padding: 10px; | |
| border-radius: 4px; | |
| border-left: 4px solid #2196f3; | |
| background: #f5f5f5; | |
| } | |
| .event-item.sent { | |
| border-left-color: #4caf50; | |
| background: #e8f5e9; | |
| } | |
| .event-item.error { | |
| border-left-color: #f44336; | |
| background: #ffebee; | |
| } | |
| .event-timestamp { | |
| font-size: 0.85em; | |
| color: #666; | |
| margin-right: 10px; | |
| } | |
| .event-name { | |
| font-weight: 600; | |
| margin-right: 10px; | |
| } | |
| .event-type-badge { | |
| display: inline-block; | |
| padding: 2px 8px; | |
| border-radius: 12px; | |
| font-size: 0.75em; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| } | |
| .event-item.sent .event-type-badge { | |
| background: #4caf50; | |
| color: white; | |
| } | |
| .event-item.received .event-type-badge { | |
| background: #2196f3; | |
| color: white; | |
| } | |
| .event-item.error .event-type-badge { | |
| background: #f44336; | |
| color: white; | |
| } | |
| .event-data { | |
| margin: 10px 0 0 0; | |
| padding: 10px; | |
| background: white; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| font-size: 0.85em; | |
| overflow-x: auto; | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| } | |
| </style> | |