mejoras de codigo
This commit is contained in:
@@ -185,6 +185,10 @@ interface Props {
|
||||
players?: PlayerData[];
|
||||
activeRooms?: any;
|
||||
aggregatedEvents?: any[];
|
||||
aggregated?: {
|
||||
detailedEvents?: any[];
|
||||
counts?: Record<string, number>;
|
||||
};
|
||||
};
|
||||
compact?: boolean;
|
||||
}
|
||||
@@ -413,23 +417,38 @@ function applyFilters() {
|
||||
players: [],
|
||||
events: [],
|
||||
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') {
|
||||
// Use active rooms data
|
||||
// Extract events from active rooms
|
||||
result.activeRooms = props.rawData.activeRooms;
|
||||
// TODO: Extract events from active rooms
|
||||
if (props.rawData.activeRooms?.rooms) {
|
||||
sourceEvents = props.rawData.activeRooms.rooms.flatMap((room: any) =>
|
||||
(room.systemMessages || []).map((msg: any) => ({
|
||||
...msg,
|
||||
playerUuid: msg.playerUuid || undefined
|
||||
}))
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Filter by time range
|
||||
// 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 || '');
|
||||
|
||||
if (props.rawData.aggregatedEvents) {
|
||||
result.events = props.rawData.aggregatedEvents.filter((ev: any) => {
|
||||
// Time filter
|
||||
if (!Number.isNaN(fromMs) && !Number.isNaN(toMs)) {
|
||||
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;
|
||||
}
|
||||
@@ -449,21 +468,25 @@ function applyFilters() {
|
||||
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
|
||||
if (props.rawData.players) {
|
||||
result.players = props.rawData.players.filter((p: PlayerData) => {
|
||||
if (filters.value.playerUuids.length > 0) {
|
||||
return filters.value.playerUuids.includes(p.uuid);
|
||||
result.players = props.rawData.players.filter((p: PlayerData) =>
|
||||
filters.value.playerUuids.includes(p.uuid)
|
||||
);
|
||||
} else {
|
||||
result.players = [...props.rawData.players];
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Calculate metrics from filtered players
|
||||
// Calculate metrics from filtered players and their score history
|
||||
let totalP1Scores = 0;
|
||||
let totalP2Scores = 0;
|
||||
let p1Count = 0;
|
||||
@@ -478,11 +501,20 @@ function applyFilters() {
|
||||
if (player.roomScoreHistory) {
|
||||
player.roomScoreHistory.forEach(roomScore => {
|
||||
roomScore.scores.forEach(score => {
|
||||
// Apply filters to scores
|
||||
if (filters.value.rounds.length > 0 && !filters.value.rounds.includes(score.round)) return;
|
||||
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;
|
||||
// Apply same filters to score history
|
||||
let includeScore = true;
|
||||
|
||||
if (filters.value.rounds.length > 0 && !filters.value.rounds.includes(score.round)) {
|
||||
includeScore = false;
|
||||
}
|
||||
if (filters.value.games.length > 0 && !filters.value.games.includes(score.variant)) {
|
||||
includeScore = false;
|
||||
}
|
||||
if (filters.value.rooms.length > 0 && !filters.value.rooms.includes(roomScore.roomId)) {
|
||||
includeScore = false;
|
||||
}
|
||||
|
||||
if (includeScore) {
|
||||
if (score.role === 'P1') {
|
||||
totalP1Scores += score.score;
|
||||
p1Count++;
|
||||
@@ -490,6 +522,7 @@ function applyFilters() {
|
||||
totalP2Scores += score.score;
|
||||
p2Count++;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -504,7 +537,7 @@ function applyFilters() {
|
||||
};
|
||||
}
|
||||
|
||||
// Count events by type
|
||||
// Count events by type from filtered events
|
||||
const eventTypes = [
|
||||
'p1_propose', 'p1_no_offer',
|
||||
'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;
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
237
client/src/components/FilterDataViewer.vue
Normal file
237
client/src/components/FilterDataViewer.vue
Normal 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>
|
||||
@@ -50,6 +50,7 @@
|
||||
:raw-data="{
|
||||
players: allPlayersWithScores,
|
||||
aggregatedEvents: fullAggregatedEvents,
|
||||
aggregated: rawActionsPayload?.aggregated,
|
||||
activeRooms: rawActionsPayload?.activeRooms
|
||||
}"
|
||||
:compact="true"
|
||||
@@ -84,6 +85,13 @@
|
||||
}"
|
||||
/>
|
||||
|
||||
<!-- Filter Data Viewer Component -->
|
||||
<FilterDataViewer
|
||||
:raw-data="rawActionsPayload"
|
||||
:filter-state="filterState"
|
||||
:filtered-data="filteredData"
|
||||
/>
|
||||
|
||||
<AppCredits position="bottom-right" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -93,6 +101,7 @@ import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import EventChart from '../components/EventChart.vue';
|
||||
import EventFilters from '../components/EventFilters.vue';
|
||||
import FilterDataViewer from '../components/FilterDataViewer.vue';
|
||||
import GameLogo from '../components/GameLogo.vue';
|
||||
import AppCredits from '../components/AppCredits.vue';
|
||||
import { useEventFilters } from '../composables/useEventFilters';
|
||||
@@ -432,6 +441,7 @@ const periodLabel = computed(() => {
|
||||
|
||||
|
||||
|
||||
|
||||
function closeStreams() {
|
||||
try { esActions.value?.close(); } catch {}
|
||||
esActions.value = null;
|
||||
|
||||
Reference in New Issue
Block a user