const HISTORICAL_SERVERS = Object.freeze([ { slug: "comunidad-hispana-01", label: "Comunidad Hispana #01", }, { slug: "comunidad-hispana-02", label: "Comunidad Hispana #02", }, { slug: "comunidad-hispana-03", label: "Comunidad Hispana #03", }, { slug: "all-servers", label: "Todos", }, ]); const HISTORICAL_SERVER_SLUGS = Object.freeze( HISTORICAL_SERVERS.map((server) => server.slug), ); const DEFAULT_HISTORICAL_SERVER = "all-servers"; const SNAPSHOT_CACHE_TTL_MS = 120000; const STALE_SNAPSHOT_CACHE_TTL_MS = 30000; const NEGATIVE_SNAPSHOT_CACHE_TTL_MS = 15000; let activeServerSlug = DEFAULT_HISTORICAL_SERVER; let activeLeaderboardMetric; let activeLeaderboardTimeframe; let activeServerRequestId = 0; let activeLeaderboardRequestId = 0; const LEADERBOARD_TIMEFRAMES = Object.freeze([ { key: "weekly", label: "Semanal", shortLabel: "semanal", }, { key: "monthly", label: "Mensual", shortLabel: "mensual", }, ]); const LEADERBOARD_METRICS = Object.freeze([ { key: "kills", title: "Top kills", valueHeading: "Kills", emptyMessage: "Sin datos historicos suficientes para mostrar este ranking de kills.", }, { key: "deaths", title: "Top muertes", valueHeading: "Muertes", emptyMessage: "Sin datos historicos suficientes para mostrar este ranking de muertes.", }, { key: "matches_over_100_kills", title: "Top partidas con 100+ kills", valueHeading: "Partidas 100+", emptyMessage: "Ningun jugador ha registrado partidas de 100+ kills en esta ventana.", }, { key: "support", title: "Top puntos de soporte", valueHeading: "Soporte", emptyMessage: "Sin datos historicos suficientes para mostrar este ranking de soporte.", }, ]); const DEFAULT_LEADERBOARD_METRIC = LEADERBOARD_METRICS[0].key; const DEFAULT_LEADERBOARD_TIMEFRAME = LEADERBOARD_TIMEFRAMES[0].key; activeLeaderboardMetric = DEFAULT_LEADERBOARD_METRIC; activeLeaderboardTimeframe = DEFAULT_LEADERBOARD_TIMEFRAME; document.addEventListener("DOMContentLoaded", () => { const backendBaseUrl = document.body.dataset.backendBaseUrl || "http://127.0.0.1:8000"; const selectorButtons = Array.from( document.querySelectorAll("[data-server-slug]"), ); const leaderboardTimeframeButtons = Array.from( document.querySelectorAll("[data-leaderboard-timeframe]"), ); const leaderboardTabButtons = Array.from( document.querySelectorAll("[data-leaderboard-metric]"), ); const summaryNode = document.getElementById("historical-summary"); const rangeNode = document.getElementById("historical-range"); const summaryNoteNode = document.getElementById("historical-summary-note"); const summarySnapshotMetaNode = document.getElementById( "historical-summary-snapshot-meta", ); const weeklyTitleNode = document.getElementById("weekly-ranking-title"); const weeklyStateNode = document.getElementById("weekly-leaderboard-state"); const weeklyTableNode = document.getElementById("weekly-leaderboard-table"); const weeklyBodyNode = document.getElementById("weekly-leaderboard-body"); const weeklyValueHeadingNode = document.getElementById("weekly-leaderboard-value-heading"); const weeklyWindowNoteNode = document.getElementById("weekly-window-note"); const weeklySnapshotMetaNode = document.getElementById( "weekly-leaderboard-snapshot-meta", ); const recentStateNode = document.getElementById("recent-matches-state"); const recentListNode = document.getElementById("recent-matches-list"); const recentNoteNode = document.getElementById("recent-matches-note"); const recentSnapshotMetaNode = document.getElementById( "recent-matches-snapshot-meta", ); const params = new URLSearchParams(window.location.search); activeServerSlug = normalizeServerSlug(params.get("server")); activeLeaderboardMetric = normalizeLeaderboardMetric(params.get("metric")); activeLeaderboardTimeframe = normalizeLeaderboardTimeframe( params.get("timeframe"), ); const summaryCache = new Map(); const recentMatchesCache = new Map(); const leaderboardCache = new Map(); const pendingRequestCache = new Map(); const getSummarySnapshot = (serverSlug) => getCachedJson( summaryCache, pendingRequestCache, buildSummarySnapshotKey(serverSlug), `${backendBaseUrl}/api/historical/snapshots/server-summary?server=${encodeURIComponent(serverSlug)}`, ); const getRecentMatchesSnapshot = (serverSlug) => getCachedJson( recentMatchesCache, pendingRequestCache, buildRecentMatchesSnapshotKey(serverSlug), `${backendBaseUrl}/api/historical/snapshots/recent-matches?server=${encodeURIComponent(serverSlug)}&limit=6`, ); const getLeaderboardSnapshot = (serverSlug, timeframeKey, metricKey) => getCachedJson( leaderboardCache, pendingRequestCache, buildLeaderboardSnapshotKey(serverSlug, timeframeKey, metricKey), `${backendBaseUrl}/api/historical/snapshots/leaderboard?server=${encodeURIComponent(serverSlug)}&timeframe=${encodeURIComponent(timeframeKey)}&metric=${encodeURIComponent(metricKey)}&limit=10`, ); const refreshServerContent = async () => { const requestId = activeServerRequestId + 1; const leaderboardRequestId = activeLeaderboardRequestId + 1; activeServerRequestId = requestId; activeLeaderboardRequestId = leaderboardRequestId; const activeMetricConfig = getLeaderboardMetricConfig(activeLeaderboardMetric); const activeTimeframeConfig = getLeaderboardTimeframeConfig( activeLeaderboardTimeframe, ); const activeServerLabel = getHistoricalServerLabel(activeServerSlug); syncActiveButtons(selectorButtons, activeServerSlug); syncLeaderboardTimeframes( leaderboardTimeframeButtons, activeLeaderboardTimeframe, ); syncLeaderboardTabs(leaderboardTabButtons, activeLeaderboardMetric); weeklyTitleNode.textContent = buildLeaderboardTitle( activeMetricConfig, activeServerSlug, activeLeaderboardTimeframe, ); weeklyValueHeadingNode.textContent = activeMetricConfig.valueHeading; setRangeBadge(rangeNode, "Cargando rango temporal", false); summaryNoteNode.textContent = `La vista esta leyendo snapshots precalculados del historico local para ${activeServerLabel}.`; setSnapshotMeta(summarySnapshotMetaNode, "Cargando snapshot de resumen..."); renderSummaryLoading(summaryNode); weeklyWindowNoteNode.textContent = "Cargando snapshot del ranking activo..."; setSnapshotMeta( weeklySnapshotMetaNode, `Preparando snapshot ${activeTimeframeConfig.shortLabel}...`, ); recentListNode.innerHTML = ""; recentNoteNode.textContent = buildRecentMatchesNote(activeServerSlug); setState(recentStateNode, "Cargando partidas recientes..."); setSnapshotMeta(recentSnapshotMetaNode, "Cargando snapshot de partidas..."); const cachedSummaryPayload = readCachedPayload( summaryCache, buildSummarySnapshotKey(activeServerSlug), ); if (cachedSummaryPayload) { hydrateSummary( { status: "fulfilled", value: cachedSummaryPayload }, summaryNode, rangeNode, summaryNoteNode, summarySnapshotMetaNode, ); } const cachedLeaderboardPayload = readCachedPayload( leaderboardCache, buildLeaderboardSnapshotKey( activeServerSlug, activeLeaderboardTimeframe, activeLeaderboardMetric, ), ); if (cachedLeaderboardPayload) { hydrateWeeklyLeaderboard( { status: "fulfilled", value: cachedLeaderboardPayload }, weeklyStateNode, weeklyTableNode, weeklyBodyNode, weeklyTitleNode, weeklyValueHeadingNode, weeklyWindowNoteNode, weeklySnapshotMetaNode, activeMetricConfig, activeLeaderboardTimeframe, ); } else { setState( weeklyStateNode, `Cargando ranking ${activeTimeframeConfig.shortLabel}...`, ); weeklyTableNode.hidden = true; } const cachedRecentMatchesPayload = readCachedPayload( recentMatchesCache, buildRecentMatchesSnapshotKey(activeServerSlug), ); if (cachedRecentMatchesPayload) { hydrateRecentMatches( { status: "fulfilled", value: cachedRecentMatchesPayload }, recentStateNode, recentListNode, recentSnapshotMetaNode, ); } const targetServerSlug = activeServerSlug; const targetTimeframe = activeLeaderboardTimeframe; const targetMetric = activeLeaderboardMetric; void settlePromise(getSummarySnapshot(targetServerSlug)).then((summaryResult) => { if ( !isActiveServerRequest( requestId, targetServerSlug, targetTimeframe, targetMetric, ) ) { return; } hydrateSummary( summaryResult, summaryNode, rangeNode, summaryNoteNode, summarySnapshotMetaNode, ); }); void settlePromise(getRecentMatchesSnapshot(targetServerSlug)).then((recentMatchesResult) => { if ( !isActiveServerRequest( requestId, targetServerSlug, targetTimeframe, targetMetric, ) ) { return; } hydrateRecentMatches( recentMatchesResult, recentStateNode, recentListNode, recentSnapshotMetaNode, ); }); void settlePromise( getLeaderboardSnapshot(targetServerSlug, targetTimeframe, targetMetric), ).then((leaderboardResult) => { if ( !isActiveLeaderboardRequest( requestId, leaderboardRequestId, targetServerSlug, targetTimeframe, targetMetric, ) ) { return; } hydrateWeeklyLeaderboard( leaderboardResult, weeklyStateNode, weeklyTableNode, weeklyBodyNode, weeklyTitleNode, weeklyValueHeadingNode, weeklyWindowNoteNode, weeklySnapshotMetaNode, activeMetricConfig, targetTimeframe, ); }); }; const refreshLeaderboardContent = async () => { const requestId = activeLeaderboardRequestId + 1; activeLeaderboardRequestId = requestId; const metricConfig = getLeaderboardMetricConfig(activeLeaderboardMetric); const timeframeConfig = getLeaderboardTimeframeConfig( activeLeaderboardTimeframe, ); const targetServerSlug = activeServerSlug; const targetTimeframe = activeLeaderboardTimeframe; const targetMetric = activeLeaderboardMetric; syncLeaderboardTimeframes( leaderboardTimeframeButtons, activeLeaderboardTimeframe, ); syncLeaderboardTabs(leaderboardTabButtons, activeLeaderboardMetric); weeklyTitleNode.textContent = buildLeaderboardTitle( metricConfig, activeServerSlug, activeLeaderboardTimeframe, ); weeklyValueHeadingNode.textContent = metricConfig.valueHeading; const cachedPayload = readCachedPayload( leaderboardCache, buildLeaderboardSnapshotKey( targetServerSlug, targetTimeframe, targetMetric, ), ); if (cachedPayload) { hydrateWeeklyLeaderboard( { status: "fulfilled", value: cachedPayload }, weeklyStateNode, weeklyTableNode, weeklyBodyNode, weeklyTitleNode, weeklyValueHeadingNode, weeklyWindowNoteNode, weeklySnapshotMetaNode, metricConfig, targetTimeframe, ); return; } weeklyWindowNoteNode.textContent = "Cargando snapshot del ranking activo..."; setSnapshotMeta( weeklySnapshotMetaNode, `Cargando snapshot ${timeframeConfig.shortLabel}...`, ); setState( weeklyStateNode, `Cargando ranking ${timeframeConfig.shortLabel}...`, ); weeklyTableNode.hidden = true; const leaderboardResult = await settlePromise( getLeaderboardSnapshot(targetServerSlug, targetTimeframe, targetMetric), ); if ( requestId !== activeLeaderboardRequestId || targetServerSlug !== activeServerSlug || targetTimeframe !== activeLeaderboardTimeframe || targetMetric !== activeLeaderboardMetric ) { return; } hydrateWeeklyLeaderboard( leaderboardResult, weeklyStateNode, weeklyTableNode, weeklyBodyNode, weeklyTitleNode, weeklyValueHeadingNode, weeklyWindowNoteNode, weeklySnapshotMetaNode, metricConfig, targetTimeframe, ); }; selectorButtons.forEach((button) => { button.addEventListener("click", () => { const nextServerSlug = normalizeServerSlug(button.dataset.serverSlug); if (nextServerSlug === activeServerSlug) { return; } activeServerSlug = nextServerSlug; params.set("server", activeServerSlug); params.set("timeframe", activeLeaderboardTimeframe); params.set("metric", activeLeaderboardMetric); window.history.replaceState({}, "", `?${params.toString()}`); void refreshServerContent(); }); }); leaderboardTimeframeButtons.forEach((button) => { button.addEventListener("click", () => { const nextTimeframe = normalizeLeaderboardTimeframe( button.dataset.leaderboardTimeframe, ); if (nextTimeframe === activeLeaderboardTimeframe) { return; } activeLeaderboardTimeframe = nextTimeframe; params.set("server", activeServerSlug); params.set("timeframe", activeLeaderboardTimeframe); params.set("metric", activeLeaderboardMetric); window.history.replaceState({}, "", `?${params.toString()}`); void refreshLeaderboardContent(); }); }); leaderboardTabButtons.forEach((button) => { button.addEventListener("click", () => { const nextMetric = normalizeLeaderboardMetric(button.dataset.leaderboardMetric); if (nextMetric === activeLeaderboardMetric) { return; } activeLeaderboardMetric = nextMetric; params.set("server", activeServerSlug); params.set("timeframe", activeLeaderboardTimeframe); params.set("metric", activeLeaderboardMetric); window.history.replaceState({}, "", `?${params.toString()}`); void refreshLeaderboardContent(); }); }); void refreshServerContent(); }); function isActiveServerRequest(requestId, serverSlug, timeframeKey, metricKey) { return ( requestId === activeServerRequestId && serverSlug === activeServerSlug && timeframeKey === activeLeaderboardTimeframe && metricKey === activeLeaderboardMetric ); } function isActiveLeaderboardRequest( serverRequestId, leaderboardRequestId, serverSlug, timeframeKey, metricKey, ) { return ( isActiveServerRequest(serverRequestId, serverSlug, timeframeKey, metricKey) && leaderboardRequestId === activeLeaderboardRequestId ); } function hydrateSummary(result, summaryNode, rangeNode, noteNode, snapshotMetaNode) { const emptyState = getHistoricalEmptyState(activeServerSlug); if (result.status !== "fulfilled") { renderSummaryError(summaryNode); setRangeBadge(rangeNode, "Snapshot de resumen no disponible", false); noteNode.textContent = "No se pudo leer el resumen precalculado para el alcance seleccionado."; setSnapshotMeta(snapshotMetaNode, "Error al leer el snapshot de resumen."); return; } const payload = result.value?.data; const summary = payload?.item; const hasHistoricalData = Number(summary?.imported_matches_count ?? summary?.matches_count ?? 0) > 0; if (!payload?.found || !summary || !hasHistoricalData) { renderSummaryEmpty(summaryNode, emptyState.summaryMessage); setRangeBadge(rangeNode, emptyState.rangeLabel, false); noteNode.textContent = emptyState.summaryNote; setSnapshotMeta( snapshotMetaNode, payload?.generated_at ? buildSnapshotMetaText(payload, "Snapshot de resumen pendiente de generacion.") : "Snapshot de resumen pendiente de generacion.", ); return; } const coverage = summary.coverage || {}; const timeRange = summary.time_range || {}; const rangeLabel = buildCoverageBadgeLabel(coverage, { start: payload?.source_range_start || timeRange.start, end: payload?.source_range_end || timeRange.end, }, summary.server?.slug); setRangeBadge( rangeNode, rangeLabel || "Cobertura historica disponible", coverage.status === "week-plus" && !payload?.is_stale, ); noteNode.textContent = buildSummaryNote( "snapshot-precomputed", 7, coverage, summary.server?.slug, ); setSnapshotMeta( snapshotMetaNode, buildSnapshotMetaText(payload, "Snapshot de resumen sin timestamp."), ); summaryNode.innerHTML = [ renderSummaryCard("Servidor", summary.server?.name || "Servidor no disponible"), renderSummaryCard( "Partidas registradas", formatNumber(summary.imported_matches_count ?? summary.matches_count), ), renderSummaryCard("Jugadores unicos", formatNumber(summary.unique_players)), renderSummaryCard( "Cobertura historica", buildCoveragePeriodLabel(coverage, timeRange, summary.server?.slug), ), renderSummaryCard("Inicio de registro", formatTimestamp(coverage.first_match_at)), renderSummaryCard("Ultimo cierre", formatTimestamp(coverage.last_match_at)), renderSummaryCard( "Mapas frecuentes", formatTopMaps(summary.top_maps), ), ].join(""); } function hydrateWeeklyLeaderboard( result, stateNode, tableNode, bodyNode, titleNode, valueHeadingNode, noteNode, snapshotMetaNode, metricConfig, timeframeKey, ) { const targetServerSlug = result.value?.data?.server_slug || activeServerSlug; const resolvedTimeframeKey = result.value?.data?.timeframe || timeframeKey; valueHeadingNode.textContent = metricConfig.valueHeading; if (result.status !== "fulfilled") { titleNode.textContent = buildLeaderboardTitle( metricConfig, targetServerSlug, resolvedTimeframeKey, ); noteNode.textContent = "No se pudo leer el snapshot precalculado para esta metrica."; setSnapshotMeta(snapshotMetaNode, "Error al leer el snapshot del ranking."); setState( stateNode, `No se pudo cargar el ranking ${getLeaderboardTimeframeConfig(resolvedTimeframeKey).shortLabel}.`, true, ); tableNode.hidden = true; return; } const payload = result.value?.data; titleNode.textContent = buildLeaderboardTitle( metricConfig, payload?.server_slug, payload?.timeframe || resolvedTimeframeKey, ); noteNode.textContent = buildWeeklyWindowNote(payload); setSnapshotMeta( snapshotMetaNode, buildSnapshotMetaText(payload, "Snapshot del ranking pendiente de generacion."), ); if (!payload?.found) { setState( stateNode, buildLeaderboardEmptyMessage( metricConfig, targetServerSlug, payload?.timeframe || resolvedTimeframeKey, ), ); tableNode.hidden = true; return; } const items = payload?.items; if (!Array.isArray(items) || items.length === 0) { setState( stateNode, buildLeaderboardEmptyMessage( metricConfig, targetServerSlug, payload?.timeframe || resolvedTimeframeKey, ), ); tableNode.hidden = true; return; } bodyNode.innerHTML = items .map( (item) => `
${escapeHtml(label)}
${escapeHtml(value)}