mejoras de codigo

This commit is contained in:
2025-08-29 13:43:33 -06:00
parent f8fe1bdf57
commit 82ab8d7709
3 changed files with 351 additions and 52 deletions

View File

@@ -185,6 +185,10 @@ interface Props {
players?: PlayerData[]; players?: PlayerData[];
activeRooms?: any; activeRooms?: any;
aggregatedEvents?: any[]; aggregatedEvents?: any[];
aggregated?: {
detailedEvents?: any[];
counts?: Record<string, number>;
};
}; };
compact?: boolean; compact?: boolean;
} }
@@ -413,57 +417,76 @@ function applyFilters() {
players: [], players: [],
events: [], events: [],
metrics: {}, metrics: {},
aggregatedCounts: {} aggregatedCounts: {},
sourceData: filters.value.timeMode
}; };
// Filter based on time mode let sourceEvents: any[] = [];
// Get events based on time mode
if (filters.value.timeMode === 'active') { if (filters.value.timeMode === 'active') {
// Use active rooms data // Extract events from active rooms
result.activeRooms = props.rawData.activeRooms; result.activeRooms = props.rawData.activeRooms;
// TODO: Extract events from active rooms if (props.rawData.activeRooms?.rooms) {
} else { sourceEvents = props.rawData.activeRooms.rooms.flatMap((room: any) =>
// Filter by time range (room.systemMessages || []).map((msg: any) => ({
const fromMs = Date.parse(filters.value.rangeFrom || ''); ...msg,
const toMs = Date.parse(filters.value.rangeTo || ''); playerUuid: msg.playerUuid || undefined
}))
if (props.rawData.aggregatedEvents) { );
result.events = props.rawData.aggregatedEvents.filter((ev: any) => {
// Time filter
if (!Number.isNaN(fromMs) && !Number.isNaN(toMs)) {
const t = ev.timestamp;
if (typeof t === 'number' && (t < fromMs || t > toMs)) return false;
}
// Round filter
if (filters.value.rounds.length > 0) {
if (!filters.value.rounds.includes(ev.round)) return false;
}
// Game filter
if (filters.value.games.length > 0) {
if (!filters.value.games.includes(ev.gameVariant)) return false;
}
// Room filter
if (filters.value.rooms.length > 0) {
if (!filters.value.rooms.includes(ev.roomId)) return false;
}
return true;
});
} }
} else {
// Use aggregated events from multiple possible sources
sourceEvents = props.rawData.aggregatedEvents ||
props.rawData.aggregated?.detailedEvents ||
[];
} }
// Apply filters to events
const fromMs = Date.parse(filters.value.rangeFrom || '');
const toMs = Date.parse(filters.value.rangeTo || '');
result.events = sourceEvents.filter((ev: any) => {
// Time filter (only for range mode)
if (filters.value.timeMode === 'range' && !Number.isNaN(fromMs) && !Number.isNaN(toMs)) {
const t = ev.timestamp;
if (typeof t === 'number' && (t < fromMs || t > toMs)) return false;
}
// Round filter
if (filters.value.rounds.length > 0) {
if (!filters.value.rounds.includes(ev.round)) return false;
}
// Game filter
if (filters.value.games.length > 0) {
if (!filters.value.games.includes(ev.gameVariant)) return false;
}
// Room filter
if (filters.value.rooms.length > 0) {
if (!filters.value.rooms.includes(ev.roomId)) return false;
}
// Player filter
if (filters.value.playerUuids.length > 0 && ev.playerUuid) {
if (!filters.value.playerUuids.includes(ev.playerUuid)) return false;
}
return true;
});
// Filter players // Filter players
if (props.rawData.players) { if (props.rawData.players) {
result.players = props.rawData.players.filter((p: PlayerData) => { if (filters.value.playerUuids.length > 0) {
if (filters.value.playerUuids.length > 0) { result.players = props.rawData.players.filter((p: PlayerData) =>
return filters.value.playerUuids.includes(p.uuid); filters.value.playerUuids.includes(p.uuid)
} );
return true; } else {
}); result.players = [...props.rawData.players];
}
// Calculate metrics from filtered players // Calculate metrics from filtered players and their score history
let totalP1Scores = 0; let totalP1Scores = 0;
let totalP2Scores = 0; let totalP2Scores = 0;
let p1Count = 0; let p1Count = 0;
@@ -478,17 +501,27 @@ function applyFilters() {
if (player.roomScoreHistory) { if (player.roomScoreHistory) {
player.roomScoreHistory.forEach(roomScore => { player.roomScoreHistory.forEach(roomScore => {
roomScore.scores.forEach(score => { roomScore.scores.forEach(score => {
// Apply filters to scores // Apply same filters to score history
if (filters.value.rounds.length > 0 && !filters.value.rounds.includes(score.round)) return; let includeScore = true;
if (filters.value.games.length > 0 && !filters.value.games.includes(score.variant)) return;
if (filters.value.rooms.length > 0 && !filters.value.rooms.includes(roomScore.roomId)) return;
if (score.role === 'P1') { if (filters.value.rounds.length > 0 && !filters.value.rounds.includes(score.round)) {
totalP1Scores += score.score; includeScore = false;
p1Count++; }
} else if (score.role === 'P2') { if (filters.value.games.length > 0 && !filters.value.games.includes(score.variant)) {
totalP2Scores += score.score; includeScore = false;
p2Count++; }
if (filters.value.rooms.length > 0 && !filters.value.rooms.includes(roomScore.roomId)) {
includeScore = false;
}
if (includeScore) {
if (score.role === 'P1') {
totalP1Scores += score.score;
p1Count++;
} else if (score.role === 'P2') {
totalP2Scores += score.score;
p2Count++;
}
} }
}); });
}); });
@@ -504,7 +537,7 @@ function applyFilters() {
}; };
} }
// Count events by type // Count events by type from filtered events
const eventTypes = [ const eventTypes = [
'p1_propose', 'p1_no_offer', 'p1_propose', 'p1_no_offer',
'p2_snatch', 'p2_accept', 'p2_force', 'p2_no_force', 'p2_reject', 'p2_snatch', 'p2_accept', 'p2_force', 'p2_no_force', 'p2_reject',
@@ -516,6 +549,25 @@ function applyFilters() {
result.aggregatedCounts[type] = result.events.filter((e: any) => e.kind === type).length; result.aggregatedCounts[type] = result.events.filter((e: any) => e.kind === type).length;
}); });
// Add debug info
result._debug = {
totalSourceEvents: sourceEvents.length,
filteredEvents: result.events.length,
activeFilters: {
timeMode: filters.value.timeMode,
rounds: filters.value.rounds,
games: filters.value.games,
players: filters.value.playerUuids,
rooms: filters.value.rooms
},
timeRange: filters.value.timeMode === 'range' ? {
from: filters.value.rangeFrom,
to: filters.value.rangeTo,
fromMs,
toMs
} : null
};
emit('filtered', result); emit('filtered', result);
} }

