leaderboard implementado v3
This commit is contained in:
@@ -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 = '';
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -288,30 +293,34 @@ function computeSelectedPlayerMetrics(uuid: string) {
|
|||||||
let totalP2Scores = 0;
|
let totalP2Scores = 0;
|
||||||
let p1Count = 0;
|
let p1Count = 0;
|
||||||
let p2Count = 0;
|
let p2Count = 0;
|
||||||
|
|
||||||
|
const hasShame = playerData.shameTokens && playerData.shameTokens > 0;
|
||||||
|
|
||||||
playerData.roomScoreHistory.forEach((roomScore: any) => {
|
if (playerData.roomScoreHistory) {
|
||||||
roomScore.scores.forEach((score: any) => {
|
playerData.roomScoreHistory.forEach((roomScore: any) => {
|
||||||
// Apply filters to scores
|
roomScore.scores.forEach((score: any) => {
|
||||||
if (!scorePassesFilters(score, roomScore.roomId)) {
|
// Apply filters to scores
|
||||||
return;
|
if (!scorePassesFilters(score, roomScore.roomId)) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
if (score.role === 'P1') {
|
|
||||||
totalP1Scores += score.score;
|
if (score.role === 'P1') {
|
||||||
p1Count++;
|
totalP1Scores += score.score;
|
||||||
} else if (score.role === 'P2') {
|
p1Count++;
|
||||||
totalP2Scores += score.score;
|
} else if (score.role === 'P2') {
|
||||||
p2Count++;
|
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'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
});
|
|
||||||
|
// Current chart totals being displayed
|
||||||
|
chartTotals: {
|
||||||
|
offers: offersTotal.value,
|
||||||
|
responses: responsesTotal.value,
|
||||||
|
force: forceTotal.value,
|
||||||
|
shame: shameTotal.value,
|
||||||
|
report: reportTotal.value,
|
||||||
|
averageScore: averageScoreTotal.value,
|
||||||
|
totalPlayers: totalPlayersCount.value
|
||||||
|
},
|
||||||
|
|
||||||
|
// Global event counts (filtered)
|
||||||
|
globalEventCounts: eventFilters.globalEventCounts.value,
|
||||||
|
|
||||||
|
// Selected player event counts (if applicable)
|
||||||
|
selectedPlayerEventCounts: selectedUuid.value ? playerEventCounts.value : null,
|
||||||
|
|
||||||
|
// Additional metrics being displayed
|
||||||
|
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
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
// Add metric data
|
// Convert to formatted JSON string
|
||||||
const currentMetrics = eventFilters.dataSource.value === 'aggregated'
|
const jsonString = JSON.stringify(currentData, null, 2);
|
||||||
? additionalMetrics.value
|
|
||||||
: additionalMetrics.value; // Same source for now
|
|
||||||
|
|
||||||
METRICS.forEach(metricType => {
|
|
||||||
const count = currentMetrics[metricType] || 0;
|
|
||||||
if (count > 0) {
|
|
||||||
rows.push([
|
|
||||||
'metric',
|
|
||||||
metricType,
|
|
||||||
count.toString(),
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
eventFilters.dataSource.value
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Convert to CSV string
|
|
||||||
const csvContent = [
|
|
||||||
headers.join(','),
|
|
||||||
...rows.map(row => row.map(cell => `"${cell}"`).join(','))
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
// 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';
|
||||||
|
|||||||
@@ -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
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user