agregado filtro de eventos por sala en la que se realizaron

This commit is contained in:
2025-08-27 23:23:17 -06:00
parent ffd97646ab
commit 5321870336
4 changed files with 162 additions and 14 deletions

View File

@@ -15,6 +15,9 @@
<span class="filter-tag player-tag" v-if="selectedPlayerUuid && activeFilters?.selectedPlayer"> <span class="filter-tag player-tag" v-if="selectedPlayerUuid && activeFilters?.selectedPlayer">
👤 {{ activeFilters.selectedPlayer }} 👤 {{ activeFilters.selectedPlayer }}
</span> </span>
<span class="filter-tag room-tag" v-if="activeFilters?.selectedRoom">
🏠 {{ activeFilters.selectedRoom }}
</span>
</div> </div>
</div> </div>
<div v-if="loading" class="placeholder">Cargando datos</div> <div v-if="loading" class="placeholder">Cargando datos</div>
@@ -124,6 +127,7 @@ interface Props {
game: string; game: string;
hasFilters: boolean; hasFilters: boolean;
selectedPlayer?: string; selectedPlayer?: string;
selectedRoom?: string;
}; };
} }

View File

@@ -1,4 +1,4 @@
import { ref, computed, watch } from 'vue'; import { ref, computed } from 'vue';
export interface DetailedEvent { export interface DetailedEvent {
kind: string; kind: string;
@@ -6,17 +6,20 @@ export interface DetailedEvent {
gameVariant?: string; gameVariant?: string;
playerUuid?: string; playerUuid?: string;
playerName?: string; playerName?: string;
roomId?: string;
} }
export type DataSource = 'aggregated' | 'active-rooms'; export type DataSource = 'aggregated' | 'active-rooms';
export type RoundFilter = 'all' | 1 | 2 | 3; export type RoundFilter = 'all' | 1 | 2 | 3;
export type GameFilter = 'all' | 'G1' | 'G2' | 'G3' | 'G4' | 'G5'; export type GameFilter = 'all' | 'G1' | 'G2' | 'G3' | 'G4' | 'G5';
export type RoomFilter = 'all' | string;
export function useEventFilters() { export function useEventFilters() {
// Filter states // Filter states
const dataSource = ref<DataSource>('aggregated'); const dataSource = ref<DataSource>('aggregated');
const roundFilter = ref<RoundFilter>('all'); const roundFilter = ref<RoundFilter>('all');
const gameFilter = ref<GameFilter>('all'); const gameFilter = ref<GameFilter>('all');
const roomFilter = ref<RoomFilter>('all');
// Event data stores // Event data stores
const detailedEventsAggregated = ref<DetailedEvent[]>([]); const detailedEventsAggregated = ref<DetailedEvent[]>([]);
@@ -33,7 +36,7 @@ export function useEventFilters() {
? detailedEventsAggregated.value ? detailedEventsAggregated.value
: detailedEventsActiveRooms.value; : detailedEventsActiveRooms.value;
// Filter events based on round and game // Filter events based on round, game, and room
const filteredEvents = sourceEvents.filter(event => { const filteredEvents = sourceEvents.filter(event => {
if (roundFilter.value !== 'all' && event.round !== roundFilter.value) { if (roundFilter.value !== 'all' && event.round !== roundFilter.value) {
return false; return false;
@@ -41,6 +44,9 @@ export function useEventFilters() {
if (gameFilter.value !== 'all' && event.gameVariant !== gameFilter.value) { if (gameFilter.value !== 'all' && event.gameVariant !== gameFilter.value) {
return false; return false;
} }
if (roomFilter.value !== 'all' && event.roomId !== roomFilter.value) {
return false;
}
return true; return true;
}); });
@@ -71,6 +77,7 @@ export function useEventFilters() {
function resetFilters() { function resetFilters() {
roundFilter.value = 'all'; roundFilter.value = 'all';
gameFilter.value = 'all'; gameFilter.value = 'all';
roomFilter.value = 'all';
} }
// Computed properties // Computed properties
@@ -83,13 +90,14 @@ export function useEventFilters() {
); );
const hasActiveFilters = computed(() => const hasActiveFilters = computed(() =>
roundFilter.value !== 'all' || gameFilter.value !== 'all' roundFilter.value !== 'all' || gameFilter.value !== 'all' || roomFilter.value !== 'all'
); );
const filterSummary = computed(() => { const filterSummary = computed(() => {
const parts = []; const parts = [];
if (roundFilter.value !== 'all') parts.push(`Round ${roundFilter.value}`); if (roundFilter.value !== 'all') parts.push(`Round ${roundFilter.value}`);
if (gameFilter.value !== 'all') parts.push(`Game ${gameFilter.value}`); if (gameFilter.value !== 'all') parts.push(`Game ${gameFilter.value}`);
if (roomFilter.value !== 'all') parts.push(`Room ${roomFilter.value.slice(0, 8)}`);
return parts.length > 0 ? parts.join(' + ') : 'Sin filtros'; return parts.length > 0 ? parts.join(' + ') : 'Sin filtros';
}); });
@@ -98,6 +106,7 @@ export function useEventFilters() {
dataSource, dataSource,
roundFilter, roundFilter,
gameFilter, gameFilter,
roomFilter,
detailedEventsAggregated, detailedEventsAggregated,
detailedEventsActiveRooms, detailedEventsActiveRooms,
globalEventCounts, globalEventCounts,

View File

@@ -42,6 +42,8 @@
<span class="key global"></span> Global <span class="key global"></span> Global
<span class="sep">·</span> <span class="sep">·</span>
<span class="key player" v-if="selectedUuid"></span> Jugador <span class="key player" v-if="selectedUuid"></span> Jugador
<span class="sep" v-if="selectedRoomId">·</span>
<span class="key room" v-if="selectedRoomId"></span> Sala
</div> </div>
<div class="player-chips"> <div class="player-chips">
<div class="search-controls"> <div class="search-controls">
@@ -68,6 +70,33 @@
<button v-if="selectedUuid" class="chip clear" @click="clearPlayer">Quitar selección</button> <button v-if="selectedUuid" class="chip clear" @click="clearPlayer">Quitar selección</button>
</div> </div>
</div> </div>
<!-- Room Filter Section -->
<div class="room-chips">
<div class="search-controls">
<input class="search" v-model="roomSearch" placeholder="Buscar sala…" />
<div class="pagination compact" v-if="roomPageCount > 1">
<button class="pg-btn compact" @click="prevRoomPage" :disabled="roomPage <= 1"></button>
<span class="pg-ind">{{ roomPage }}/{{ roomPageCount }}</span>
<button class="pg-btn compact" @click="nextRoomPage" :disabled="roomPage >= roomPageCount"></button>
</div>
</div>
<div class="chips">
<button
v-for="r in roomsPage"
:key="r.roomId"
class="chip room-chip"
:class="{ active: r.roomId === selectedRoomId }"
@click="selectRoom(r.roomId)"
:title="`Sala: ${r.roomId} (${r.playerCount || 0} jugadores)`"
>
<span class="avatar">🏠</span>
<span class="label">{{ r.name }}</span>
<span class="count" v-if="r.playerCount">{{ r.playerCount }}</span>
</button>
<button v-if="selectedRoomId" class="chip clear" @click="clearRoom">Quitar selección</button>
</div>
</div>
</div> </div>
</Transition> </Transition>
@@ -85,8 +114,9 @@
dataSource: eventFilters.dataSource.value, dataSource: eventFilters.dataSource.value,
round: eventFilters.roundFilter.value.toString(), round: eventFilters.roundFilter.value.toString(),
game: eventFilters.gameFilter.value, game: eventFilters.gameFilter.value,
hasFilters: eventFilters.hasActiveFilters.value, hasFilters: eventFilters.hasActiveFilters.value || !!selectedRoomId,
selectedPlayer: selectedUuid ? players.find(p => p.uuid === selectedUuid)?.name || 'Jugador' : undefined selectedPlayer: selectedUuid ? players.find(p => p.uuid === selectedUuid)?.name || 'Jugador' : undefined,
selectedRoom: selectedRoomId ? availableRooms.find(r => r.roomId === selectedRoomId)?.name || 'Sala' : undefined
}" }"
/> />
</div> </div>
@@ -235,7 +265,7 @@ function computeMetrics(roomDetails: any) {
} }
// Function to compute metrics for a selected player // Function to compute metrics for a selected player
function computeSelectedPlayerMetrics(uuid: string) { function computeSelectedPlayerMetrics(_uuid: string) {
// Individual player metrics are simpler - just show if they're seated, their score, etc. // Individual player metrics are simpler - just show if they're seated, their score, etc.
// For now, set basic values. In a real implementation, we'd need to query current player state // For now, set basic values. In a real implementation, we'd need to query current player state
selectedPlayerMetrics.value = { selectedPlayerMetrics.value = {
@@ -258,7 +288,7 @@ const combinedPlayerCounts = computed(() => ({
})); }));
// Watch for changes in filters and data source // Watch for changes in filters and data source
watch([eventFilters.dataSource, eventFilters.roundFilter, eventFilters.gameFilter], () => { watch([eventFilters.dataSource, eventFilters.roundFilter, eventFilters.gameFilter, eventFilters.roomFilter], () => {
eventFilters.applyFilters(EVENTS); eventFilters.applyFilters(EVENTS);
}); });
@@ -266,18 +296,33 @@ watch([eventFilters.dataSource, eventFilters.roundFilter, eventFilters.gameFilte
const rooms = ref<RoomInfo[]>([]); const rooms = ref<RoomInfo[]>([]);
const players = ref<{ uuid: string; name: string; color?: string }[]>([]); const players = ref<{ uuid: string; name: string; color?: string }[]>([]);
const availableRooms = ref<{ roomId: string; name: string; playerCount?: number }[]>([]);
const search = ref(''); const search = ref('');
const roomSearch = ref('');
const playersFiltered = computed(() => { const playersFiltered = computed(() => {
const q = (search.value || '').toLowerCase(); const q = (search.value || '').toLowerCase();
if (!q) return players.value; if (!q) return players.value;
return players.value.filter(p => (p.name || '').toLowerCase().includes(q) || (p.uuid || '').toLowerCase().includes(q)); return players.value.filter(p => (p.name || '').toLowerCase().includes(q) || (p.uuid || '').toLowerCase().includes(q));
}); });
const roomsFiltered = computed(() => {
const q = (roomSearch.value || '').toLowerCase();
if (!q) return availableRooms.value;
return availableRooms.value.filter(r =>
(r.roomId || '').toLowerCase().includes(q) ||
(r.name || '').toLowerCase().includes(q)
);
});
// Reset page when search changes // Reset page when search changes
watch(search, () => { watch(search, () => {
page.value = 1; page.value = 1;
}); });
watch(roomSearch, () => {
roomPage.value = 1;
});
const page = ref(1); const page = ref(1);
const roomPage = ref(1);
const containerWidth = ref(1200); // Default width, will be updated dynamically const containerWidth = ref(1200); // Default width, will be updated dynamically
const dynamicPageSize = computed(() => { const dynamicPageSize = computed(() => {
// Estimate space needed per chip and controls // Estimate space needed per chip and controls
@@ -293,13 +338,35 @@ const dynamicPageSize = computed(() => {
return Math.min(maxChips, 15); // Maximum 15 chips per page return Math.min(maxChips, 15); // Maximum 15 chips per page
}); });
const roomDynamicPageSize = computed(() => {
// Similar calculation for rooms
const chipWidth = 160; // Room chips might be slightly wider
const clearBtnWidth = selectedRoomId.value ? 150 : 0;
const searchWidth = 240;
const paginationWidth = roomPageCount.value > 1 ? 100 : 0;
const margin = 40;
const availableWidth = containerWidth.value - searchWidth - paginationWidth - clearBtnWidth - margin;
const maxChips = Math.max(3, Math.floor(availableWidth / chipWidth));
return Math.min(maxChips, 10); // Maximum 10 room chips per page
});
const pageCount = computed(() => Math.max(1, Math.ceil((playersFiltered.value.length || 0) / dynamicPageSize.value))); const pageCount = computed(() => Math.max(1, Math.ceil((playersFiltered.value.length || 0) / dynamicPageSize.value)));
const roomPageCount = computed(() => Math.max(1, Math.ceil((roomsFiltered.value.length || 0) / roomDynamicPageSize.value)));
const playersPage = computed(() => { const playersPage = computed(() => {
const start = (page.value - 1) * dynamicPageSize.value; const start = (page.value - 1) * dynamicPageSize.value;
return playersFiltered.value.slice(start, start + dynamicPageSize.value); return playersFiltered.value.slice(start, start + dynamicPageSize.value);
}); });
const roomsPage = computed(() => {
const start = (roomPage.value - 1) * roomDynamicPageSize.value;
return roomsFiltered.value.slice(start, start + roomDynamicPageSize.value);
});
const selectedUuid = ref(''); const selectedUuid = ref('');
const selectedRoomId = ref('');
const playerLoading = ref(false); const playerLoading = ref(false);
const playerEventCounts = ref<Record<string, number>>((Object.fromEntries(EVENTS.map(k => [k, 0])) as Record<string, number>)); const playerEventCounts = ref<Record<string, number>>((Object.fromEntries(EVENTS.map(k => [k, 0])) as Record<string, number>));
const playersActionsByUuid = ref<Record<string, Record<string, number>>>({}); const playersActionsByUuid = ref<Record<string, Record<string, number>>>({});
@@ -319,6 +386,10 @@ function initials(name: string): string {
return chars || '🙂'; return chars || '🙂';
} }
function shortRoomId(roomId: string): string {
return roomId.slice(0, 8);
}
function selectPlayer(uuid: string) { function selectPlayer(uuid: string) {
if (selectedUuid.value === uuid) return; if (selectedUuid.value === uuid) return;
selectedUuid.value = uuid; selectedUuid.value = uuid;
@@ -329,8 +400,23 @@ function clearPlayer() {
selectedUuid.value = ''; selectedUuid.value = '';
} }
function selectRoom(roomId: string) {
if (selectedRoomId.value === roomId) return;
selectedRoomId.value = roomId;
eventFilters.roomFilter.value = roomId;
eventFilters.applyFilters(EVENTS);
}
function clearRoom() {
selectedRoomId.value = '';
eventFilters.roomFilter.value = 'all';
eventFilters.applyFilters(EVENTS);
}
function prevPage() { page.value = Math.max(1, page.value - 1); } function prevPage() { page.value = Math.max(1, page.value - 1); }
function nextPage() { page.value = Math.min(pageCount.value, page.value + 1); } function nextPage() { page.value = Math.min(pageCount.value, page.value + 1); }
function prevRoomPage() { roomPage.value = Math.max(1, roomPage.value - 1); }
function nextRoomPage() { roomPage.value = Math.min(roomPageCount.value, roomPage.value + 1); }
// Ensure page doesn't exceed pageCount when players change // Ensure page doesn't exceed pageCount when players change
watch(pageCount, (newCount) => { watch(pageCount, (newCount) => {
@@ -339,6 +425,12 @@ watch(pageCount, (newCount) => {
} }
}); });
watch(roomPageCount, (newCount) => {
if (roomPage.value > newCount) {
roomPage.value = Math.max(1, newCount);
}
});
// Dynamic per-player overlay bar gradient and label color // Dynamic per-player overlay bar gradient and label color
const playerBarGradient = computed(() => { const playerBarGradient = computed(() => {
const p = players.value.find(x => x.uuid === selectedUuid.value); const p = players.value.find(x => x.uuid === selectedUuid.value);
@@ -372,11 +464,13 @@ function setupStreams() {
const data = JSON.parse((e as MessageEvent).data || '{}'); const data = JSON.parse((e as MessageEvent).data || '{}');
const details = data?.roomDetails || {}; const details = data?.roomDetails || {};
// Room details are for active rooms, we'll get room data from player history instead
// Collect detailed events for active rooms // Collect detailed events for active rooms
const detailedEvents: Array<{ kind: string; round?: number; gameVariant?: string }> = []; const detailedEvents: Array<{ kind: string; round?: number; gameVariant?: string; roomId?: string }> = [];
const counts: Record<string, number> = Object.fromEntries(EVENTS.map(k => [k, 0])) as any; const counts: Record<string, number> = Object.fromEntries(EVENTS.map(k => [k, 0])) as any;
Object.values(details).forEach((d: any) => { Object.entries(details).forEach(([roomId, d]: [string, any]) => {
(Array.isArray(d?.systemMessages) ? d.systemMessages : []).forEach((m: any) => { (Array.isArray(d?.systemMessages) ? d.systemMessages : []).forEach((m: any) => {
const k = (m?.kind || '').toString(); const k = (m?.kind || '').toString();
if (EVENTS.includes(k)) { if (EVENTS.includes(k)) {
@@ -384,7 +478,8 @@ function setupStreams() {
detailedEvents.push({ detailedEvents.push({
kind: k, kind: k,
round: m?.round, round: m?.round,
gameVariant: m?.gameVariant || m?.variant gameVariant: m?.gameVariant || m?.variant,
roomId: m?.roomId || roomId // Use message roomId if available, otherwise use room key
}); });
} }
}); });
@@ -395,6 +490,8 @@ function setupStreams() {
// Compute additional metrics // Compute additional metrics
computeMetrics(details); computeMetrics(details);
// Don't extract rooms from active data - we'll get them from aggregated player history
// Apply filters and update display // Apply filters and update display
if (eventFilters.dataSource.value === 'active-rooms') { if (eventFilters.dataSource.value === 'active-rooms') {
eventFilters.applyFilters(EVENTS); eventFilters.applyFilters(EVENTS);
@@ -445,8 +542,9 @@ function setupStreams() {
const list = Array.isArray(data?.players) ? data.players : []; const list = Array.isArray(data?.players) ? data.players : [];
allPlayersActions.value = list.map((p: any) => ({ uuid: String(p.uuid||''), name: String(p.name||''), total: Number(p.total||0) })); allPlayersActions.value = list.map((p: any) => ({ uuid: String(p.uuid||''), name: String(p.name||''), total: Number(p.total||0) }));
// Collect all detailed events from all players // Collect all detailed events from all players
const allDetailedEvents: Array<{ kind: string; round?: number; gameVariant?: string }> = []; const allDetailedEvents: Array<{ kind: string; round?: number; gameVariant?: string; roomId?: string }> = [];
const aggregatedCounts: Record<string, number> = Object.fromEntries(EVENTS.map(k => [k, 0])) as any; const aggregatedCounts: Record<string, number> = Object.fromEntries(EVENTS.map(k => [k, 0])) as any;
// Update detailed counts map // Update detailed counts map
@@ -473,6 +571,21 @@ function setupStreams() {
playersActionsByUuid.value = byUuid; playersActionsByUuid.value = byUuid;
eventFilters.updateAggregatedData(allDetailedEvents, aggregatedCounts); eventFilters.updateAggregatedData(allDetailedEvents, aggregatedCounts);
// Extract unique room IDs from aggregated events - this is our primary source for rooms
const roomIds = new Set<string>();
allDetailedEvents.forEach(event => {
if (event.roomId && event.roomId.trim()) {
roomIds.add(event.roomId);
}
});
// Build available rooms list from aggregated events
availableRooms.value = Array.from(roomIds).map(roomId => ({
roomId,
name: `Sala ${roomId.slice(0, 8)}`,
playerCount: allDetailedEvents.filter(e => e.roomId === roomId).length
}));
// Apply filters and update display if viewing aggregated data // Apply filters and update display if viewing aggregated data
if (eventFilters.dataSource.value === 'aggregated') { if (eventFilters.dataSource.value === 'aggregated') {
eventFilters.applyFilters(EVENTS); eventFilters.applyFilters(EVENTS);
@@ -580,11 +693,12 @@ watch(() => playersFiltered.value.length, () => {
// Removed totals table and sorting; keep actions stream for per-player counts only // Removed totals table and sorting; keep actions stream for per-player counts only
const allPlayersActions = ref<{ uuid: string; name: string; total: number }[]>([]); const allPlayersActions = ref<{ uuid: string; name: string; total: number }[]>([]);
function downloadCSV() { function downloadCSV() {
const currentEvents = eventFilters.currentSourceEvents.value; const currentEvents = eventFilters.currentSourceEvents.value;
// Create CSV headers // Create CSV headers
const headers = ['Type', 'Event', 'Count', 'Round', 'GameVariant', 'PlayerUuid', 'PlayerName', 'DataSource']; const headers = ['Type', 'Event', 'Count', 'Round', 'GameVariant', 'PlayerUuid', 'PlayerName', 'RoomId', 'DataSource'];
// Create CSV rows - one row per individual event occurrence // Create CSV rows - one row per individual event occurrence
const rows: string[][] = []; const rows: string[][] = [];
@@ -600,6 +714,7 @@ function downloadCSV() {
event.gameVariant || '', event.gameVariant || '',
event.playerUuid || '', event.playerUuid || '',
event.playerName || '', event.playerName || '',
event.roomId || '',
eventFilters.dataSource.value eventFilters.dataSource.value
]); ]);
} }
@@ -621,6 +736,7 @@ function downloadCSV() {
'', // GameVariant info not available in aggregated player data '', // GameVariant info not available in aggregated player data
uuid, uuid,
player?.name || '', player?.name || '',
'', // Room info not available in aggregated player data
'player-aggregated' 'player-aggregated'
]); ]);
} }
@@ -644,6 +760,7 @@ function downloadCSV() {
'', '',
'', '',
'', '',
'',
eventFilters.dataSource.value eventFilters.dataSource.value
]); ]);
} }
@@ -695,14 +812,21 @@ function downloadCSV() {
.key { width: 12px; height: 12px; border-radius: 999px; display:inline-block; } .key { width: 12px; height: 12px; border-radius: 999px; display:inline-block; }
.key.global { background: linear-gradient(90deg, #34d399, #10b981); box-shadow: 0 0 8px rgba(16,185,129,0.35); } .key.global { background: linear-gradient(90deg, #34d399, #10b981); box-shadow: 0 0 8px rgba(16,185,129,0.35); }
.key.player { background: linear-gradient(90deg, #a78bfa, #6366f1); box-shadow: 0 0 8px rgba(99,102,241,0.35); } .key.player { background: linear-gradient(90deg, #a78bfa, #6366f1); box-shadow: 0 0 8px rgba(99,102,241,0.35); }
.key.room { background: linear-gradient(90deg, #f59e0b, #d97706); box-shadow: 0 0 8px rgba(245,158,11,0.35); }
.sep { opacity: 0.6; } .sep { opacity: 0.6; }
.player-chips { .player-chips, .room-chips {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
} }
.room-chips {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid rgba(203, 213, 225, 0.5);
}
.search-controls { .search-controls {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -736,6 +860,16 @@ function downloadCSV() {
.chip:hover { transform: translateY(-1px); background: color-mix(in srgb, var(--primary) 10%, white); box-shadow: 0 6px 18px rgba(102,126,234,0.18); } .chip:hover { transform: translateY(-1px); background: color-mix(in srgb, var(--primary) 10%, white); box-shadow: 0 6px 18px rgba(102,126,234,0.18); }
.chip.active { background: color-mix(in srgb, var(--primary) 18%, white); border-color: color-mix(in srgb, var(--primary) 45%, #c7d2fe); box-shadow: 0 6px 22px rgba(99,102,241,0.22); } .chip.active { background: color-mix(in srgb, var(--primary) 18%, white); border-color: color-mix(in srgb, var(--primary) 45%, #c7d2fe); box-shadow: 0 6px 22px rgba(99,102,241,0.22); }
.chip.clear { background:#fff; border-style:dashed; color:#334155; } .chip.clear { background:#fff; border-style:dashed; color:#334155; }
.chip.room-chip { --primary: #f59e0b; }
.chip.room-chip .count {
background: color-mix(in srgb, var(--primary) 20%, white);
color: color-mix(in srgb, var(--primary) 80%, #111);
font-size: 11px;
font-weight: 800;
padding: 2px 6px;
border-radius: 10px;
margin-left: 4px;
}
.avatar { width: 24px; height: 24px; border-radius: 50%; background: color-mix(in srgb, var(--primary) 25%, #eef2ff); display:grid; place-items:center; font-weight:900; color: color-mix(in srgb, var(--primary) 80%, #111); } .avatar { width: 24px; height: 24px; border-radius: 50%; background: color-mix(in srgb, var(--primary) 25%, #eef2ff); display:grid; place-items:center; font-weight:900; color: color-mix(in srgb, var(--primary) 80%, #111); }

View File

@@ -828,7 +828,8 @@ async function sendPlayersActionsUpdate(client?: Response) {
detailedHistory.push({ detailedHistory.push({
kind, kind,
round: (entry as any)?.round, round: (entry as any)?.round,
gameVariant: (entry as any)?.gameVariant || (entry as any)?.variant gameVariant: (entry as any)?.gameVariant || (entry as any)?.variant,
roomId: (entry as any)?.roomId
}); });
} }
const total = ACTION_EVENTS.reduce((acc, k) => acc + (counts[k] || 0), 0); const total = ACTION_EVENTS.reduce((acc, k) => acc + (counts[k] || 0), 0);