Add annual ranking snapshots for supported metrics

This commit is contained in:
devRaGonSa
2026-06-10 12:12:51 +02:00
parent 6a27f4c2b5
commit d5dee75166
7 changed files with 492 additions and 93 deletions

View File

@@ -0,0 +1,198 @@
---
id: TASK-217
title: Add annual ranking snapshots for supported metrics
status: done
type: backend
team: Backend Senior
supporting_teams:
- Frontend Senior
- Arquitecto de Base de Datos
- Arquitecto Python
roadmap_item: foundation
priority: high
---
# TASK-217 - Add annual ranking snapshots for supported metrics
## Goal
Permitir ranking anual para metricas adicionales usando un snapshot anual independiente por metrica, sin runtime publico pesado ni reutilizacion del snapshot top kills para otras metricas.
## Context
`TASK-211` dejo la lectura publica anual rapida y basada en `rcon_annual_ranking_snapshots` y `rcon_annual_ranking_snapshot_items`. `TASK-216` mantuvo annual limitado a `kills` porque no habia snapshot propio para otras metricas y corrigio el falso KPM: `kills_per_match` debe mostrarse como `Kills/partida`, no como kills por minuto.
Esta task amplia la generacion annual para metricas que ya se pueden calcular de forma segura desde los read models materializados durante un proceso interno/CLI. La lectura publica sigue leyendo solo snapshots anuales ya generados.
Preserve the current product identity: Spanish-speaking HLL Vietnam community, military/Vietnam/tactical/sober visual direction and controlled repository evolution.
## Steps
1. Revisar la lectura y generacion anual actual.
2. Confirmar que el esquema permite `metric_value` decimal para ratios.
3. Actualizar la normalizacion de metricas annual soportadas.
4. Generar snapshots independientes por metrica:
- `kills`
- `deaths`
- `teamkills`
- `matches_considered`
- `kd_ratio`
- `kills_per_match`
5. Mantener la lectura publica anual sobre tablas de snapshot, sin fallback runtime.
6. Actualizar frontend para permitir metricas annual soportadas y mostrar `kills_per_match` como `Kills/partida`.
7. Añadir tests de normalizacion, lectura publica y calculo/orden de `kd_ratio` y `kills_per_match`.
## Files to Read First
- `AGENTS.md`
- `ai/repo-context.md`
- `ai/architecture-index.md`
- `backend/app/rcon_annual_rankings.py`
- `backend/app/payloads.py`
- `frontend/assets/js/ranking.js`
## Expected Files to Modify
- `backend/app/rcon_annual_rankings.py`
- `backend/app/rcon_admin_log_materialization.py`
- `backend/app/postgres_rcon_storage.py`
- `backend/tests/test_annual_ranking_payload.py`
- `frontend/ranking.html`
- `frontend/assets/js/ranking.js`
- `ai/tasks/done/TASK-217-add-annual-ranking-snapshots-for-supported-metrics.md`
## Constraints
- Keep the change minimal.
- Preserve HLL Vietnam project identity.
- Do not introduce unnecessary frameworks or dependencies.
- No ejecutar `ai-platform run`.
- No hacer push.
- 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 implementar KPM real en esta task.
- No mostrar `kills_per_match` como KPM.
- No exponer metricas annual sin snapshot anual propio.
- No usar el snapshot top kills para representar otras metricas.
- La lectura publica annual debe leer solo `rcon_annual_ranking_snapshots` y `rcon_annual_ranking_snapshot_items`.
## Validation
Before completing the task ensure:
- `node --check frontend/assets/js/ranking.js`
- `python -m compileall backend\app\rcon_annual_rankings.py backend\app\payloads.py backend\tests\test_annual_ranking_payload.py`
- `python -m unittest backend.tests.test_annual_ranking_payload`
- Medicion directa de `build_global_ranking_payload` annual para metricas soportadas si el entorno local tiene datos.
- `git diff --name-only` matches the expected scope.
- No unrelated files were modified.
## Outcome
Archivos modificados por esta task:
- `backend/app/rcon_annual_rankings.py`
- `backend/app/rcon_admin_log_materialization.py`
- `backend/app/postgres_rcon_storage.py`
- `backend/tests/test_annual_ranking_payload.py`
- `frontend/ranking.html`
- `frontend/assets/js/ranking.js`
- `ai/tasks/done/TASK-217-add-annual-ranking-snapshots-for-supported-metrics.md`
Metricas anuales soportadas:
- `kills`
- `deaths`
- `teamkills`
- `matches_considered`
- `kd_ratio`
- `kills_per_match`
Cambios de backend:
- `_normalize_metric()` acepta solo las seis metricas anuales soportadas.
- La generacion anual calcula y ordena snapshots independientes por metrica real.
- `kd_ratio` se calcula como `kills / deaths` sobre agregados.
- `kills_per_match` se calcula como `kills / matches_considered` y sigue siendo `Kills/partida`, no KPM.
- La lectura publica anual no cambia de modelo: sigue usando `rcon_annual_ranking_snapshots` y `rcon_annual_ranking_snapshot_items`.
- No se introdujo fallback runtime ni lectura publica sobre `rcon_match_player_stats`.
- Se agregaron tests de normalizacion, rechazo de metrica no soportada, lectura publica sin inicializar storage y orden/calculo para `kd_ratio` y `kills_per_match`.
Cambios de esquema:
- Si hubo cambios de esquema.
- `rcon_annual_ranking_snapshots.metric` ahora permite:
- `kills`
- `deaths`
- `teamkills`
- `matches_considered`
- `kd_ratio`
- `kills_per_match`
- `rcon_annual_ranking_snapshot_items.metric_value` pasa de entero a valor decimal:
- SQLite: `REAL`
- PostgreSQL: `DOUBLE PRECISION`
- La inicializacion PostgreSQL incluye una migracion idempotente para ajustar `metric_value` y reemplazar el `CHECK` antiguo.
Cambios de frontend:
- Annual permite seleccionar las metricas soportadas.
- No se muestra KPM.
- `kills_per_match` se muestra como `Kills/partida`.
- Se renombraron identificadores internos heredados de KPM a KPP.
- Annual mantiene `2026` interno y no muestra input de año.
- Si falta un snapshot anual, la UI muestra `Snapshot anual no disponible para esta metrica.` sin fallback ni bloqueo.
Comandos de produccion para generar snapshots anuales 2026:
```bash
for server in all-servers comunidad-hispana-01 comunidad-hispana-02; do
for metric in kills deaths teamkills matches_considered kd_ratio kills_per_match; do
docker compose exec backend python -m app.rcon_annual_rankings generate \
--year 2026 \
--server-key "$server" \
--metric "$metric" \
--limit 30 \
--replace-existing
done
done
```
Validaciones ejecutadas:
- `node --check frontend/assets/js/ranking.js`
- `$env:PYTHONPATH='backend'; python -m compileall backend\app\rcon_annual_rankings.py backend\app\payloads.py backend\tests\test_annual_ranking_payload.py backend\app\postgres_rcon_storage.py backend\app\rcon_admin_log_materialization.py`
- `$env:PYTHONPATH='backend'; python -m unittest backend.tests.test_annual_ranking_payload`
- Busqueda en `frontend/ranking.html` y `frontend/assets/js/ranking.js` para confirmar que no aparecen `KPM`, `Actualizar ranking`, `ranking-year`, `El ranking expone`, `Kills listo`, `listo en` ni `annualMetric`.
- Medicion directa con `build_global_ranking_payload(timeframe="annual", year=2026)` para las seis metricas soportadas.
Tiempos obtenidos en lectura directa local:
- `kills`: `1.66 ms`, `snapshot_status=ready`, `read_model=rcon-annual-ranking-snapshot`, `fallback_used=False`, `items=20`.
- `deaths`: `1.17 ms`, `snapshot_status=missing`, `read_model=rcon-annual-ranking-snapshot`, `fallback_used=False`, `items=0`.
- `teamkills`: `1.10 ms`, `snapshot_status=missing`, `read_model=rcon-annual-ranking-snapshot`, `fallback_used=False`, `items=0`.
- `matches_considered`: `1.09 ms`, `snapshot_status=missing`, `read_model=rcon-annual-ranking-snapshot`, `fallback_used=False`, `items=0`.
- `kd_ratio`: `1.11 ms`, `snapshot_status=missing`, `read_model=rcon-annual-ranking-snapshot`, `fallback_used=False`, `items=0`.
- `kills_per_match`: `1.10 ms`, `snapshot_status=missing`, `read_model=rcon-annual-ranking-snapshot`, `fallback_used=False`, `items=0`.
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 modificados por la task: 7.
- El cambio supera el objetivo preferente de 5 archivos porque incluye schema SQLite/PostgreSQL, generador, test, frontend y documentacion de task.
- No se implemento KPM real; queda fuera de alcance hasta materializar tiempo jugado real por jugador.

