Clean ranking UI and rename false KPM metric

This commit is contained in:
devRaGonSa
2026-06-10 11:57:42 +02:00
parent 31ca3297cd
commit 6a27f4c2b5
4 changed files with 275 additions and 122 deletions

View File

@@ -0,0 +1,236 @@
---
id: TASK-216
title: Clean ranking UI and fix KPM metric
status: done
type: frontend
team: Frontend Senior
supporting_teams:
- Backend Senior
- Arquitecto Python
- Experto en interfaz
roadmap_item: foundation
priority: high
---
# TASK-216 - Clean ranking UI and fix KPM metric
## Goal
Limpiar la UX de `frontend/ranking.html`, eliminar estados y controles redundantes, corregir el uso conceptual de KPM y ampliar el ranking anual solo para metricas soportadas por snapshots/read models seguros sin introducir lecturas runtime pesadas en requests publicos.
## Context
Despues de `TASK-213`, la vista de ranking ya carga rapido y concentra filtros + tabla dentro de una sola card. Sin embargo, la revision visual detecto varios residuos de UX y un problema funcional serio: la interfaz seguia mostrando mensajes redundantes, mantenia un boton `Actualizar ranking` innecesario y exponia `KPM` usando un valor que correspondia a `kills_per_match`.
En paralelo, `TASK-211` dejo la lectura publica annual optimizada para snapshots anuales, pero `backend/app/rcon_annual_rankings.py` sigue restringiendo annual a `kills`. Esta task revisa ese limite sin introducir lecturas publicas runtime sobre `rcon_match_player_stats`.
Preserve the current product identity: Spanish-speaking HLL Vietnam community, military/Vietnam/tactical/sober visual direction and controlled repository evolution.
## Steps
1. Leer primero los archivos listados y confirmar el estado actual de UI, payloads y snapshots de ranking.
2. Auditar si existe tiempo jugado real por jugador antes de tocar la UI.
3. Limpiar la UI de `ranking.html` y `ranking.js`:
- eliminar del DOM visible el texto `El ranking expone los resultados de los lideres. Para busqueda individual usa Estadisticas.`
- eliminar el estado informativo tipo `Kills listo en semanal para Todos los servidores.`
- mantener solo estados utiles de loading, error y metadatos compactos
- quitar el boton `Actualizar ranking`
- cargar automaticamente al entrar y al cambiar periodo, servidor, metrica o limite
- mover `Buscar jugador en Estadisticas` por encima de los filtros dentro de la misma card
4. Ajustar el modo annual en frontend:
- no mostrar input de ano
- usar `2026` internamente en JS
- no mostrar mensaje de limitacion anual a kills
5. No exponer metricas annual que no tengan snapshots propios y seguros.
6. Documentar la decision final sobre KPM y el diseno futuro de `player_active_seconds`.
## Files to Read First
- `AGENTS.md`
- `ai/repo-context.md`
- `ai/architecture-index.md`
- `frontend/ranking.html`
- `frontend/assets/js/ranking.js`
- `backend/app/rcon_annual_rankings.py`
Tambien se revisaron:
- `frontend/assets/css/styles.css`
- `backend/app/payloads.py`
- `backend/app/postgres_rcon_storage.py`
- `backend/app/rcon_historical_leaderboards.py`
- `backend/app/rcon_historical_player_stats.py`
- `backend/app/rcon_admin_log_storage.py`
- `backend/app/rcon_admin_log_materialization.py`
- `backend/app/historical_runner.py`
- `ai/tasks/done/TASK-211-optimize-annual-ranking-read-path.md`
- `ai/tasks/done/TASK-213-fix-ranking-layout-and-loading-state.md`
- `ai/tasks/done/TASK-215-optimize-weekly-monthly-ranking-read-path.md`
- `docs/PERFORMANCE_PUBLIC_QUERY_AUDIT.md`
## Expected Files to Modify
- `frontend/ranking.html`
- `frontend/assets/js/ranking.js`
- `frontend/assets/css/styles.css`
- `ai/tasks/done/TASK-216-clean-ranking-ui-and-fix-kpm-metric.md`
## Constraints
- Keep the change minimal.
- Preserve HLL Vietnam project identity.
- Do not introduce unnecessary frameworks or dependencies.
- Do not overwrite repository-specific context with generic platform template text.
- No ejecutar `ai-platform run`.
- No tocar `frontend/assets/img/weapons/`.
- No tocar SVGs.
- No modificar imagenes fisicas.
- No tocar `ai/system-metrics.md`.
- No reactivar Elo/MMR.
- No reintroducir Comunidad Hispana #03.
- No hacer `push`.
- No hacer `commit` en esta task.
- No introducir runtime pesado ni fallback publico sobre `rcon_match_player_stats`.
- La lectura publica annual debe seguir leyendo unicamente:
- `rcon_annual_ranking_snapshots`
- `rcon_annual_ranking_snapshot_items`
- Weekly/monthly/annual deben seguir priorizando read models/snapshots con `fallback_used = false` en la ruta publica normal.
- No aceptar que `kills_per_match` se muestre como `KPM`.
- Si no existe tiempo jugado fiable materializado, no inventar `KPM`.
- Mantener compatibilidad con apertura directa en navegador cuando aplique.
## Validation
Before completing the task ensure:
- `node --check frontend/assets/js/ranking.js`
- validar endpoints:
- `/api/ranking?timeframe=weekly&metric=kills&limit=20`
- `/api/ranking?timeframe=monthly&metric=kills&limit=20`
- `/api/ranking?timeframe=annual&metric=kills&limit=20&year=2026`
- por inspeccion visual o revision DOM:
- no aparece boton `Actualizar ranking`
- `Buscar jugador en Estadisticas` queda encima de filtros y dentro de la misma card
- no aparecen los textos redundantes eliminados
- annual no muestra input de ano
- annual no muestra mensaje de limitacion a kills
- la columna `KPM` no aparece si no existe KPM real
- si no hay minutos reales, aparece `Kills/partida`
- `git diff --name-only` matches the expected scope
- no unrelated files were modified
## Outcome
Archivos modificados por esta task:
- `frontend/ranking.html`
- `frontend/assets/js/ranking.js`
- `frontend/assets/css/styles.css`
- `ai/tasks/done/TASK-216-clean-ranking-ui-and-fix-kpm-metric.md`
Resultado de auditoria KPM:
- No existe hoy un campo materializado y fiable de tiempo jugado real por jugador listo para el ranking publico.
- Se encontro `rcon_player_profile_snapshots.play_time`, pero es texto procedente de snapshots de perfil, no un campo numerico normalizado por jugador/partida ni agregado en los read models publicos de ranking.
- Se encontraron `rcon_match_player_stats.first_seen_server_time` y `rcon_match_player_stats.last_seen_server_time`, pero representan presencia observada por eventos dentro de una partida, no minutos reales jugados materializados como contrato estable de ranking.
- Se encontraron `rcon_materialized_matches.started_at`, `ended_at`, `started_server_time` y `ended_server_time`, pero son duracion de partida o ventana de partida, no tiempo real por jugador.
- Las tablas `ranking_snapshot_items`, `rcon_annual_ranking_snapshot_items` y `player_period_stats` no tienen `player_active_seconds`, `minutes_played`, `seconds_played` ni equivalente fiable.
Decision final sobre KPM:
- No se implemento KPM real.
- Se elimino el uso visible de `KPM` en `ranking.html` y `ranking.js`.
- `kills_per_match` se conserva solo bajo el nombre `Kills/partida`.
- No se reutilizo `kills_per_match` como kills por minuto.
Propuesta para KPM real futuro:
- Crear `player_active_seconds` como campo numerico base por jugador y partida.
- Ubicacion preferida: `rcon_match_player_stats`, si se acepta extender la tabla materializada principal.
- Alternativa: nueva tabla derivada `rcon_match_player_presence` con `target_key`, `match_key`, `player_id`, `first_seen_server_time`, `last_seen_server_time`, `active_seconds`, `quality`.
- Relleno exacto: eventos `connected` y `disconnected` dentro de limites `match_start`/`match_end`.
- Relleno razonable: presencia observada por eventos `kill`, `team_switch`, `chat`, `message`, acotada por `first_seen_server_time` y `last_seen_server_time`.
- Relleno estimado: duracion completa de partida solo como calidad `estimated`, no apta para publicar como KPM real sin etiqueta.
- Criterios de calidad propuestos:
- `exact`: join/leave real dentro de partida.
- `observed`: presencia inferida por eventos del jugador.
- `estimated`: duracion completa de partida.
- `unknown`: no calcular.
- Read models a extender cuando exista el campo:
- `player_period_stats`
- `ranking_snapshot_items`
- `rcon_annual_ranking_snapshot_items`
- Task futura recomendada: `TASK-XXX-materialize-player-active-seconds-for-real-kpm`.
Decision sobre metricas annual:
- No se ampliaron metricas annual en esta task.
- Annual sigue exponiendo solo `kills` porque es la unica metrica con snapshot anual seguro ya soportado por `rcon_annual_rankings.py`.
- No se usa snapshot top kills para representar `deaths`, `teamkills`, `matches_considered`, `kd_ratio` ni `kills_per_match`.
- No se introdujo runtime publico pesado ni fallback sobre `rcon_match_player_stats`.
Cambios de UI aplicados:
- Se elimino el texto redundante de introduccion dentro de la card.
- Se elimino el boton `Actualizar ranking`.
- La tabla sigue cargando automaticamente al entrar y al cambiar periodo, servidor, metrica o limite.
- `Buscar jugador en Estadisticas` queda encima de los filtros dentro de la misma card.
- En annual no hay input visible de ano.
- `ranking.js` usa `2026` internamente para annual.
- Se elimino el mensaje visible de annual limitado a kills.
- Se elimino el estado de exito tipo `Kills listo en semanal para Todos los servidores.`
- La metrica `kills_per_match` se muestra como `Kills/partida`.
Comandos de produccion:
- No hace falta regenerar snapshots para este cambio de UI.
- Si falta el snapshot anual existente de kills para 2026, regenerarlo con:
```bash
docker compose exec backend python -m app.rcon_annual_rankings generate --year 2026 --server-key all-servers --metric kills --limit 30 --replace-existing
```
- No ejecutar comandos de metricas annual adicionales hasta que existan snapshots propios por metrica.
Validaciones ejecutadas:
- `node --check frontend/assets/js/ranking.js`
- Revision por busqueda de que `frontend/ranking.html` y `frontend/assets/js/ranking.js` ya no contienen `KPM`, `Actualizar ranking`, `ranking-year`, `ranking-submit`, `ranking-filter-note`, `El ranking expone` ni `listo en`.
- Intento de validacion HTTP local contra:
- `/api/ranking?timeframe=weekly&metric=kills&limit=20`
- `/api/ranking?timeframe=monthly&metric=kills&limit=20`
- `/api/ranking?timeframe=annual&metric=kills&limit=20&year=2026`
- El backend local no estaba disponible en `http://127.0.0.1:8000`; las tres peticiones fallaron con `No es posible conectar con el servidor remoto`.
- Revision de alcance con `git status --short` y `git diff --name-only`.
Validaciones no ejecutadas:
- No se ejecuto `compileall` ni `unittest` porque no se tocaron archivos backend.
Tiempos obtenidos:
- No se obtuvieron tiempos nuevos de endpoint porque el backend local no estaba escuchando.
- Intentos HTTP fallidos por conexion:
- weekly kills top 20: `2156.7 ms`
- monthly kills top 20: `2033.7 ms`
- annual kills top 20 2026: `2046.1 ms`
- Referencia previa de `TASK-213`: annual kills 2026 ~39 ms, weekly/monthly kills < 130 ms.
Confirmacion de exclusiones:
- No se ejecuto `ai-platform run`.
- No se hizo push.
- No se hizo commit.
- No se tocaron assets de armas.
- No se tocaron SVGs.
- No se modificaron imagenes fisicas.
- No se toco `ai/system-metrics.md`.
- No se reactivo Elo/MMR.
- No se reintrodujo Comunidad Hispana #03.
- No se incluyeron cambios previos no relacionados.
## Change Budget
- Archivos de producto modificados: 3.
- Sin cambios backend.
- El alcance se mantuvo dentro de la limpieza UI y documentacion de task.

