From 5e8dcabd168f8d943abb8361c195832596932a65 Mon Sep 17 00:00:00 2001 From: devRaGonSa Date: Mon, 8 Jun 2026 14:33:22 +0200 Subject: [PATCH] Connect annual ranking snapshot to Stats frontend --- ...nual-ranking-snapshot-to-stats-frontend.md | 125 +++++++++++++++ frontend/assets/css/styles.css | 28 ++++ frontend/assets/js/stats.js | 149 +++++++++++++++++- frontend/stats.html | 34 ++-- 4 files changed, 326 insertions(+), 10 deletions(-) create mode 100644 ai/tasks/done/TASK-170-connect-annual-ranking-snapshot-to-stats-frontend.md diff --git a/ai/tasks/done/TASK-170-connect-annual-ranking-snapshot-to-stats-frontend.md b/ai/tasks/done/TASK-170-connect-annual-ranking-snapshot-to-stats-frontend.md new file mode 100644 index 0000000..5d17640 --- /dev/null +++ b/ai/tasks/done/TASK-170-connect-annual-ranking-snapshot-to-stats-frontend.md @@ -0,0 +1,125 @@ +--- +id: TASK-170 +title: Connect annual ranking snapshot to Stats frontend +status: done +type: frontend +team: Frontend Senior +supporting_teams: [] +roadmap_item: foundation +priority: medium +--- + +# TASK-170 - Connect annual ranking snapshot to Stats frontend + +## Goal + +Conectar el bloque anual top 20 del módulo Stats con la API pública ya creada: + +- `GET /api/stats/rankings/annual?year=&server_id=&metric=kills&limit=20` + +## Context + +Después de implementar el endpoint anual en backend y diseñar la integración frontend de Stats, esta tarea conecta el bloque de placeholder con datos reales de snapshots precomputados y mantiene el comportamiento por estados sin romper la sección actual. + +No se implementan cambios de backend ni migraciones en esta tarea. + +## Steps + +1. Revisar los archivos obligatorios antes de iniciar cambios: + - `AGENTS.md` + - `ai/repo-context.md` + - `ai/architecture-index.md` + - `docs/stats-section-functional-plan.md` + - `docs/stats-frontend-integration-plan.md` + - `docs/annual-ranking-snapshot-schema-plan.md` + - `frontend/stats.html` + - `frontend/assets/js/stats.js` + - `frontend/assets/css/styles.css` + - `backend/app/routes.py` + - `backend/app/payloads.py` + - `backend/app/rcon_annual_rankings.py` +2. Mantener el placeholder de ranking anual en el HTML, reemplazándolo por estados y tabla dinámica del endpoint. +3. Implementar consumo del endpoint anual con estado por: + - ready + - missing + - error/backend no disponible +4. Mantener `metric=kills` y `limit=20` fijos en V1. +5. Validar carga/estado con `node --check` y pruebas de endpoint si hay backend. + +## Files to Read First + +- `AGENTS.md` +- `ai/repo-context.md` +- `ai/architecture-index.md` +- `docs/stats-section-functional-plan.md` +- `docs/stats-frontend-integration-plan.md` +- `docs/annual-ranking-snapshot-schema-plan.md` +- `frontend/stats.html` +- `frontend/assets/js/stats.js` +- `frontend/assets/css/styles.css` +- `backend/app/routes.py` +- `backend/app/payloads.py` +- `backend/app/rcon_annual_rankings.py` + +## Expected Files to Modify + +- `frontend/stats.html` +- `frontend/assets/js/stats.js` +- `frontend/assets/css/styles.css` (solo si se requiere ajuste visual para el bloque anual) +- `ai/tasks/pending/TASK-170-connect-annual-ranking-snapshot-to-stats-frontend.md` (estado + outcome) + +## Constraints + +- Mantener frontend vanilla sin frameworks ni cambios de backend. +- No crear migraciones. +- No modificar `frontend/assets/js/partida-actual.js`. +- No modificar `frontend/assets/img/clans/bxb.png`. +- No reintroducir Comunidad Hispana #03. +- Mantener el bloque anual como consumo de snapshot (`ready`, `missing` o backend no disponible). +- No implementar generador anual desde frontend. + +## Validation + +Antes de cerrar: + +- `node --check frontend/assets/js/stats.js` +- (si aplica) `python -m http.server` y comprobar HTTP 200 para `frontend/stats.html` y `frontend/assets/js/stats.js`. +- Consumir `GET /api/stats/rankings/annual` desde la UI cuando backend local esté disponible. +- Verificar caso con snapshot existente y caso sin snapshot (`snapshot_status="missing"`). +- `scripts/run-integration-tests.ps1` si aplica. +- `git diff --name-only` y validar que el alcance no excede la task. +- `git status --short --untracked-files=all` y confirmar que no se tocó `partida-actual.js` ni `img/clans/bxb.png`. + +## Outcome + +- Archivos modificados: + - `frontend/stats.html` + - `frontend/assets/js/stats.js` + - `frontend/assets/css/styles.css` (ajustes del bloque anual) + - `ai/tasks/done/TASK-170-connect-annual-ranking-snapshot-to-stats-frontend.md` +- Endpoint consumido: + - `GET /api/stats/rankings/annual?year=&server_id=&metric=kills&limit=20` +- Comportamiento `snapshot_status=ready`: + - Muestra el bloque de ranking anual con filas del top (`ranking_position`, `player_name`, `metric_value`, `matches_considered`, `kills`, `deaths`, `teamkills`, `kd_ratio`), metadatos de servidor, fuente y fecha. +- Comportamiento `snapshot_status=missing`: + - Muestra estado informativo de no disponibilidad de ranking para el año seleccionado. + - No lanza error ni bloquea el resto del flujo. +- Comportamiento con backend no disponible: + - El estado general y anual muestran error (`Backend no disponible...`) y no se hacen cambios de UI inconsistentes. +- Validaciones ejecutadas: + - `node --check frontend/assets/js/stats.js` + - `powershell -ExecutionPolicy Bypass -File scripts/run-integration-tests.ps1` + - Servido local con `python -m http.server` y comprobación HTTP 200 de `frontend/stats.html` y `frontend/assets/js/stats.js` + - `git status --short --untracked-files=all` + - `git diff --name-only` +- Limitaciones conocidas: + - No se pudo validar integración real contra backend porque `http://127.0.0.1:8000` no estaba disponible en este entorno. + - Los textos en este bloque se mantienen en ASCII para evitar cambios de encoding. +- Siguiente tarea recomendada: + - Conectar la vista de `Stats` con `frontend/index.html`/`frontend/historico.html` si aplica y/o añadir mejoras visuales en el bloque anual. + + +## Change Budget + +- Preferir cambios < 5 archivos y < 200 líneas. +- Mantener cambio pequeño y verificable. diff --git a/frontend/assets/css/styles.css b/frontend/assets/css/styles.css index f395b04..961ddaf 100644 --- a/frontend/assets/css/styles.css +++ b/frontend/assets/css/styles.css @@ -1034,6 +1034,34 @@ h2 { font-size: 0.85rem; } +.stats-annual-table-wrap { + display: grid; + gap: 10px; +} + +.stats-annual-meta { + display: grid; + gap: 4px; + margin: 0 0 8px; +} + +.stats-annual-item { + padding: 10px 12px; + border: 1px solid rgba(159, 168, 141, 0.22); + border-radius: 14px; + margin-bottom: 8px; + background: + linear-gradient(180deg, rgba(28, 34, 25, 0.82), rgba(12, 15, 11, 0.92)); +} + +.stats-annual-item p { + margin: 0 0 6px; +} + +.stats-annual-sub { + color: var(--muted); +} + .stats-summary-grid { display: grid; gap: 12px; diff --git a/frontend/assets/js/stats.js b/frontend/assets/js/stats.js index d700997..1834cec 100644 --- a/frontend/assets/js/stats.js +++ b/frontend/assets/js/stats.js @@ -12,11 +12,28 @@ document.addEventListener("DOMContentLoaded", () => { const summaryGrid = document.getElementById("stats-summary-grid"); const weeklySummaryNode = document.getElementById("stats-weekly-summary"); const monthlySummaryNode = document.getElementById("stats-monthly-summary"); + const annualForm = document.getElementById("stats-annual-form"); + const annualYearInput = document.getElementById("stats-annual-year"); + const annualStateNode = document.getElementById("stats-annual-state"); + const annualContentNode = document.getElementById("stats-annual-content"); + const annualDefaultYear = new Date().getUTCFullYear(); + const annualMetric = "kills"; + const annualLimit = 20; + const annualServerId = "all"; let isBackendOnline = false; setBackendState("Comprobando disponibilidad del backend", false); + if (annualYearInput) { + annualYearInput.value = String(annualDefaultYear); + } refreshBackendHealth(); + if (annualForm && annualYearInput) { + annualForm.addEventListener("submit", (event) => { + event.preventDefault(); + void loadAnnualRanking(); + }); + } if (searchForm && searchInput) { searchForm.addEventListener("submit", (event) => { @@ -46,6 +63,7 @@ document.addEventListener("DOMContentLoaded", () => { searchStateNode.textContent = "Backend no disponible. Reintenta en unos segundos."; searchStateNode.className = "stats-state stats-state--error"; } + setAnnualState("error", "Backend no disponible. No se puede cargar ranking anual."); } async function refreshBackendHealth() { @@ -57,6 +75,8 @@ document.addEventListener("DOMContentLoaded", () => { const payload = await response.json(); if (payload && payload.status === "ok") { setBackendState("Backend operativo", true); + setAnnualState("loading", "Cargando ranking anual..."); + void loadAnnualRanking(); return; } throw new Error("Unexpected health payload"); @@ -116,6 +136,126 @@ document.addEventListener("DOMContentLoaded", () => { } } + async function loadAnnualRanking() { + if (!annualForm || !annualYearInput || !annualContentNode) { + return; + } + if (!isBackendOnline) { + markAsBackendUnavailable(); + return; + } + + const year = resolveAnnualYear(); + if (year === null) { + setAnnualState("error", "El ano ingresado no es valido."); + return; + } + + setAnnualState("loading", "Cargando ranking anual..."); + annualContentNode.innerHTML = ""; + + try { + const annualUrl = + `${backendBaseUrl}/api/stats/rankings/annual?` + + `year=${encodeURIComponent(year)}&` + + `server_id=${encodeURIComponent(annualServerId)}&` + + `metric=${encodeURIComponent(annualMetric)}&` + + `limit=${encodeURIComponent(annualLimit)}`; + + const annualResponse = await fetch(annualUrl); + if (!annualResponse.ok) { + throw new Error(`Annual ranking request failed with ${annualResponse.status}`); + } + + const payload = await annualResponse.json(); + if (!payload || payload.status !== "ok") { + throw new Error(payload?.message || "Respuesta de ranking anual invalida"); + } + renderAnnualRanking(payload.data || {}); + } catch (error) { + console.warn("Annual ranking failed", error); + markAsBackendUnavailable(); + setAnnualState("error", "Error al cargar ranking anual."); + annualContentNode.innerHTML = ""; + } + } + + function resolveAnnualYear() { + const normalized = Number.parseInt(String(annualYearInput?.value || "").trim(), 10); + if (!Number.isFinite(normalized) || normalized <= 0) { + return null; + } + return normalized; + } + + function renderAnnualRanking(data) { + if (!annualContentNode || !annualStateNode) { + return; + } + + const snapshotStatus = String(data.snapshot_status || "").toLowerCase(); + const items = normalizeArray(data.items); + const limit = safeInt(data.limit, 0); + const serverId = String(data.server_id || annualServerId); + const sourceMatches = safeInt(data.source_matches_count, 0); + const generatedAt = formatDateTime(data.generated_at); + + if (snapshotStatus !== "ready" || !items.length) { + const isReadyButEmpty = snapshotStatus === "ready" && !items.length; + setAnnualState( + "neutral", + isReadyButEmpty + ? "Ranking anual listo pero sin resultados para el ano seleccionado." + : "No hay ranking anual generado para el ano seleccionado.", + ); + if (!items.length) { + annualContentNode.innerHTML = ""; + } + return; + } + + setAnnualState( + "neutral", + `Ranking anual listo para ${serverId}. Generado: ${generatedAt || "sin marca de tiempo"}.`, + ); + + annualContentNode.innerHTML = ` +
+