View File

@@ -0,0 +1,237 @@
<template>
<div class="data-viewer glass light">
<div class="viewer-header">
<div class="tab-selector">
<button
v-for="tab in dataTabs"
:key="tab.id"
class="tab-btn"
:class="{ active: activeTab === tab.id }"
@click="activeTab = tab.id"
>
{{ tab.icon }} {{ tab.label }}
</button>
</div>
<div class="viewer-actions">
<button class="action-btn" @click="expanded = !expanded">
{{ expanded ? ' Minimizar' : ' Expandir' }}
</button>
<button class="action-btn" @click="copyCurrentTab" title="Copiar JSON">
📋 Copiar
</button>
</div>
</div>
<Transition name="slide">
<div v-show="expanded" class="viewer-content">
<pre class="data-pre">{{ currentTabData }}</pre>
</div>
</Transition>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
interface Props {
rawData?: any;
filterState?: any;
filteredData?: any;
}
const props = defineProps<Props>();
// Data viewer state
const activeTab = ref<'raw' | 'filters' | 'filtered'>('raw');
const expanded = ref(false);
const dataTabs = [
{ id: 'raw' as const, label: 'Raw Data', icon: '📊' },
{ id: 'filters' as const, label: 'Filter State', icon: '🔧' },
{ id: 'filtered' as const, label: 'Datos Filtrados', icon: '✨' }
];
const currentTabData = computed(() => {
try {
switch (activeTab.value) {
case 'raw':
return JSON.stringify(props.rawData || {}, null, 2);
case 'filters':
return JSON.stringify(props.filterState || {}, null, 2);
case 'filtered':
return JSON.stringify(props.filteredData || {}, null, 2);
default:
return '{}';
}
} catch (e) {
return `Error: ${e}`;
}
});
function copyCurrentTab() {
try {
navigator.clipboard.writeText(currentTabData.value);
} catch (e) {
console.error('Error copying to clipboard:', e);
}
}
</script>
<style scoped>
/* Data Viewer Styles */
.data-viewer {
margin-top: 20px;
padding: 16px;
border-radius: 16px;
}
.viewer-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
gap: 12px;
flex-wrap: wrap;
}
.tab-selector {
display: flex;
gap: 4px;
background: rgba(148, 163, 184, 0.1);
padding: 3px;
border-radius: 10px;
}
.tab-btn {
padding: 8px 16px;
border: none;
background: transparent;
color: #64748b;
font-weight: 700;
font-size: 13px;
cursor: pointer;
border-radius: 8px;
transition: all 0.3s ease;
white-space: nowrap;
}
.tab-btn:hover {
background: rgba(255, 255, 255, 0.5);
color: #334155;
}
.tab-btn.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.viewer-actions {
display: flex;
gap: 8px;
}
.action-btn {
padding: 6px 12px;
border: 1px solid #cbd5e1;
background: rgba(255, 255, 255, 0.8);
color: #334155;
font-weight: 700;
font-size: 12px;
cursor: pointer;
border-radius: 8px;
transition: all 0.2s ease;
}
.action-btn:hover {
background: #fff;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.viewer-content {
overflow: hidden;
}
.data-pre {
background: #0b1020;
color: #e5e7eb;
padding: 16px;
border-radius: 12px;
font-size: 12px;
line-height: 1.5;
border: 1px solid #1f2937;
max-height: 400px;
overflow: auto;
margin: 0;
font-family: 'Fira Code', 'Consolas', 'Monaco', monospace;
}
/* Scrollbar styling for data-pre */
.data-pre::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.data-pre::-webkit-scrollbar-track {
background: #1a1f2e;
border-radius: 4px;
}
.data-pre::-webkit-scrollbar-thumb {
background: #374151;
border-radius: 4px;
}
.data-pre::-webkit-scrollbar-thumb:hover {
background: #4b5563;
}
/* Slide transition */
.slide-enter-active,
.slide-leave-active {
transition: all 0.3s ease;
}
.slide-enter-from {
max-height: 0;
opacity: 0;
}
.slide-enter-to {
max-height: 420px;
opacity: 1;
}
.slide-leave-from {
max-height: 420px;
opacity: 1;
}
.slide-leave-to {
max-height: 0;
opacity: 0;
}
@media (max-width: 768px) {
.viewer-header {
flex-direction: column;
align-items: stretch;
}
.tab-selector {
width: 100%;
overflow-x: auto;
}
.viewer-actions {
width: 100%;
justify-content: space-between;
}
.data-pre {
font-size: 11px;
max-height: 300px;
}
}
</style>

