leaderboard implementado v3

This commit is contained in:
2025-08-28 02:47:07 -06:00
parent 220248b588
commit 318a7847b8
4 changed files with 150 additions and 106 deletions

View File

@@ -248,11 +248,22 @@ async function handleFileUpload(event: Event) {
} }
const result = await response.json(); const result = await response.json();
alert(`Estado cargado exitosamente. ${result.message || 'NameManager actualizado.'}`); const message = result.importedUuids
? `Estado cargado exitosamente. ${result.importedUuids} UUIDs importados. ${result.message || ''}`
: `Estado cargado exitosamente. ${result.message || 'NameManager actualizado.'}`;
alert(message);
} catch (error) { } catch (error) {
console.error('Error uploading nameManager state:', error); console.error('Error uploading nameManager state:', error);
alert('Error al cargar el estado del nameManager. Verifica que el archivo sea válido.');
// Handle different error types
if (error.message && error.message.includes('413')) {
alert('Error: El archivo es demasiado grande (máximo 50MB). Considera crear un archivo de guardado más pequeño.');
} else if (error.message && error.message.includes('400')) {
alert('Error: Archivo inválido. Asegúrate de subir un archivo .snatchSave válido.');
} else {
alert('Error al cargar el estado del nameManager. Verifica que el archivo sea válido y la conexión sea estable.');
}
} finally { } finally {
// Reset file input // Reset file input
target.value = ''; target.value = '';

View File

@@ -13,8 +13,8 @@
<span class="collapse-text">{{ filtersCollapsed ? 'Mostrar filtros' : 'Ocultar filtros' }}</span> <span class="collapse-text">{{ filtersCollapsed ? 'Mostrar filtros' : 'Ocultar filtros' }}</span>
</button> </button>
<button class="btn" @click="refreshAll" :disabled="loading">{{ loading ? 'Actualizando…' : 'Actualizar' }}</button> <button class="btn" @click="refreshAll" :disabled="loading">{{ loading ? 'Actualizando…' : 'Actualizar' }}</button>
<button class="btn" @click="downloadCSV" :disabled="loading" title="Descargar datos actuales como CSV"> <button class="btn" @click="downloadJSON" :disabled="loading" title="Descargar datos actuales como JSON">
📊 CSV 📊 JSON
</button> </button>
</div> </div>
</div> </div>
@@ -236,6 +236,11 @@ function computeMetricsFromScores() {
if (player.name) { if (player.name) {
totalPlayersWithNames++; totalPlayersWithNames++;
// Count players with shame tokens
if (player.shameTokens && player.shameTokens > 0) {
playersWithShame++;
}
// Extract scores from roomScoreHistory if available // Extract scores from roomScoreHistory if available
if (player.roomScoreHistory) { if (player.roomScoreHistory) {
player.roomScoreHistory.forEach((roomScore: any) => { player.roomScoreHistory.forEach((roomScore: any) => {
@@ -273,13 +278,13 @@ function computeMetricsFromScores() {
// Function to compute metrics for a selected player // Function to compute metrics for a selected player
function computeSelectedPlayerMetrics(uuid: string) { function computeSelectedPlayerMetrics(uuid: string) {
const playerData = allPlayersWithScores.value.find(p => p.uuid === uuid); const playerData = allPlayersWithScores.value.find(p => p.uuid === uuid);
if (!playerData?.roomScoreHistory) { if (!playerData) {
selectedPlayerMetrics.value = { selectedPlayerMetrics.value = {
players_seated: 1, players_seated: 1,
score_p1: 0, score_p1: 0,
score_p2: 0, score_p2: 0,
players_with_shame: 0, players_with_shame: 0,
players_without_shame: 0 players_without_shame: 1
}; };
return; return;
} }
@@ -289,29 +294,33 @@ function computeSelectedPlayerMetrics(uuid: string) {
let p1Count = 0; let p1Count = 0;
let p2Count = 0; let p2Count = 0;
playerData.roomScoreHistory.forEach((roomScore: any) => { const hasShame = playerData.shameTokens && playerData.shameTokens > 0;
roomScore.scores.forEach((score: any) => {
// Apply filters to scores
if (!scorePassesFilters(score, roomScore.roomId)) {
return;
}
if (score.role === 'P1') { if (playerData.roomScoreHistory) {
totalP1Scores += score.score; playerData.roomScoreHistory.forEach((roomScore: any) => {
p1Count++; roomScore.scores.forEach((score: any) => {
} else if (score.role === 'P2') { // Apply filters to scores
totalP2Scores += score.score; if (!scorePassesFilters(score, roomScore.roomId)) {
p2Count++; return;
} }
if (score.role === 'P1') {
totalP1Scores += score.score;
p1Count++;
} else if (score.role === 'P2') {
totalP2Scores += score.score;
p2Count++;
}
});
}); });
}); }
selectedPlayerMetrics.value = { selectedPlayerMetrics.value = {
players_seated: 1, players_seated: 1,
score_p1: p1Count > 0 ? Math.round((totalP1Scores / p1Count) * 10) / 10 : 0, score_p1: p1Count > 0 ? Math.round((totalP1Scores / p1Count) * 10) / 10 : 0,
score_p2: p2Count > 0 ? Math.round((totalP2Scores / p2Count) * 10) / 10 : 0, score_p2: p2Count > 0 ? Math.round((totalP2Scores / p2Count) * 10) / 10 : 0,
players_with_shame: 0, // Would need shame data players_with_shame: hasShame ? 1 : 0,
players_without_shame: 0 players_without_shame: hasShame ? 0 : 1
}; };
} }
@@ -815,86 +824,82 @@ watch(() => playersFiltered.value.length, () => {
const allPlayersActions = ref<{ uuid: string; name: string; total: number }[]>([]); const allPlayersActions = ref<{ uuid: string; name: string; total: number }[]>([]);
function downloadCSV() { function downloadJSON() {
const currentEvents = eventFilters.currentSourceEvents.value; // Create a comprehensive snapshot of current interface data
const currentData = {
// Create CSV headers metadata: {
const headers = ['Type', 'Event', 'Count', 'Round', 'GameVariant', 'PlayerUuid', 'PlayerName', 'RoomId', 'DataSource']; timestamp: new Date().toISOString(),
dataSource: eventFilters.dataSource.value,
// Create CSV rows - one row per individual event occurrence filters: {
const rows: string[][] = []; room: eventFilters.roomFilter.value,
round: eventFilters.roundFilter.value,
// Add detailed individual event rows if we have detailed data game: eventFilters.gameFilter.value,
currentEvents.forEach(event => { hasActiveFilters: eventFilters.hasActiveFilters.value,
if (EVENTS.includes(event.kind)) { filterSummary: eventFilters.filterSummary.value,
rows.push([ selectedPlayer: selectedUuid.value ? {
'event', uuid: selectedUuid.value,
event.kind, name: players.value.find(p => p.uuid === selectedUuid.value)?.name || 'Jugador'
'1', } : null,
event.round?.toString() || '', selectedRoom: selectedRoomId.value ? {
event.gameVariant || '', roomId: selectedRoomId.value,
event.playerUuid || '', name: availableRooms.value.find(r => r.roomId === selectedRoomId.value)?.name || 'Sala'
event.playerName || '', } : null
event.roomId || '',
eventFilters.dataSource.value
]);
}
});
// Also add per-player aggregated data from playersActionsByUuid
Object.entries(playersActionsByUuid.value).forEach(([uuid, counts]) => {
const player = players.value.find(p => p.uuid === uuid);
EVENTS.forEach(eventType => {
const count = counts[eventType] || 0;
if (count > 0) {
// Add one row per occurrence for proper import compatibility
for (let i = 0; i < count; i++) {
rows.push([
'event',
eventType,
'1',
'', // Round info not available in aggregated player data
'', // GameVariant info not available in aggregated player data
uuid,
player?.name || '',
'', // Room info not available in aggregated player data
'player-aggregated'
]);
}
} }
}); },
});
// Add metric data // Current chart totals being displayed
const currentMetrics = eventFilters.dataSource.value === 'aggregated' chartTotals: {
? additionalMetrics.value offers: offersTotal.value,
: additionalMetrics.value; // Same source for now responses: responsesTotal.value,
force: forceTotal.value,
shame: shameTotal.value,
report: reportTotal.value,
averageScore: averageScoreTotal.value,
totalPlayers: totalPlayersCount.value
},
METRICS.forEach(metricType => { // Global event counts (filtered)
const count = currentMetrics[metricType] || 0; globalEventCounts: eventFilters.globalEventCounts.value,
if (count > 0) {
rows.push([
'metric',
metricType,
count.toString(),
'',
'',
'',
'',
'',
eventFilters.dataSource.value
]);
}
});
// Convert to CSV string // Selected player event counts (if applicable)
const csvContent = [ selectedPlayerEventCounts: selectedUuid.value ? playerEventCounts.value : null,
headers.join(','),
...rows.map(row => row.map(cell => `"${cell}"`).join(',')) // Additional metrics being displayed
].join('\n'); additionalMetrics: additionalMetrics.value,
// Selected player metrics (if applicable)
selectedPlayerMetrics: selectedUuid.value ? selectedPlayerMetrics.value : null,
// Combined counts used in charts
combinedGlobalCounts: combinedGlobalCounts.value,
combinedPlayerCounts: selectedUuid.value ? combinedPlayerCounts.value : null,
// Players data
players: players.value,
availableRooms: availableRooms.value,
// Current filtered data if available
currentSourceEvents: eventFilters.currentSourceEvents.value,
// Per-player action counts
playersActionsByUuid: playersActionsByUuid.value,
// All players with scores data
allPlayersWithScores: allPlayersWithScores.value.map(player => ({
uuid: player.uuid,
name: player.name,
total: player.total,
shameTokens: player.shameTokens,
counts: player.counts,
roomScoreHistory: player.roomScoreHistory
}))
};
// Convert to formatted JSON string
const jsonString = JSON.stringify(currentData, null, 2);
// Create and download file // Create and download file
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const blob = new Blob([jsonString], { type: 'application/json;charset=utf-8;' });
const link = document.createElement('a'); const link = document.createElement('a');
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
link.setAttribute('href', url); link.setAttribute('href', url);
@@ -903,7 +908,7 @@ function downloadCSV() {
const timestamp = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-'); const timestamp = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-');
const dataSourceLabel = eventFilters.dataSource.value === 'aggregated' ? 'agregados' : 'activos'; const dataSourceLabel = eventFilters.dataSource.value === 'aggregated' ? 'agregados' : 'activos';
const filterLabel = eventFilters.hasActiveFilters.value ? `_${eventFilters.filterSummary.value.replace(/\s+/g, '_')}` : ''; const filterLabel = eventFilters.hasActiveFilters.value ? `_${eventFilters.filterSummary.value.replace(/\s+/g, '_')}` : '';
const filename = `datos_${dataSourceLabel}${filterLabel}_${timestamp}.csv`; const filename = `leaderboard_${dataSourceLabel}${filterLabel}_${timestamp}.json`;
link.setAttribute('download', filename); link.setAttribute('download', filename);
link.style.visibility = 'hidden'; link.style.visibility = 'hidden';

View File

@@ -938,7 +938,8 @@ async function sendPlayersActionsUpdate(client?: Response) {
total, total,
detailedHistory, detailedHistory,
rawHistory: history, rawHistory: history,
roomScoreHistory roomScoreHistory,
shameTokens: nameManager.getShameTokens(uuid)
}; };
}).filter((p: any) => p.total > 0 || p.name); }).filter((p: any) => p.total > 0 || p.name);
@@ -977,23 +978,49 @@ adminRouter.post("/admin/namemanager/import", async (req: Request, res: Response
const nameManager = NameManager.getInstance(); const nameManager = NameManager.getInstance();
const state = req.body; const state = req.body;
if (!state || !state.data) { // Validate request size and format
return res.status(400).json({ error: 'Invalid state format' }); if (!state) {
return res.status(400).json({ error: 'No data provided' });
} }
if (!state.data) {
return res.status(400).json({
error: 'Invalid state format - missing data property',
hint: 'Ensure you are uploading a valid .snatchSave file'
});
}
console.log(`[AdminAPI] Importing nameManager state - Version: ${state.version || 'unknown'}`);
nameManager.importState(state); nameManager.importState(state);
// Broadcast update to SSE clients after importing // Broadcast update to SSE clients after importing
await sendUuidsUpdate(); await sendUuidsUpdate();
await sendPlayersActionsUpdate(); await sendPlayersActionsUpdate();
const importedUuids = nameManager.getAllKnownUuids().length;
console.log(`[AdminAPI] Successfully imported nameManager state with ${importedUuids} UUIDs`);
res.json({ res.json({
success: true, success: true,
message: `NameManager state imported successfully. Version: ${state.version || 'unknown'}` message: `NameManager state imported successfully. Version: ${state.version || 'unknown'}, UUIDs: ${importedUuids}`,
importedUuids
}); });
} catch (error) { } catch (error) {
console.error('[AdminAPI] Error importing nameManager state:', error); console.error('[AdminAPI] Error importing nameManager state:', error);
res.status(500).json({ error: 'Failed to import nameManager state', details: error.message });
// Handle specific error types
if (error.message && error.message.includes('PayloadTooLargeError')) {
return res.status(413).json({
error: 'File too large',
details: 'The save file is too large to process. Maximum size: 50MB'
});
}
res.status(500).json({
error: 'Failed to import nameManager state',
details: error.message
});
} }
}); });

View File

@@ -12,7 +12,8 @@ const port = Number(process.env.PORT) || 3000;
const app = express(); const app = express();
app.use(cors()); app.use(cors());
app.use(express.json()); app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: true }));
const server = createServer(app); const server = createServer(app);
const gameServer = new Server({ const gameServer = new Server({