Top ${limit} anual

+
+

Servidor: ${escapeHtml(serverId)}

+

Metrica: kills

+

Partidas fuente: ${safeInt(sourceMatches, 0)}

+

Actualizado: ${escapeHtml(generatedAt || "No disponible")}

+
+ ${renderAnnualRows(items)} +
+ `; + } + + function renderAnnualRows(items) { + return items + .map((item) => { + const rank = safeInt(item.ranking_position, 0); + const playerId = escapeHtml(String(item.player_id || "")); + const playerName = escapeHtml(String(item.player_name || "Jugador sin nombre")); + const metricValue = safeInt(item.metric_value, 0); + const matches = safeInt(item.matches_considered, 0); + const kills = safeInt(item.kills, 0); + const deaths = safeInt(item.deaths, 0); + const teamkills = safeInt(item.teamkills, 0); + const kd = safeDecimal(item.kd_ratio, 2, "0.00"); + return ` +
+

#${rank} ${playerName} (ID: ${playerId})

+

Valor: ${metricValue} · Partidas: ${matches}

+

Kills: ${kills} · Deaths: ${deaths} · Teamkills: ${teamkills} · K/D: ${kd}

+
+ `; + }) + .join(""); + } + function renderResultItem(item) { const playerId = escapeHtml(String(item.player_id || "")); const playerName = escapeHtml(String(item.player_name || "Jugador sin nombre")); @@ -235,6 +375,14 @@ document.addEventListener("DOMContentLoaded", () => { searchStateNode.className = `stats-state stats-state--${state}`; } + function setAnnualState(state, message) { + if (!annualStateNode) { + return; + } + annualStateNode.textContent = message; + annualStateNode.className = `stats-state stats-state--${state}`; + } + function setProfileState(state, message) { if (!profileStateNode || !profilePanel) { return; @@ -336,4 +484,3 @@ document.addEventListener("DOMContentLoaded", () => { .replaceAll("'", "'"); } }); - diff --git a/frontend/stats.html b/frontend/stats.html index dad3dbb..08efacf 100644 --- a/frontend/stats.html +++ b/frontend/stats.html @@ -5,7 +5,7 @@ Stats - HLL Vietnam @@ -16,7 +16,7 @@
-

Sección Stats

+

Seccion Stats

Estadisticas Personales @@ -48,7 +48,7 @@

Buscar por nombre o ID

- Usa minimo 1 carácter para comenzar la consulta. + Usa minimo 1 caracter para comenzar la consulta.