View File

@@ -1429,6 +1429,10 @@ h2 {
margin-top: 20px; margin-top: 20px;
} }
.ranking-secondary-action {
margin: 0 0 18px;
}
.ranking-form__grid { .ranking-form__grid {
display: grid; display: grid;
gap: 12px; gap: 12px;

View File

@@ -5,9 +5,6 @@
const serverSelect = document.getElementById("ranking-server"); const serverSelect = document.getElementById("ranking-server");
const metricSelect = document.getElementById("ranking-metric"); const metricSelect = document.getElementById("ranking-metric");
const limitSelect = document.getElementById("ranking-limit"); const limitSelect = document.getElementById("ranking-limit");
const submitButton = document.getElementById("ranking-submit");
const yearWrap = document.getElementById("ranking-year-wrap");
const yearInput = document.getElementById("ranking-year");
const stateNode = document.getElementById("ranking-state"); const stateNode = document.getElementById("ranking-state");
const titleNode = document.getElementById("ranking-title"); const titleNode = document.getElementById("ranking-title");
const metaNode = document.getElementById("ranking-meta"); const metaNode = document.getElementById("ranking-meta");
@@ -16,9 +13,8 @@
const metricHeadingNode = document.getElementById("ranking-metric-heading"); const metricHeadingNode = document.getElementById("ranking-metric-heading");
const kpmHeadingNode = document.getElementById("ranking-kpm-heading"); const kpmHeadingNode = document.getElementById("ranking-kpm-heading");
const emptyNode = document.getElementById("ranking-empty"); const emptyNode = document.getElementById("ranking-empty");
const filterNoteNode = document.getElementById("ranking-filter-note");
const currentYear = new Date().getUTCFullYear(); const annualYear = 2026;
const annualMetric = "kills"; const annualMetric = "kills";
const defaultMetric = "kills"; const defaultMetric = "kills";
const defaultLimit = "20"; const defaultLimit = "20";
@@ -43,20 +39,13 @@
let currentRequestId = 0; let currentRequestId = 0;
let activeController = null; let activeController = null;
if (yearInput) {
yearInput.value = String(currentYear);
}
applyInitialUrlState(); applyInitialUrlState();
toggleYearField();
syncMetricState(); syncMetricState();
setRankingState("neutral", "Preparando ranking p\u00fablico...");
clearRankingSurface(); clearRankingSurface();
void loadRanking(); void loadRanking();
if (timeframeSelect) { if (timeframeSelect) {
timeframeSelect.addEventListener("change", () => { timeframeSelect.addEventListener("change", () => {
toggleYearField();
syncMetricState(); syncMetricState();
updateUrlState(); updateUrlState();
void loadRanking(); void loadRanking();
@@ -74,20 +63,9 @@
}); });
}); });
if (yearInput) {
yearInput.addEventListener("change", () => {
updateUrlState();
if (String(timeframeSelect?.value || defaultTimeframe) === "annual") {
void loadRanking();
}
});
}
if (form) { if (form) {
form.addEventListener("submit", (event) => { form.addEventListener("submit", (event) => {
event.preventDefault(); event.preventDefault();
updateUrlState();
void loadRanking();
}); });
} }
@@ -95,26 +73,12 @@
if (!stateNode) { if (!stateNode) {
return; return;
} }
stateNode.textContent = message; const normalizedMessage = String(message || "").trim();
stateNode.hidden = !normalizedMessage;
stateNode.textContent = normalizedMessage;
stateNode.className = `stats-state stats-state--${state}`; stateNode.className = `stats-state stats-state--${state}`;
} }
function setFilterNote(message, tone = "neutral") {
if (!filterNoteNode) {
return;
}
filterNoteNode.textContent = message;
filterNoteNode.className = `ranking-form__note ranking-form__note--${tone}`;
}
function setFormBusy(isBusy) {
if (!submitButton) {
return;
}
submitButton.disabled = isBusy;
submitButton.setAttribute("aria-busy", String(isBusy));
}
function applyInitialUrlState() { function applyInitialUrlState() {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const timeframe = params.get("timeframe"); const timeframe = params.get("timeframe");
@@ -143,12 +107,6 @@
? String(limit) ? String(limit)
: defaultLimit; : defaultLimit;
} }
if (yearInput && year) {
const normalizedYear = Number.parseInt(year, 10);
if (Number.isFinite(normalizedYear) && normalizedYear > 0) {
yearInput.value = String(normalizedYear);
}
}
if (params.has("limit") && !supportedLimits.includes(limit || "")) { if (params.has("limit") && !supportedLimits.includes(limit || "")) {
setRankingState( setRankingState(
@@ -156,15 +114,14 @@
"El limite del URL no es valido para esta interfaz. Se restauro el valor permitido por defecto.", "El limite del URL no es valido para esta interfaz. Se restauro el valor permitido por defecto.",
); );
} }
} if (params.has("year")) {
const normalizedYear = Number.parseInt(year || "", 10);
function toggleYearField() { if (Number.isFinite(normalizedYear) && normalizedYear !== annualYear) {
const isAnnual = timeframeSelect?.value === "annual"; setRankingState(
if (yearWrap) { "warning",
yearWrap.hidden = !isAnnual; `El ranking anual de esta vista usa ${annualYear}. Se ajusto el a\u00f1o del URL.`,
} );
if (yearInput) { }
yearInput.disabled = !isAnnual;
} }
} }
@@ -181,19 +138,6 @@
if (isAnnual && metricSelect.value !== annualMetric) { if (isAnnual && metricSelect.value !== annualMetric) {
metricSelect.value = annualMetric; metricSelect.value = annualMetric;
} }
if (isAnnual) {
setFilterNote(
"El ranking anual sigue limitado a kills porque solo esa m\u00e9trica tiene lectura snapshot segura.",
"warning",
);
return;
}
setFilterNote(
"El ranking expone los resultados de los l\u00edderes. Para b\u00fasqueda individual usa Estad\u00edsticas.",
"neutral",
);
} }
function updateUrlState() { function updateUrlState() {
@@ -204,7 +148,7 @@
limit: String(limitSelect?.value || defaultLimit), limit: String(limitSelect?.value || defaultLimit),
}); });
if (searchParams.get("timeframe") === "annual") { if (searchParams.get("timeframe") === "annual") {
searchParams.set("year", String(yearInput?.value || currentYear)); searchParams.set("year", String(annualYear));
} }
window.history.replaceState({}, "", `?${searchParams.toString()}`); window.history.replaceState({}, "", `?${searchParams.toString()}`);
} }
@@ -244,16 +188,10 @@
let year = null; let year = null;
if (timeframe === "annual") { if (timeframe === "annual") {
year = Number.parseInt(String(yearInput?.value || "").trim(), 10); year = annualYear;
if (!Number.isFinite(year) || year <= 0) {
setRankingState("error", "El a\u00f1o solicitado no es v\u00e1lido.");
renderEmptyState("Corrige el a\u00f1o y vuelve a consultar el ranking anual.");
return;
}
} }
setRankingState("loading", "Cargando ranking global..."); setRankingState("loading", "Cargando ranking global...");
setFormBusy(true);
if (activeController) { if (activeController) {
activeController.abort(); activeController.abort();
@@ -306,7 +244,6 @@
); );
} finally { } finally {
if (requestId === currentRequestId) { if (requestId === currentRequestId) {
setFormBusy(false);
activeController = null; activeController = null;
} }
} }
@@ -326,24 +263,19 @@
normalizedMessage.includes("metric") && normalizedMessage.includes("metric") &&
normalizedMessage.includes("annual") normalizedMessage.includes("annual")
) { ) {
setRankingState("warning", "El ranking anual solo admite kills por ahora."); setRankingState("warning", "La metrica anual solicitada no esta disponible.");
renderEmptyState( renderEmptyState(
"Las m\u00e9tricas extra est\u00e1n disponibles en semanal y mensual. El ranking anual sigue limitado a kills mientras no existan snapshots seguros adicionales.", "La consulta anual devolvio una metrica sin snapshot seguro para esta vista.",
); );
return; return;
} }
if (statusCode === 400 && normalizedMessage.includes("metric")) { if (statusCode === 400 && normalizedMessage.includes("metric")) {
setRankingState("warning", "La m\u00e9trica solicitada no est\u00e1 soportada."); setRankingState("warning", "La m\u00e9trica solicitada no est\u00e1 soportada.");
renderEmptyState( renderEmptyState(
"Usa kills, deaths, teamkills, partidas jugadas, K/D o KPM.", "Usa kills, deaths, teamkills, partidas jugadas, K/D o Kills/partida.",
); );
return; return;
} }
if (statusCode === 400 && normalizedMessage.includes("year")) {
setRankingState("warning", "El ranking anual requiere un a\u00f1o v\u00e1lido.");
renderEmptyState("Define un a\u00f1o v\u00e1lido para consultar la lectura anual.");
return;
}
if (statusCode === 400 && normalizedMessage.includes("timeframe")) { if (statusCode === 400 && normalizedMessage.includes("timeframe")) {
setRankingState("warning", "El periodo solicitado no esta soportado."); setRankingState("warning", "El periodo solicitado no esta soportado.");
renderEmptyState("Usa un periodo semanal, mensual o anual."); renderEmptyState("Usa un periodo semanal, mensual o anual.");
@@ -403,10 +335,7 @@
return; return;
} }
setRankingState( setRankingState("ready", "");
"ready",
`${labelForMetric(metric)} listo en ${labelForTimeframe(timeframe)} para ${labelForServer(serverId)}.`,
);
if (tableBodyNode) { if (tableBodyNode) {
tableBodyNode.innerHTML = items.map((item) => renderRow(item, metric)).join(""); tableBodyNode.innerHTML = items.map((item) => renderRow(item, metric)).join("");
@@ -453,7 +382,11 @@
} }
function renderRow(item, metric) { function renderRow(item, metric) {
const kpm = formatKpm(item.kills_per_match, item.kills, item.matches_considered); const killsPerMatch = formatKillsPerMatch(
item.kills_per_match,
item.kills,
item.matches_considered,
);
const hideKpmColumn = metric === "kills_per_match"; const hideKpmColumn = metric === "kills_per_match";
return ` return `
@@ -471,7 +404,7 @@
<td>${safeInt(item.teamkills, 0)}</td> <td>${safeInt(item.teamkills, 0)}</td>
<td>${safeInt(item.matches_considered, 0)}</td> <td>${safeInt(item.matches_considered, 0)}</td>
<td>${safeDecimal(item.kd_ratio, 2, "0.00")}</td> <td>${safeDecimal(item.kd_ratio, 2, "0.00")}</td>
${hideKpmColumn ? "" : `<td>${kpm}</td>`} ${hideKpmColumn ? "" : `<td>${killsPerMatch}</td>`}
</tr> </tr>
`; `;
} }
@@ -495,7 +428,7 @@
function labelForWindow(data) { function labelForWindow(data) {
if (String(data.timeframe || "") === "annual") { if (String(data.timeframe || "") === "annual") {
return `A\u00f1o ${safeInt(data.year, currentYear)}`; return `A\u00f1o ${safeInt(data.year, annualYear)}`;
} }
return String(data.window_label || data.window_kind || "Periodo activo"); return String(data.window_label || data.window_kind || "Periodo activo");
} }
@@ -527,7 +460,7 @@
teamkills: "Teamkills", teamkills: "Teamkills",
matches_considered: "Partidas jugadas", matches_considered: "Partidas jugadas",
kd_ratio: "K/D", kd_ratio: "K/D",
kills_per_match: "KPM", kills_per_match: "Kills/partida",
}; };
return labels[metric] || "Kills"; return labels[metric] || "Kills";
} }
@@ -558,7 +491,7 @@
}); });
} }
function formatKpm(rawKillsPerMatch, rawKills, rawMatches) { function formatKillsPerMatch(rawKillsPerMatch, rawKills, rawMatches) {
const directValue = Number(rawKillsPerMatch); const directValue = Number(rawKillsPerMatch);
if (Number.isFinite(directValue)) { if (Number.isFinite(directValue)) {
return safeDecimal(directValue, 2, "0.00"); return safeDecimal(directValue, 2, "0.00");

View File

@@ -61,9 +61,11 @@
<p class="eyebrow eyebrow--section">Filtros de lectura</p> <p class="eyebrow eyebrow--section">Filtros de lectura</p>
<h2>Top p&uacute;blico por periodo</h2> <h2>Top p&uacute;blico por periodo</h2>
</div> </div>
<p class="panel__intro panel__intro--tight"> <div class="ranking-secondary-action">
El ranking expone los resultados de los l&iacute;deres. Para b&uacute;squeda individual usa Estad&iacute;sticas. <a class="secondary-button secondary-button--compact" href="./stats.html">
</p> Buscar jugador en Estad&iacute;sticas
</a>
</div>
<form class="ranking-form" id="ranking-form"> <form class="ranking-form" id="ranking-form">
<div class="ranking-form__grid"> <div class="ranking-form__grid">
<label class="stats-search-form__label" for="ranking-timeframe"> <label class="stats-search-form__label" for="ranking-timeframe">
@@ -90,7 +92,7 @@
<option value="teamkills">Teamkills</option> <option value="teamkills">Teamkills</option>
<option value="matches_considered">Partidas jugadas</option> <option value="matches_considered">Partidas jugadas</option>
<option value="kd_ratio">K/D</option> <option value="kd_ratio">K/D</option>
<option value="kills_per_match">KPM</option> <option value="kills_per_match">Kills/partida</option>
</select> </select>
</label> </label>
<label class="stats-search-form__label" for="ranking-limit"> <label class="stats-search-form__label" for="ranking-limit">
@@ -104,32 +106,10 @@
<option value="100">Top 100</option> <option value="100">Top 100</option>
</select> </select>
</label> </label>
<label class="stats-search-form__label" for="ranking-year" id="ranking-year-wrap" hidden>
A&ntilde;o
<input
class="stats-search-input"
id="ranking-year"
name="year"
type="number"
min="1"
step="1"
inputmode="numeric"
aria-describedby="ranking-state"
/>
</label>
</div>
<p class="ranking-form__note" id="ranking-filter-note">
Consulta l&iacute;deres p&uacute;blicos y cambia de periodo o servidor sin salir de la p&aacute;gina.
</p>
<div class="hero__actions ranking-form__actions">
<button class="discord-button" id="ranking-submit" type="submit">Actualizar ranking</button>
<a class="secondary-button secondary-button--compact" href="./stats.html">
Buscar jugador en Estad&iacute;sticas
</a>
</div> </div>
</form> </form>
<p class="stats-state stats-state--neutral" id="ranking-state" role="status" aria-live="polite"> <p class="stats-state stats-state--neutral" id="ranking-state" role="status" aria-live="polite">
Preparando ranking p&uacute;blico... Cargando ranking p&uacute;blico...
</p> </p>
<h3 class="ranking-results-title" id="ranking-title">Tabla activa</h3> <h3 class="ranking-results-title" id="ranking-title">Tabla activa</h3>
<div class="ranking-meta" id="ranking-meta"></div> <div class="ranking-meta" id="ranking-meta"></div>
@@ -145,7 +125,7 @@
<th>Teamkills</th> <th>Teamkills</th>
<th>Partidas</th> <th>Partidas</th>
<th>K/D</th> <th>K/D</th>
<th id="ranking-kpm-heading">KPM</th> <th id="ranking-kpm-heading">Kills/partida</th>
</tr> </tr>
</thead> </thead>
<tbody id="ranking-table-body"></tbody> <tbody id="ranking-table-body"></tbody>