View File

@@ -201,7 +201,7 @@ CREATE TABLE IF NOT EXISTS rcon_annual_ranking_snapshots (
status TEXT NOT NULL DEFAULT 'ready',
source_matches_count INTEGER NOT NULL DEFAULT 0,
CHECK (limit_size > 0),
CHECK (metric IN ('kills', 'deaths', 'matches_over_100_kills', 'support')),
CHECK (metric IN ('kills', 'deaths', 'teamkills', 'matches_considered', 'kd_ratio', 'kills_per_match')),
UNIQUE (year, server_key, metric)
);
@@ -211,7 +211,7 @@ CREATE TABLE IF NOT EXISTS rcon_annual_ranking_snapshot_items (
ranking_position INTEGER NOT NULL,
player_id TEXT NOT NULL,
player_name TEXT NOT NULL,
metric_value BIGINT NOT NULL DEFAULT 0,
metric_value DOUBLE PRECISION NOT NULL DEFAULT 0,
matches_considered INTEGER NOT NULL DEFAULT 0,
kills BIGINT NOT NULL DEFAULT 0,
deaths BIGINT NOT NULL DEFAULT 0,
@@ -379,12 +379,49 @@ CREATE INDEX IF NOT EXISTS idx_rcon_scoreboard_candidates_server_end
ON rcon_scoreboard_match_candidates(server_slug, ended_at DESC, started_at DESC);
"""
POSTGRES_ANNUAL_RANKING_SCHEMA_MIGRATION_SQL = """
ALTER TABLE rcon_annual_ranking_snapshot_items
ALTER COLUMN metric_value TYPE DOUBLE PRECISION USING metric_value::double precision;
DO $$
DECLARE
constraint_record record;
BEGIN
FOR constraint_record IN
SELECT con.conname
FROM pg_constraint AS con
JOIN pg_class AS rel ON rel.oid = con.conrelid
JOIN pg_namespace AS nsp ON nsp.oid = rel.relnamespace
WHERE rel.relname = 'rcon_annual_ranking_snapshots'
AND con.contype = 'c'
AND pg_get_constraintdef(con.oid) LIKE '%metric%'
LOOP
EXECUTE format(
'ALTER TABLE rcon_annual_ranking_snapshots DROP CONSTRAINT %I',
constraint_record.conname
);
END LOOP;
ALTER TABLE rcon_annual_ranking_snapshots
ADD CONSTRAINT rcon_annual_ranking_snapshots_metric_check
CHECK (metric IN (
'kills',
'deaths',
'teamkills',
'matches_considered',
'kd_ratio',
'kills_per_match'
));
END $$;
"""
def initialize_postgres_rcon_storage() -> None:
"""Create deterministic PostgreSQL schema for migrated RCON domains."""
with connect_postgres() as connection:
with connection.cursor() as cursor:
cursor.execute(RCON_SCHEMA_SQL)
cursor.execute(POSTGRES_ANNUAL_RANKING_SCHEMA_MIGRATION_SQL)
@contextmanager

View File

@@ -120,7 +120,7 @@ def initialize_rcon_materialized_storage(*, db_path: Path | None = None) -> Path
status TEXT NOT NULL DEFAULT 'ready',
source_matches_count INTEGER NOT NULL DEFAULT 0,
CHECK (limit_size > 0),
CHECK (metric IN ('kills', 'deaths', 'matches_over_100_kills', 'support')),
CHECK (metric IN ('kills', 'deaths', 'teamkills', 'matches_considered', 'kd_ratio', 'kills_per_match')),
UNIQUE (year, server_key, metric)
);
@@ -136,7 +136,7 @@ def initialize_rcon_materialized_storage(*, db_path: Path | None = None) -> Path
ranking_position INTEGER NOT NULL,
player_id TEXT NOT NULL,
player_name TEXT NOT NULL,
metric_value INTEGER NOT NULL DEFAULT 0,
metric_value REAL NOT NULL DEFAULT 0,
matches_considered INTEGER NOT NULL DEFAULT 0,
kills INTEGER NOT NULL DEFAULT 0,
deaths INTEGER NOT NULL DEFAULT 0,

View File

@@ -7,6 +7,7 @@ import json
import sqlite3
from contextlib import closing
from contextlib import contextmanager
from contextlib import nullcontext
from datetime import date, datetime, timezone
from pathlib import Path
@@ -16,6 +17,16 @@ from .rcon_admin_log_materialization import MATCH_RESULT_SOURCE, initialize_rcon
from .sqlite_utils import connect_sqlite_readonly, connect_sqlite_writer
SUPPORTED_ANNUAL_RANKING_METRICS = (
"kills",
"deaths",
"teamkills",
"matches_considered",
"kd_ratio",
"kills_per_match",
)
def generate_annual_ranking_snapshot(
*,
year: int,
@@ -34,14 +45,17 @@ def generate_annual_ranking_snapshot(
resolved_path = initialize_rcon_materialized_storage(db_path=db_path)
scope_sql, scope_params = _build_scope_sql(normalized_server_key)
if use_postgres_rcon_storage(explicit_sqlite_path=db_path):
postgres_enabled = use_postgres_rcon_storage(explicit_sqlite_path=db_path)
if postgres_enabled:
from .postgres_rcon_storage import connect_postgres_compat
connection_scope = connect_postgres_compat()
else:
connection_scope = connect_sqlite_writer(resolved_path)
connection_scope = closing(connect_sqlite_writer(resolved_path))
with connection_scope as connection:
transaction_scope = nullcontext() if postgres_enabled else connection
with transaction_scope:
source_matches_count = _count_matches_in_window(
connection=connection,
start=window_start,
@@ -241,7 +255,7 @@ def _normalize_server_key(server_key: str | None) -> str:
def _normalize_metric(metric: str) -> str:
normalized = str(metric or "kills").strip().lower()
if normalized != "kills":
if normalized not in SUPPORTED_ANNUAL_RANKING_METRICS:
raise ValueError(
f"Metric '{normalized}' is not supported for annual ranking snapshots."
)
@@ -287,20 +301,18 @@ def _fetch_annual_ranking_rows(
scope_sql: str,
scope_params: list[object],
) -> list[dict[str, object]]:
# For now metric support is intentionally narrowed to kills only.
if metric != "kills":
return []
metric_sql, having_sql = _resolve_metric_sql(metric)
rows = connection.execute(
f"""
SELECT
stats.player_id,
COALESCE(MAX(stats.player_name), stats.player_id) AS player_name,
{metric_sql} AS metric_value,
SUM(COALESCE(stats.kills, 0)) AS kills,
SUM(COALESCE(stats.deaths, 0)) AS deaths,
SUM(COALESCE(stats.teamkills, 0)) AS teamkills,
COUNT(DISTINCT stats.match_key) AS matches_considered,
SUM(COALESCE(stats.kills, 0)) AS metric_value
COUNT(DISTINCT stats.match_key) AS matches_considered
FROM rcon_match_player_stats AS stats
INNER JOIN rcon_materialized_matches AS matches
ON matches.target_key = stats.target_key
@@ -311,8 +323,8 @@ def _fetch_annual_ranking_rows(
{scope_sql}
AND TRIM(COALESCE(stats.player_name, '')) != ''
GROUP BY stats.player_id
HAVING SUM(COALESCE(stats.kills, 0)) > 0
ORDER BY metric_value DESC, matches_considered DESC, player_name ASC
{having_sql}
ORDER BY metric_value DESC, matches_considered DESC, kills DESC, player_name ASC
LIMIT ?
""",
[MATCH_RESULT_SOURCE, start, end, *scope_params, limit],
@@ -320,6 +332,43 @@ def _fetch_annual_ranking_rows(
return [dict(row) for row in rows]
def _resolve_metric_sql(metric: str) -> tuple[str, str]:
metric_sql_by_metric = {
"kills": "SUM(COALESCE(stats.kills, 0))",
"deaths": "SUM(COALESCE(stats.deaths, 0))",
"teamkills": "SUM(COALESCE(stats.teamkills, 0))",
"matches_considered": "COUNT(DISTINCT stats.match_key)",
"kd_ratio": (
"CASE "
"WHEN SUM(COALESCE(stats.deaths, 0)) > 0 "
"THEN ROUND(CAST(SUM(COALESCE(stats.kills, 0)) AS NUMERIC) / "
"CAST(SUM(COALESCE(stats.deaths, 0)) AS NUMERIC), 2) "
"ELSE CAST(SUM(COALESCE(stats.kills, 0)) AS NUMERIC) "
"END"
),
"kills_per_match": (
"CASE "
"WHEN COUNT(DISTINCT stats.match_key) > 0 "
"THEN ROUND(CAST(SUM(COALESCE(stats.kills, 0)) AS NUMERIC) / "
"CAST(COUNT(DISTINCT stats.match_key) AS NUMERIC), 2) "
"ELSE CAST(0 AS NUMERIC) "
"END"
),
}
having_sql_by_metric = {
"kills": "HAVING SUM(COALESCE(stats.kills, 0)) > 0",
"deaths": "HAVING SUM(COALESCE(stats.deaths, 0)) > 0",
"teamkills": "HAVING SUM(COALESCE(stats.teamkills, 0)) > 0",
"matches_considered": "HAVING COUNT(DISTINCT stats.match_key) > 0",
"kd_ratio": "HAVING SUM(COALESCE(stats.kills, 0)) > 0",
"kills_per_match": (
"HAVING COUNT(DISTINCT stats.match_key) > 0 "
"AND SUM(COALESCE(stats.kills, 0)) > 0"
),
}
return metric_sql_by_metric[metric], having_sql_by_metric[metric]
def _count_matches_in_window(
*,
connection: object,
@@ -457,7 +506,10 @@ def _insert_snapshot(
source_matches_count,
],
)
try:
row = cursor.fetchone()
finally:
cursor.close()
if row is not None and row["id"] is not None:
return int(row["id"])
@@ -482,7 +534,7 @@ def _insert_items(
for index, row in enumerate(rows[:limit], start=1):
kills = int(row.get("kills") or 0)
deaths = int(row.get("deaths") or 0)
metric_value = int(row.get("metric_value") or 0)
metric_value = _coerce_metric_value(row.get("metric_value"))
connection.execute(
"""
INSERT INTO rcon_annual_ranking_snapshot_items (
@@ -563,6 +615,16 @@ def _list_items(*, connection: object, snapshot_id: int, limit: int | None = Non
return [dict(row) for row in rows]
def _coerce_metric_value(value: object) -> int | float:
try:
numeric = float(value or 0)
except (TypeError, ValueError):
return 0
if numeric.is_integer():
return int(numeric)
return round(numeric, 2)
def _count_items(*, connection: object, snapshot_id: int) -> int:
row = connection.execute(
"""

View File

@@ -1,8 +1,23 @@
import gc
import unittest
from contextlib import nullcontext
import warnings
from contextlib import closing, nullcontext
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import patch
from app.rcon_annual_rankings import get_annual_ranking_snapshot
from app.historical_storage import ALL_SERVERS_SLUG
from app.rcon_admin_log_materialization import (
MATCH_RESULT_SOURCE,
initialize_rcon_materialized_storage,
)
from app.rcon_annual_rankings import (
SUPPORTED_ANNUAL_RANKING_METRICS,
_normalize_metric,
generate_annual_ranking_snapshot,
get_annual_ranking_snapshot,
)
from app.sqlite_utils import connect_sqlite_writer
class AnnualRankingPayloadTests(unittest.TestCase):
@@ -28,6 +43,94 @@ class AnnualRankingPayloadTests(unittest.TestCase):
self.assertEqual(result["source"], "rcon-annual-ranking-snapshot")
self.assertEqual(result["requested_limit"], 30)
def test_normalize_metric_accepts_supported_annual_metrics(self):
for metric in SUPPORTED_ANNUAL_RANKING_METRICS:
with self.subTest(metric=metric):
self.assertEqual(_normalize_metric(metric), metric)
def test_normalize_metric_rejects_unsupported_annual_metrics(self):
with self.assertRaises(ValueError):
_normalize_metric("kills_per_minute")
def test_generate_annual_snapshot_orders_kd_ratio_and_kills_per_match(self):
with TemporaryDirectory() as temp_dir:
db_path = Path(temp_dir) / "annual-ranking.sqlite3"
self._seed_materialized_stats(db_path)
kd_result = generate_annual_ranking_snapshot(
year=2026,
server_key=ALL_SERVERS_SLUG,
metric="kd_ratio",
limit=10,
db_path=db_path,
)
kpp_result = generate_annual_ranking_snapshot(
year=2026,
server_key=ALL_SERVERS_SLUG,
metric="kills_per_match",
limit=10,
db_path=db_path,
)
kd_items = kd_result["items"]
kpp_items = kpp_result["items"]
self.assertEqual(kd_items[0]["player_id"], "player-bravo")
self.assertEqual(kd_items[0]["metric_value"], 10)
self.assertEqual(kpp_items[0]["player_id"], "player-bravo")
self.assertEqual(kpp_items[0]["metric_value"], 20)
self.assertEqual(kpp_items[1]["player_id"], "player-alpha")
self.assertEqual(kpp_items[1]["metric_value"], 15)
with warnings.catch_warnings():
warnings.simplefilter("ignore", ResourceWarning)
gc.collect()
def _seed_materialized_stats(self, db_path: Path) -> None:
initialize_rcon_materialized_storage(db_path=db_path)
with closing(connect_sqlite_writer(db_path)) as connection:
with connection:
for match_key in ("match-1", "match-2", "match-3"):
connection.execute(
"""
INSERT INTO rcon_materialized_matches (
target_key,
external_server_id,
match_key,
started_at,
ended_at,
confidence_mode,
source_basis
) VALUES (?, ?, ?, ?, ?, ?, ?)
""",
[
"target-1",
"comunidad-hispana-01",
match_key,
"2026-01-01T19:00:00Z",
"2026-01-01T20:00:00Z",
"exact",
MATCH_RESULT_SOURCE,
],
)
for row in (
("match-1", "player-alpha", "Alpha", 12, 4, 0),
("match-2", "player-alpha", "Alpha", 18, 6, 1),
("match-3", "player-bravo", "Bravo", 20, 2, 0),
):
connection.execute(
"""
INSERT INTO rcon_match_player_stats (
target_key,
match_key,
player_id,
player_name,
kills,
deaths,
teamkills
) VALUES (?, ?, ?, ?, ?, ?, ?)
""",
["target-1", *row],
)
if __name__ == "__main__":
unittest.main()

View File

@@ -11,11 +11,10 @@
const tableNode = document.getElementById("ranking-table");
const tableBodyNode = document.getElementById("ranking-table-body");
const metricHeadingNode = document.getElementById("ranking-metric-heading");
const kpmHeadingNode = document.getElementById("ranking-kpm-heading");
const kppHeadingNode = document.getElementById("ranking-kpp-heading");
const emptyNode = document.getElementById("ranking-empty");
const annualYear = 2026;
const annualMetric = "kills";
const defaultMetric = "kills";
const defaultLimit = "20";
const defaultTimeframe = "weekly";
@@ -132,11 +131,11 @@
}
Array.from(metricSelect.options).forEach((option) => {
option.disabled = isAnnual && option.value !== annualMetric;
option.disabled = isAnnual && !supportedMetrics.includes(option.value);
});
if (isAnnual && metricSelect.value !== annualMetric) {
metricSelect.value = annualMetric;
if (isAnnual && !supportedMetrics.includes(metricSelect.value)) {
metricSelect.value = defaultMetric;
}
}
@@ -313,9 +312,9 @@
}
if (timeframe === "annual" && snapshotStatus === "missing") {
setRankingState("warning", "El snapshot anual solicitado aun no fue generado.");
setRankingState("warning", "Snapshot anual no disponible para esta metrica.");
renderEmptyState(
"No existe snapshot anual para el a\u00f1o y servidor elegidos. Este estado es informativo y no implica ca\u00edda del backend.",
"Genera el snapshot anual para esta combinacion de metrica, servidor y a\u00f1o antes de publicarlo.",
);
return;
}
@@ -340,7 +339,7 @@
if (tableBodyNode) {
tableBodyNode.innerHTML = items.map((item) => renderRow(item, metric)).join("");
}
syncKpmColumn(metric);
syncKppColumn(metric);
if (tableNode) {
tableNode.hidden = false;
}
@@ -387,7 +386,7 @@
item.kills,
item.matches_considered,
);
const hideKpmColumn = metric === "kills_per_match";
const hideKppColumn = metric === "kills_per_match";
return `
<tr>
@@ -404,24 +403,24 @@
<td>${safeInt(item.teamkills, 0)}</td>
<td>${safeInt(item.matches_considered, 0)}</td>
<td>${safeDecimal(item.kd_ratio, 2, "0.00")}</td>
${hideKpmColumn ? "" : `<td>${killsPerMatch}</td>`}
${hideKppColumn ? "" : `<td>${killsPerMatch}</td>`}
</tr>
`;
}
function syncKpmColumn(metric) {
if (!tableNode || !kpmHeadingNode) {
function syncKppColumn(metric) {
if (!tableNode || !kppHeadingNode) {
return;
}
const kpmColumnIndex = kpmHeadingNode.cellIndex + 1;
const hideKpmColumn = metric === "kills_per_match";
kpmHeadingNode.hidden = hideKpmColumn;
const kppColumnIndex = kppHeadingNode.cellIndex + 1;
const hideKppColumn = metric === "kills_per_match";
kppHeadingNode.hidden = hideKppColumn;
tableNode.querySelectorAll("tbody tr").forEach((row) => {
const cell = row.children[kpmColumnIndex - 1];
const cell = row.children[kppColumnIndex - 1];
if (cell) {
cell.hidden = hideKpmColumn;
cell.hidden = hideKppColumn;
}
});
}

View File

@@ -125,7 +125,7 @@
<th>Teamkills</th>
<th>Partidas</th>
<th>K/D</th>
<th id="ranking-kpm-heading">Kills/partida</th>
<th id="ranking-kpp-heading">Kills/partida</th>
</tr>
</thead>
<tbody id="ranking-table-body"></tbody>