View File

@@ -50,6 +50,7 @@
:raw-data="{ :raw-data="{
players: allPlayersWithScores, players: allPlayersWithScores,
aggregatedEvents: fullAggregatedEvents, aggregatedEvents: fullAggregatedEvents,
aggregated: rawActionsPayload?.aggregated,
activeRooms: rawActionsPayload?.activeRooms activeRooms: rawActionsPayload?.activeRooms
}" }"
:compact="true" :compact="true"
@@ -84,6 +85,13 @@
}" }"
/> />
<!-- Filter Data Viewer Component -->
<FilterDataViewer
:raw-data="rawActionsPayload"
:filter-state="filterState"
:filtered-data="filteredData"
/>
<AppCredits position="bottom-right" /> <AppCredits position="bottom-right" />
</div> </div>
</template> </template>
@@ -93,6 +101,7 @@ import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import EventChart from '../components/EventChart.vue'; import EventChart from '../components/EventChart.vue';
import EventFilters from '../components/EventFilters.vue'; import EventFilters from '../components/EventFilters.vue';
import FilterDataViewer from '../components/FilterDataViewer.vue';
import GameLogo from '../components/GameLogo.vue'; import GameLogo from '../components/GameLogo.vue';
import AppCredits from '../components/AppCredits.vue'; import AppCredits from '../components/AppCredits.vue';
import { useEventFilters } from '../composables/useEventFilters'; import { useEventFilters } from '../composables/useEventFilters';
@@ -432,6 +441,7 @@ const periodLabel = computed(() => {
function closeStreams() { function closeStreams() {
try { esActions.value?.close(); } catch {} try { esActions.value?.close(); } catch {}
esActions.value = null; esActions.value = null;