diff --git a/ai/tasks/done/TASK-252-fix-historical-leaderboard-table-columns-and-title-labels.md b/ai/tasks/done/TASK-252-fix-historical-leaderboard-table-columns-and-title-labels.md new file mode 100644 index 0000000..262dad2 --- /dev/null +++ b/ai/tasks/done/TASK-252-fix-historical-leaderboard-table-columns-and-title-labels.md @@ -0,0 +1,159 @@ +# TASK-252 - Fix historical leaderboard table columns and title labels + +## Summary + +This task fixes two regressions introduced after TASK-250 in the public historical leaderboard: + +- the section title could render as `undefined ...` +- the primary metric column header could disappear because `hydrateWeeklyLeaderboard(...)` was called with the wrong argument order in some refresh paths + +It also tightens the leaderboard table behavior so each top shows only the columns that match its metric. + +## Files Read First + +- `frontend/historico.html` +- `frontend/assets/js/historico.js` + +## Root Cause + +### 1. `undefined` in title + +`buildLeaderboardTitle(...)` expects a valid metric config with a `.title`. + +After TASK-250, some calls to `hydrateWeeklyLeaderboard(...)` still used the old function signature and omitted the new `ratioHeadingNode` argument. That shifted the remaining arguments: + +- `weeklyWindowNoteNode` was received as `ratioHeadingNode` +- `weeklySnapshotMetaNode` was received as `noteNode` +- the metric config was received as `snapshotMetaNode` +- the timeframe string was received as `metricConfig` + +That left `metricConfig.title` undefined and produced titles like: + +- `undefined Semanal - Todos los servidores` + +### 2. Primary metric header missing + +The same bad argument order also broke the header sync path: + +- the value column header was still updated +- but the ratio-heading logic was running against the wrong node +- depending on the metric, the table could lose the intended visible structure and appear with the main metric column effectively unlabeled in production + +## Changes Applied + +- Updated every `hydrateWeeklyLeaderboard(...)` call to pass `weeklyRatioHeadingNode` in the correct position. +- Hardened `buildLeaderboardTitle(...)` so it always falls back to a safe metric config and never emits `undefined`. +- Adjusted metric labels to match the required public wording: + - `kills` -> title `Top kills`, value heading `Kills` + - `deaths` -> title `Top muertes`, value heading `Muertes` + - `matches_over_100_kills` -> title `Partidas 100+ kills`, value heading `Partidas 100+ kills` + - `support` -> title `Soporte`, value heading `Soporte` +- Kept ratio-column behavior metric-aware: + - kills -> `Kills/partida` + - deaths -> `Muertes/partida` + - matches_over_100_kills -> no ratio column + - support -> `Soporte/partida` only when rows exist; otherwise the ratio column stays hidden + +## Final Headers By Top + +### Top kills + +- `Pos.` +- `Jugador` +- `Kills` +- `Partidas` +- `Kills/partida` + +### Top muertes + +- `Pos.` +- `Jugador` +- `Muertes` +- `Partidas` +- `Muertes/partida` + +### Partidas 100+ kills + +- `Pos.` +- `Jugador` +- `Partidas 100+ kills` +- `Partidas` + +### Soporte + +If rows exist: + +- `Pos.` +- `Jugador` +- `Soporte` +- `Partidas` +- `Soporte/partida` + +If no rows exist: + +- `Pos.` +- `Jugador` +- `Soporte` +- `Partidas` + +## Support Status + +Current support payload behavior was reviewed only to the extent needed to diagnose the UI: + +- the RCON historical leaderboard code documents support as a special case +- when support data is not available from the materialized RCON read model, it returns an empty supported payload instead of falling back to unrelated public-scoreboard totals + +Frontend behavior now matches that contract: + +- no `undefined` +- no stale `Kills/partida` +- no ratio column for support when there are no rows + +## Validation + +Executed: + +- `node --check frontend/assets/js/historico.js` + +Static checks: + +- every `hydrateWeeklyLeaderboard(...)` call now passes `weeklyRatioHeadingNode` +- `buildLeaderboardTitle(...)` has a safe fallback path and no longer depends on an unchecked `metricConfig.title` +- header/value mappings were checked for: + - `kills` + - `deaths` + - `matches_over_100_kills` + - `support` + +Expected output matrix: + +- kills -> `Pos., Jugador, Kills, Partidas, Kills/partida` +- deaths -> `Pos., Jugador, Muertes, Partidas, Muertes/partida` +- matches_over_100_kills -> `Pos., Jugador, Partidas 100+ kills, Partidas` +- support -> `Pos., Jugador, Soporte, Partidas`, plus `Soporte/partida` only when rows exist + +## Post-Deploy Visual Check + +1. Open `historico.html` +2. Select `Semanal + Top kills` + - title must be `Top kills Semanal - ...` + - `Kills` column visible + - `Kills/partida` visible +3. Select `Mensual + Top muertes` + - title must be `Top muertes Mensual - ...` + - `Muertes` column visible + - `Muertes/partida` visible +4. Select `Partidas 100+ kills` + - title must be `Partidas 100+ kills ...` + - `Partidas 100+ kills` column visible + - no ratio column +5. Select `Soporte` + - title must be `Soporte ...` + - no `undefined` + - no `Kills/partida` + - if no rows, empty state remains intact + +## Notes + +- No backend changes were required. +- No assets, maps, brands, clan images or weapon images were modified. diff --git a/frontend/assets/js/historico.js b/frontend/assets/js/historico.js index 6a55f07..fc1e558 100644 --- a/frontend/assets/js/historico.js +++ b/frontend/assets/js/historico.js @@ -59,15 +59,15 @@ const LEADERBOARD_METRICS = Object.freeze([ }, { key: "matches_over_100_kills", - title: "Top partidas con 100+ kills", - valueHeading: "Partidas 100+", + title: "Partidas 100+ kills", + valueHeading: "Partidas 100+ kills", ratioHeading: null, ratioMode: null, emptyMessage: "Ningun jugador ha registrado partidas de 100+ kills en esta ventana.", }, { key: "support", - title: "Top puntos de soporte", + title: "Soporte", valueHeading: "Soporte", ratioHeading: "Soporte/partida", ratioMode: "support", @@ -312,6 +312,7 @@ document.addEventListener("DOMContentLoaded", () => { weeklyBodyNode, weeklyTitleNode, weeklyValueHeadingNode, + weeklyRatioHeadingNode, weeklyWindowNoteNode, weeklySnapshotMetaNode, activeMetricConfig, @@ -360,6 +361,7 @@ document.addEventListener("DOMContentLoaded", () => { weeklyBodyNode, weeklyTitleNode, weeklyValueHeadingNode, + weeklyRatioHeadingNode, weeklyWindowNoteNode, weeklySnapshotMetaNode, metricConfig, @@ -399,6 +401,7 @@ document.addEventListener("DOMContentLoaded", () => { weeklyBodyNode, weeklyTitleNode, weeklyValueHeadingNode, + weeklyRatioHeadingNode, weeklyWindowNoteNode, weeklySnapshotMetaNode, metricConfig, @@ -1509,8 +1512,12 @@ function buildWeeklyWindowNote(payload) { } function buildLeaderboardTitle(metricConfig, serverSlug, timeframeKey) { + const safeMetricConfig = metricConfig?.key + ? metricConfig + : getLeaderboardMetricConfig(metricConfig?.key); const timeframeLabel = getLeaderboardTimeframeConfig(timeframeKey).label; - return `${metricConfig.title} ${timeframeLabel} - ${getHistoricalServerLabel(serverSlug)}`; + const titleLabel = safeMetricConfig?.title || LEADERBOARD_METRICS[0].title; + return `${titleLabel} ${timeframeLabel} - ${getHistoricalServerLabel(serverSlug)}`; } function buildRecentMatchesNote(serverSlug) { @@ -1739,7 +1746,7 @@ function syncLeaderboardRatioColumn(tableNode, ratioHeadingNode, bodyNode, metri if (!tableNode || !ratioHeadingNode || !bodyNode) { return; } - const showRatio = Boolean(metricConfig?.ratioMode); + const showRatio = shouldShowLeaderboardRatioColumn(metricConfig, bodyNode); ratioHeadingNode.hidden = !showRatio; ratioHeadingNode.textContent = metricConfig?.ratioHeading || ""; const ratioColumnIndex = ratioHeadingNode.cellIndex; @@ -1751,6 +1758,16 @@ function syncLeaderboardRatioColumn(tableNode, ratioHeadingNode, bodyNode, metri }); } +function shouldShowLeaderboardRatioColumn(metricConfig, bodyNode) { + if (!metricConfig?.ratioMode) { + return false; + } + if (metricConfig.ratioMode === "support") { + return bodyNode.children.length > 0; + } + return true; +} + function formatHistoricalRatio(item, metricConfig, matches) { if (!metricConfig?.ratioMode) { return "";