@@ -42,6 +42,8 @@
< span class = "key global" > < / span > Global
< span class = "sep" > · < / span >
< 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 class = "player-chips" >
< div class = "search-controls" >
@@ -68,6 +70,33 @@
< button v-if = "selectedUuid" class="chip clear" @click="clearPlayer" > Quitar selección < / button >
< / 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 >
< / Transition >
@@ -85,8 +114,9 @@
dataSource : eventFilters.dataSource.value ,
round : eventFilters.roundFilter.value.toString ( ) ,
game : eventFilters.gameFilter.value ,
hasFilters : eventFilters.hasActiveFilters.value ,
selectedPlayer : selectedUuid ? players.find ( p = > p . uuid === selectedUuid ) ? . name || 'Jugador' : undefined
hasFilters : eventFilters.hasActiveFilters.value | | ! ! selectedRoomId ,
selectedPlayer : selectedUuid ? players.find ( p = > p . uuid === selectedUuid ) ? . name || 'Jugador' : undefined ,
selectedRoom : selectedRoomId ? availableRooms . find ( r => r . roomId === selectedRoomId ) ? . name || 'Sala' : undefined
} "
/>
</div>
@@ -235,7 +265,7 @@ function computeMetrics(roomDetails: any) {
}
// 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.
// For now, set basic values. In a real implementation, we'd need to query current player state
selectedPlayerMetrics.value = {
@@ -258,7 +288,7 @@ const combinedPlayerCounts = computed(() => ({
}));
// 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);
});
@@ -266,18 +296,33 @@ watch([eventFilters.dataSource, eventFilters.roundFilter, eventFilters.gameFilte
const rooms = ref<RoomInfo[]>([]);
const players = ref<{ uuid: string; name: string; color?: string }[]>([]);
const availableRooms = ref<{ roomId: string; name: string; playerCount?: number }[]>([]);
const search = ref('');
const roomSearch = ref('');
const playersFiltered = computed(() => {
const q = (search.value || '').toLowerCase();
if (!q) return players.value;
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
watch(search, () => {
page.value = 1;
});
watch(roomSearch, () => {
roomPage.value = 1;
});
const page = ref(1);
const roomPage = ref(1);
const containerWidth = ref(1200); // Default width, will be updated dynamically
const dynamicPageSize = computed(() => {
// Estimate space needed per chip and controls
@@ -293,13 +338,35 @@ const dynamicPageSize = computed(() => {
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 roomPageCount = computed(() => Math.max(1, Math.ceil((roomsFiltered.value.length || 0) / roomDynamicPageSize.value)));
const playersPage = computed(() => {
const start = (page.value - 1) * 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 selectedRoomId = ref('');
const playerLoading = ref(false);
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>>>({});
@@ -319,6 +386,10 @@ function initials(name: string): string {
return chars || '🙂';
}
function shortRoomId(roomId: string): string {
return roomId.slice(0, 8);
}
function selectPlayer(uuid: string) {
if (selectedUuid.value === uuid) return;
selectedUuid.value = uuid;
@@ -329,8 +400,23 @@ function clearPlayer() {
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 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
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
const playerBarGradient = computed(() => {
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 details = data?.roomDetails || {};
// Room details are for active rooms, we'll get room data from player history instead
// 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;
Object.valu es(details).forEach((d: any) => {
Object.entri es(details).forEach(([roomId, d]: [string, any] ) => {
(Array.isArray(d?.systemMessages) ? d.systemMessages : []).forEach((m: any) => {
const k = (m?.kind || '').toString();
if (EVENTS.includes(k)) {
@@ -384,7 +478,8 @@ function setupStreams() {
detailedEvents.push({
kind: k,
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
computeMetrics(details);
// Don't extract rooms from active data - we'll get them from aggregated player history
// Apply filters and update display
if (eventFilters.dataSource.value === 'active-rooms') {
eventFilters.applyFilters(EVENTS);
@@ -445,8 +542,9 @@ function setupStreams() {
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) }));
// 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;
// Update detailed counts map
@@ -473,6 +571,21 @@ function setupStreams() {
playersActionsByUuid.value = byUuid;
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
if (eventFilters.dataSource.value === 'aggregated') {
eventFilters.applyFilters(EVENTS);
@@ -580,11 +693,12 @@ watch(() => playersFiltered.value.length, () => {
// Removed totals table and sorting; keep actions stream for per-player counts only
const allPlayersActions = ref<{ uuid: string; name: string; total: number }[]>([]);
function downloadCSV() {
const currentEvents = eventFilters.currentSourceEvents.value;
// 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
const rows: string[][] = [];
@@ -600,6 +714,7 @@ function downloadCSV() {
event.gameVariant || '',
event.playerUuid || '',
event.playerName || '',
event.roomId || '',
eventFilters.dataSource.value
]);
}
@@ -621,6 +736,7 @@ function downloadCSV() {
'', // GameVariant info not available in aggregated player data
uuid,
player?.name || '',
'', // Room info not available in aggregated player data
'player-aggregated'
]);
}
@@ -644,6 +760,7 @@ function downloadCSV() {
'',
'',
'',
'',
eventFilters.dataSource.value
]);
}
@@ -695,14 +812,21 @@ function downloadCSV() {
.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.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; }
.player-chips {
.player-chips, .room-chips {
display: flex;
flex-direction: column;
gap: 12px;
}
.room-chips {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid rgba(203, 213, 225, 0.5);
}
.search-controls {
display: flex;
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.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.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); }