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: "Totales / Todos", }, ]); const HISTORICAL_SERVER_SLUGS = Object.freeze( HISTORICAL_SERVERS.map((server) => server.slug), ); const DEFAULT_HISTORICAL_SERVER = HISTORICAL_SERVER_SLUGS[0]; const SNAPSHOT_CACHE_TTL_MS = 120000; const STALE_SNAPSHOT_CACHE_TTL_MS = 30000; const NEGATIVE_SNAPSHOT_CACHE_TTL_MS = 15000; const LEADERBOARD_METRICS = Object.freeze([ { key: "kills", title: "Top kills semanales", valueHeading: "Kills", emptyMessage: "Sin datos historicos suficientes para mostrar el top de kills semanal.", }, { key: "deaths", title: "Top muertes semanales", valueHeading: "Muertes", emptyMessage: "Sin datos historicos suficientes para mostrar el top de muertes semanal.", }, { 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 semanal.", }, { key: "support", title: "Top puntos de soporte semanales", valueHeading: "Soporte", emptyMessage: "Sin datos historicos suficientes para mostrar el top de soporte semanal.", }, ]); const DEFAULT_LEADERBOARD_METRIC = LEADERBOARD_METRICS[0].key; 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 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); let activeServerSlug = normalizeServerSlug(params.get("server")); let activeLeaderboardMetric = normalizeLeaderboardMetric(params.get("metric")); let activeServerRequestId = 0; let activeLeaderboardRequestId = 0; 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, metricKey) => getCachedJson( leaderboardCache, pendingRequestCache, buildLeaderboardSnapshotKey(serverSlug, metricKey), `${backendBaseUrl}/api/historical/snapshots/weekly-leaderboard?server=${encodeURIComponent(serverSlug)}&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 activeServerLabel = getHistoricalServerLabel(activeServerSlug); syncActiveButtons(selectorButtons, activeServerSlug); syncLeaderboardTabs(leaderboardTabButtons, activeLeaderboardMetric); weeklyTitleNode.textContent = buildLeaderboardTitle( activeMetricConfig, activeServerSlug, ); 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 semanal del alcance activo..."; setSnapshotMeta(weeklySnapshotMetaNode, "Preparando snapshot semanal..."); 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, activeLeaderboardMetric), ); if (cachedLeaderboardPayload) { hydrateWeeklyLeaderboard( { status: "fulfilled", value: cachedLeaderboardPayload }, weeklyStateNode, weeklyTableNode, weeklyBodyNode, weeklyTitleNode, weeklyValueHeadingNode, weeklyWindowNoteNode, weeklySnapshotMetaNode, activeMetricConfig, ); } else { setState(weeklyStateNode, "Cargando ranking semanal..."); weeklyTableNode.hidden = true; } const cachedRecentMatchesPayload = readCachedPayload( recentMatchesCache, buildRecentMatchesSnapshotKey(activeServerSlug), ); if (cachedRecentMatchesPayload) { hydrateRecentMatches( { status: "fulfilled", value: cachedRecentMatchesPayload }, recentStateNode, recentListNode, recentSnapshotMetaNode, ); } const targetServerSlug = activeServerSlug; const targetMetric = activeLeaderboardMetric; void settlePromise(getSummarySnapshot(targetServerSlug)).then((summaryResult) => { if (!isActiveServerRequest(requestId, targetServerSlug, targetMetric)) { return; } hydrateSummary( summaryResult, summaryNode, rangeNode, summaryNoteNode, summarySnapshotMetaNode, ); }); void settlePromise(getRecentMatchesSnapshot(targetServerSlug)).then((recentMatchesResult) => { if (!isActiveServerRequest(requestId, targetServerSlug, targetMetric)) { return; } hydrateRecentMatches( recentMatchesResult, recentStateNode, recentListNode, recentSnapshotMetaNode, ); }); void settlePromise( getLeaderboardSnapshot(targetServerSlug, targetMetric), ).then((leaderboardResult) => { if ( !isActiveLeaderboardRequest( requestId, leaderboardRequestId, targetServerSlug, targetMetric, ) ) { return; } hydrateWeeklyLeaderboard( leaderboardResult, weeklyStateNode, weeklyTableNode, weeklyBodyNode, weeklyTitleNode, weeklyValueHeadingNode, weeklyWindowNoteNode, weeklySnapshotMetaNode, activeMetricConfig, ); }); }; const refreshLeaderboardContent = async () => { const requestId = activeLeaderboardRequestId + 1; activeLeaderboardRequestId = requestId; const metricConfig = getLeaderboardMetricConfig(activeLeaderboardMetric); const targetServerSlug = activeServerSlug; const targetMetric = activeLeaderboardMetric; syncLeaderboardTabs(leaderboardTabButtons, activeLeaderboardMetric); weeklyTitleNode.textContent = buildLeaderboardTitle( metricConfig, activeServerSlug, ); weeklyValueHeadingNode.textContent = metricConfig.valueHeading; const cachedPayload = readCachedPayload( leaderboardCache, buildLeaderboardSnapshotKey(targetServerSlug, targetMetric), ); if (cachedPayload) { hydrateWeeklyLeaderboard( { status: "fulfilled", value: cachedPayload }, weeklyStateNode, weeklyTableNode, weeklyBodyNode, weeklyTitleNode, weeklyValueHeadingNode, weeklyWindowNoteNode, weeklySnapshotMetaNode, metricConfig, ); return; } weeklyWindowNoteNode.textContent = "Cargando snapshot semanal del alcance activo..."; setSnapshotMeta(weeklySnapshotMetaNode, "Cargando snapshot semanal..."); setState(weeklyStateNode, "Cargando ranking semanal..."); weeklyTableNode.hidden = true; const leaderboardResult = await settlePromise( getLeaderboardSnapshot(targetServerSlug, targetMetric), ); if ( requestId !== activeLeaderboardRequestId || targetServerSlug !== activeServerSlug || targetMetric !== activeLeaderboardMetric ) { return; } hydrateWeeklyLeaderboard( leaderboardResult, weeklyStateNode, weeklyTableNode, weeklyBodyNode, weeklyTitleNode, weeklyValueHeadingNode, weeklyWindowNoteNode, weeklySnapshotMetaNode, metricConfig, ); }; selectorButtons.forEach((button) => { button.addEventListener("click", () => { const nextServerSlug = normalizeServerSlug(button.dataset.serverSlug); if (nextServerSlug === activeServerSlug) { return; } activeServerSlug = nextServerSlug; params.set("server", activeServerSlug); params.set("metric", activeLeaderboardMetric); window.history.replaceState({}, "", `?${params.toString()}`); void refreshServerContent(); }); }); leaderboardTabButtons.forEach((button) => { button.addEventListener("click", () => { const nextMetric = normalizeLeaderboardMetric(button.dataset.leaderboardMetric); if (nextMetric === activeLeaderboardMetric) { return; } activeLeaderboardMetric = nextMetric; params.set("server", activeServerSlug); params.set("metric", activeLeaderboardMetric); window.history.replaceState({}, "", `?${params.toString()}`); void refreshLeaderboardContent(); }); }); void refreshServerContent(); }); function isActiveServerRequest(requestId, serverSlug, metricKey) { return ( requestId === activeServerRequestId && serverSlug === activeServerSlug && metricKey === activeLeaderboardMetric ); } function isActiveLeaderboardRequest( serverRequestId, leaderboardRequestId, serverSlug, metricKey, ) { return ( isActiveServerRequest(serverRequestId, serverSlug, metricKey) && leaderboardRequestId === activeLeaderboardRequestId ); } function hydrateSummary(result, summaryNode, rangeNode, noteNode, snapshotMetaNode) { 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; if (!payload?.found || !summary) { renderSummaryEmpty(summaryNode); setRangeBadge(rangeNode, "Sin snapshot de resumen", false); noteNode.textContent = "Todavia no existe un snapshot de resumen listo para el alcance seleccionado."; setSnapshotMeta(snapshotMetaNode, "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, }); setRangeBadge( rangeNode, rangeLabel || "Cobertura registrada parcial", 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( "Periodo registrado", formatCoverageDays(coverage.coverage_days), ), renderSummaryCard("Primera partida", formatTimestamp(coverage.first_match_at)), renderSummaryCard("Ultima partida", formatTimestamp(coverage.last_match_at)), renderSummaryCard( "Mapas frecuentes", formatTopMaps(summary.top_maps), ), ].join(""); } function hydrateWeeklyLeaderboard( result, stateNode, tableNode, bodyNode, titleNode, valueHeadingNode, noteNode, snapshotMetaNode, metricConfig, ) { valueHeadingNode.textContent = metricConfig.valueHeading; if (result.status !== "fulfilled") { titleNode.textContent = metricConfig.title; noteNode.textContent = "No se pudo leer el snapshot semanal precalculado para esta metrica."; setSnapshotMeta(snapshotMetaNode, "Error al leer el snapshot semanal."); setState(stateNode, "No se pudo cargar el ranking semanal.", true); tableNode.hidden = true; return; } const payload = result.value?.data; titleNode.textContent = buildLeaderboardTitle(metricConfig, payload?.server_slug); noteNode.textContent = buildWeeklyWindowNote(payload); setSnapshotMeta( snapshotMetaNode, buildSnapshotMetaText(payload, "Snapshot semanal pendiente de generacion."), ); if (!payload?.found) { setState(stateNode, metricConfig.emptyMessage); tableNode.hidden = true; return; } const items = payload?.items; if (!Array.isArray(items) || items.length === 0) { setState(stateNode, metricConfig.emptyMessage); tableNode.hidden = true; return; } bodyNode.innerHTML = items .map( (item) => `
${escapeHtml(label)}
${escapeHtml(value)}