Fix historical leaderboard labels, stats ranking table uniformity and publish Steam icon

This commit is contained in:
devRaGonSa
2026-06-11 13:26:13 +02:00
parent d50d800554
commit ab696ab25d
7 changed files with 209 additions and 43 deletions

View File

@@ -0,0 +1,113 @@
# TASK-250 - Fix historical ranking labels, table uniformity and Steam icon
## Summary
This task fixes public labels that were still misleading in historical leaderboards, aligns the visible column order and wording between Stats and Ranking, and audits the Steam brand icon path used by external profiles.
## Files Read First
- `frontend/historico.html`
- `frontend/assets/js/historico.js`
- `frontend/ranking.html`
- `frontend/assets/js/ranking.js`
- `frontend/assets/js/stats.js`
- `frontend/assets/js/historico-partida.js`
## Historical Leaderboards
The weekly/monthly leaderboard table now adapts its ratio column to the active metric:
- `kills` -> `Kills/partida`
- `deaths` -> `Muertes/partida`
- `support` -> `Soporte/partida`
- `matches_over_100_kills` -> ratio column hidden
This removes the incorrect `Kills/partida` label from:
- Top muertes
- Top soporte
- Partidas 100+ kills
No aggregated real KPM was added in this task.
## Stats / Ranking Uniformity
Visible labels were normalized to Spanish or accepted abbreviations:
- `Deaths` -> `Muertes`
- `Teamkills` -> `TK`
- `matches_considered` -> `Partidas`
- `kd_ratio` -> `KD`
- `kills_per_match` -> `Kills/partida`
Shared player ranking column order now stays consistent:
- `Pos.`
- `Jugador`
- `Partidas`
- `Kills`
- `Muertes`
- `TK`
- `KD`
- `Kills/partida`
`ranking.html` keeps its extra `Valor activo` column before the shared block because that table is metric-driven.
## Steam Icon Audit
The code path in `historico-partida.js` is already correct:
- `./assets/img/brands/steam.png`
The local file also exists:
- `frontend/assets/img/brands/steam.png`
It is currently untracked in git:
- `git status --short --untracked-files=all -- frontend/assets/img/brands`
- `?? frontend/assets/img/brands/steam.png`
Production currently returns:
- `https://comunidadhll.devzamode.es/assets/img/brands/steam.png` -> `404`
So the remaining issue is deployment scope, not the frontend path.
The defensive `onerror` behavior remains in place.
Because this exact local asset exists and is the only Steam brand asset detected in:
- `frontend/assets/img/brands/`
- `frontend/assets/img/`
- `frontend/assets/`
it is the one asset that should be included with the TASK-250 frontend commit if the goal is to make the icon visible in production.
## Validation
Executed:
- `node --check frontend/assets/js/historico.js`
- `node --check frontend/assets/js/stats.js`
- `node --check frontend/assets/js/ranking.js`
Static validation performed:
- no visible `Deaths` label remains in Stats / Ranking tables or selects
- no visible `Teamkills` label remains in Stats / Ranking tables or selects
- `Kills/partida` no longer appears for Top muertes, Top soporte or Partidas 100+ kills
- Top muertes now uses `Muertes/partida`
- Stats and Ranking keep the same relative order for shared columns
Operational validation performed:
- production Steam icon URL returns `404`
- local `frontend/assets/img/brands/steam.png` exists but is not tracked
- no alternate local Steam asset name or extension was detected
## Notes
- No backend changes were required.
- No map, weapon or clan asset was modified.
- To fully fix the Steam icon in production, the existing local `steam.png` must be included in a later isolated commit.

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

View File

@@ -45,24 +45,32 @@ const LEADERBOARD_METRICS = Object.freeze([
key: "kills", key: "kills",
title: "Top kills", title: "Top kills",
valueHeading: "Kills", valueHeading: "Kills",
ratioHeading: "Kills/partida",
ratioMode: "kills",
emptyMessage: "Sin datos historicos suficientes para mostrar este ranking de kills.", emptyMessage: "Sin datos historicos suficientes para mostrar este ranking de kills.",
}, },
{ {
key: "deaths", key: "deaths",
title: "Top muertes", title: "Top muertes",
valueHeading: "Muertes", valueHeading: "Muertes",
ratioHeading: "Muertes/partida",
ratioMode: "deaths",
emptyMessage: "Sin datos historicos suficientes para mostrar este ranking de muertes.", emptyMessage: "Sin datos historicos suficientes para mostrar este ranking de muertes.",
}, },
{ {
key: "matches_over_100_kills", key: "matches_over_100_kills",
title: "Top partidas con 100+ kills", title: "Top partidas con 100+ kills",
valueHeading: "Partidas 100+", valueHeading: "Partidas 100+",
ratioHeading: null,
ratioMode: null,
emptyMessage: "Ningun jugador ha registrado partidas de 100+ kills en esta ventana.", emptyMessage: "Ningun jugador ha registrado partidas de 100+ kills en esta ventana.",
}, },
{ {
key: "support", key: "support",
title: "Top puntos de soporte", title: "Top puntos de soporte",
valueHeading: "Soporte", valueHeading: "Soporte",
ratioHeading: "Soporte/partida",
ratioMode: "support",
emptyMessage: "Sin datos historicos suficientes para mostrar este ranking de soporte.", emptyMessage: "Sin datos historicos suficientes para mostrar este ranking de soporte.",
}, },
]); ]);
@@ -94,6 +102,7 @@ document.addEventListener("DOMContentLoaded", () => {
const weeklyTableNode = document.getElementById("weekly-leaderboard-table"); const weeklyTableNode = document.getElementById("weekly-leaderboard-table");
const weeklyBodyNode = document.getElementById("weekly-leaderboard-body"); const weeklyBodyNode = document.getElementById("weekly-leaderboard-body");
const weeklyValueHeadingNode = document.getElementById("weekly-leaderboard-value-heading"); const weeklyValueHeadingNode = document.getElementById("weekly-leaderboard-value-heading");
const weeklyRatioHeadingNode = document.getElementById("weekly-leaderboard-ratio-heading");
const weeklyWindowNoteNode = document.getElementById("weekly-window-note"); const weeklyWindowNoteNode = document.getElementById("weekly-window-note");
const weeklySnapshotMetaNode = document.getElementById( const weeklySnapshotMetaNode = document.getElementById(
"weekly-leaderboard-snapshot-meta", "weekly-leaderboard-snapshot-meta",
@@ -210,6 +219,7 @@ document.addEventListener("DOMContentLoaded", () => {
weeklyBodyNode, weeklyBodyNode,
weeklyTitleNode, weeklyTitleNode,
weeklyValueHeadingNode, weeklyValueHeadingNode,
weeklyRatioHeadingNode,
weeklyWindowNoteNode, weeklyWindowNoteNode,
weeklySnapshotMetaNode, weeklySnapshotMetaNode,
activeMetricConfig, activeMetricConfig,
@@ -547,6 +557,7 @@ function hydrateWeeklyLeaderboard(
bodyNode, bodyNode,
titleNode, titleNode,
valueHeadingNode, valueHeadingNode,
ratioHeadingNode,
noteNode, noteNode,
snapshotMetaNode, snapshotMetaNode,
metricConfig, metricConfig,
@@ -555,6 +566,7 @@ function hydrateWeeklyLeaderboard(
const targetServerSlug = result.value?.data?.server_slug || activeServerSlug; const targetServerSlug = result.value?.data?.server_slug || activeServerSlug;
const resolvedTimeframeKey = result.value?.data?.timeframe || timeframeKey; const resolvedTimeframeKey = result.value?.data?.timeframe || timeframeKey;
valueHeadingNode.textContent = metricConfig.valueHeading; valueHeadingNode.textContent = metricConfig.valueHeading;
syncLeaderboardRatioColumn(tableNode, ratioHeadingNode, bodyNode, metricConfig);
if (result.status !== "fulfilled") { if (result.status !== "fulfilled") {
titleNode.textContent = buildLeaderboardTitle( titleNode.textContent = buildLeaderboardTitle(
metricConfig, metricConfig,
@@ -614,13 +626,11 @@ function hydrateWeeklyLeaderboard(
bodyNode.innerHTML = items bodyNode.innerHTML = items
.map( .map(
(item) => { (item) => {
const kills = resolveHistoricalKills(item, metricConfig);
const matches = Number(item.matches_considered); const matches = Number(item.matches_considered);
const killsPerMatch = formatHistoricalKillsPerMatch( const ratioValue = formatHistoricalRatio(item, metricConfig, matches);
item?.kills_per_match, const ratioCell = metricConfig.ratioMode
kills, ? `<td>${escapeHtml(ratioValue)}</td>`
matches, : "";
);
return ` return `
<tr> <tr>
@@ -628,13 +638,14 @@ function hydrateWeeklyLeaderboard(
<td>${escapeHtml(item.player?.name || "Jugador no identificado")}</td> <td>${escapeHtml(item.player?.name || "Jugador no identificado")}</td>
<td>${escapeHtml(formatNumber(item.metric_value))}</td> <td>${escapeHtml(formatNumber(item.metric_value))}</td>
<td>${escapeHtml(formatNumber(item.matches_considered))}</td> <td>${escapeHtml(formatNumber(item.matches_considered))}</td>
<td>${escapeHtml(killsPerMatch)}</td> ${ratioCell}
</tr> </tr>
`; `;
}, },
) )
.join(""); .join("");
stateNode.hidden = true; stateNode.hidden = true;
syncLeaderboardRatioColumn(tableNode, ratioHeadingNode, bodyNode, metricConfig);
tableNode.hidden = false; tableNode.hidden = false;
} }
@@ -1724,13 +1735,43 @@ function resolveHistoricalKills(item, metricConfig) {
return Number.NaN; return Number.NaN;
} }
function formatHistoricalKillsPerMatch(rawKillsPerMatch, rawKills, rawMatches) { function syncLeaderboardRatioColumn(tableNode, ratioHeadingNode, bodyNode, metricConfig) {
const directValue = Number(rawKillsPerMatch); if (!tableNode || !ratioHeadingNode || !bodyNode) {
return;
}
const showRatio = Boolean(metricConfig?.ratioMode);
ratioHeadingNode.hidden = !showRatio;
ratioHeadingNode.textContent = metricConfig?.ratioHeading || "";
const ratioColumnIndex = ratioHeadingNode.cellIndex;
bodyNode.querySelectorAll("tr").forEach((row) => {
const ratioCell = row.children[ratioColumnIndex];
if (ratioCell) {
ratioCell.hidden = !showRatio;
}
});
}
function formatHistoricalRatio(item, metricConfig, matches) {
if (!metricConfig?.ratioMode) {
return "";
}
if (metricConfig.ratioMode === "kills") {
const kills = resolveHistoricalKills(item, metricConfig);
return formatHistoricalPerMatch(item?.kills_per_match, kills, matches);
}
if (metricConfig.ratioMode === "deaths" || metricConfig.ratioMode === "support") {
return formatHistoricalPerMatch(null, Number(item?.metric_value), matches);
}
return "";
}
function formatHistoricalPerMatch(rawDirectValue, rawTotalValue, rawMatches) {
const directValue = Number(rawDirectValue);
if (Number.isFinite(directValue)) { if (Number.isFinite(directValue)) {
return formatDecimal(directValue, 2); return formatDecimal(directValue, 2);
} }
const kills = Number(rawKills); const kills = Number(rawTotalValue);
const matches = Number(rawMatches); const matches = Number(rawMatches);
if (!Number.isFinite(kills) || !Number.isFinite(matches) || matches <= 0) { if (!Number.isFinite(kills) || !Number.isFinite(matches) || matches <= 0) {
return "-"; return "-";

View File

@@ -271,7 +271,7 @@
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 Kills/partida.", "Usa Kills, Muertes, TK, Partidas, KD o Kills/partida.",
); );
return; return;
} }
@@ -397,10 +397,10 @@
</div> </div>
</td> </td>
<td class="ranking-table__metric">${formatMetricValue(item.metric_value, metric)}</td> <td class="ranking-table__metric">${formatMetricValue(item.metric_value, metric)}</td>
<td>${safeInt(item.matches_considered, 0)}</td>
<td>${safeInt(item.kills, 0)}</td> <td>${safeInt(item.kills, 0)}</td>
<td>${safeInt(item.deaths, 0)}</td> <td>${safeInt(item.deaths, 0)}</td>
<td>${safeInt(item.teamkills, 0)}</td> <td>${safeInt(item.teamkills, 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>
${hideKppColumn ? "" : `<td>${killsPerMatch}</td>`} ${hideKppColumn ? "" : `<td>${killsPerMatch}</td>`}
</tr> </tr>
@@ -454,10 +454,10 @@
function labelForMetric(metric) { function labelForMetric(metric) {
const labels = { const labels = {
kills: "Kills", kills: "Kills",
deaths: "Deaths", deaths: "Muertes",
teamkills: "Teamkills", teamkills: "TK",
matches_considered: "Partidas jugadas", matches_considered: "Partidas",
kd_ratio: "K/D", kd_ratio: "KD",
kills_per_match: "Kills/partida", kills_per_match: "Kills/partida",
}; };
return labels[metric] || "Kills"; return labels[metric] || "Kills";

View File

@@ -359,8 +359,8 @@
.map((item) => { .map((item) => {
const rank = safeInt(item.ranking_position, 0); const rank = safeInt(item.ranking_position, 0);
const playerName = escapeHtml(String(item.player_name || "Jugador sin nombre")); const playerName = escapeHtml(String(item.player_name || "Jugador sin nombre"));
const kills = safeInt(firstFiniteValue(item.kills, item.metric_value), 0);
const matches = safeInt(item.matches_considered, 0); const matches = safeInt(item.matches_considered, 0);
const kills = safeInt(firstFiniteValue(item.kills, item.metric_value), 0);
const killsPerMatch = formatKillsPerMatch( const killsPerMatch = formatKillsPerMatch(
item.kills_per_match, item.kills_per_match,
kills, kills,
@@ -378,12 +378,12 @@
<strong>${playerName}</strong> <strong>${playerName}</strong>
</div> </div>
</td> </td>
<td class="stats-annual-metric">${kills}</td>
<td>${matches}</td> <td>${matches}</td>
<td>${killsPerMatch}</td> <td class="stats-annual-metric">${kills}</td>
<td>${deaths}</td> <td>${deaths}</td>
<td>${teamkills}</td> <td>${teamkills}</td>
<td>${kd}</td> <td>${kd}</td>
<td>${killsPerMatch}</td>
</tr> </tr>
`; `;
}) })
@@ -396,12 +396,12 @@
<tr> <tr>
<th>Posici\u00f3n</th> <th>Posici\u00f3n</th>
<th>Jugador</th> <th>Jugador</th>
<th>Kills</th>
<th>Partidas</th> <th>Partidas</th>
<th>Kills/partida</th> <th>Kills</th>
<th>Muertes</th> <th>Muertes</th>
<th>Teamkills</th> <th>TK</th>
<th>K/D</th> <th>KD</th>
<th>Kills/partida</th>
</tr> </tr>
</thead> </thead>
<tbody>${rowsMarkup}</tbody> <tbody>${rowsMarkup}</tbody>
@@ -574,8 +574,8 @@
<article class="stats-summary-card"> <article class="stats-summary-card">
<p class="stats-summary-title">Ventana semanal</p> <p class="stats-summary-title">Ventana semanal</p>
<p><strong>Kills:</strong> ${safeInt(weeklyData?.kills, 0)}</p> <p><strong>Kills:</strong> ${safeInt(weeklyData?.kills, 0)}</p>
<p><strong>Deaths:</strong> ${safeInt(weeklyData?.deaths, 0)}</p> <p><strong>Muertes:</strong> ${safeInt(weeklyData?.deaths, 0)}</p>
<p><strong>Teamkills:</strong> ${safeInt(weeklyData?.teamkills, 0)}</p> <p><strong>TK:</strong> ${safeInt(weeklyData?.teamkills, 0)}</p>
</article> </article>
`; `;
@@ -590,8 +590,8 @@
<article class="stats-summary-card"> <article class="stats-summary-card">
<p class="stats-summary-title">Ventana mensual</p> <p class="stats-summary-title">Ventana mensual</p>
<p><strong>Kills:</strong> ${safeInt(monthlyData?.kills, 0)}</p> <p><strong>Kills:</strong> ${safeInt(monthlyData?.kills, 0)}</p>
<p><strong>Deaths:</strong> ${safeInt(monthlyData?.deaths, 0)}</p> <p><strong>Muertes:</strong> ${safeInt(monthlyData?.deaths, 0)}</p>
<p><strong>Teamkills:</strong> ${safeInt(monthlyData?.teamkills, 0)}</p> <p><strong>TK:</strong> ${safeInt(monthlyData?.teamkills, 0)}</p>
</article> </article>
`; `;
@@ -697,16 +697,16 @@
<span class="stats-comparison-card__metric-value">${kills}</span> <span class="stats-comparison-card__metric-value">${kills}</span>
</div> </div>
<div class="stats-comparison-card__metric"> <div class="stats-comparison-card__metric">
<span class="stats-comparison-card__metric-label">Deaths</span> <span class="stats-comparison-card__metric-label">Muertes</span>
<span class="stats-comparison-card__metric-value">${deaths}</span> <span class="stats-comparison-card__metric-value">${deaths}</span>
</div> </div>
<div class="stats-comparison-card__metric"> <div class="stats-comparison-card__metric">
<span class="stats-comparison-card__metric-label">K/D</span> <span class="stats-comparison-card__metric-label">KD</span>
<span class="stats-comparison-card__metric-value">${kdRatio}</span> <span class="stats-comparison-card__metric-value">${kdRatio}</span>
</div> </div>
</div> </div>
<p class="stats-comparison-card__detail"> <p class="stats-comparison-card__detail">
<strong>Kills por partida:</strong> ${killsPerMatch} <strong>Kills/partida:</strong> ${killsPerMatch}
</p> </p>
<p class="stats-comparison-card__note">${escapeHtml(rankingState.detail)}</p> <p class="stats-comparison-card__note">${escapeHtml(rankingState.detail)}</p>
</article> </article>
@@ -739,11 +739,11 @@
<span class="stats-comparison-card__metric-value">${safeDecimal(monthlyKillsPerMatch, 2, "0.00")}</span> <span class="stats-comparison-card__metric-value">${safeDecimal(monthlyKillsPerMatch, 2, "0.00")}</span>
</div> </div>
<div class="stats-comparison-card__metric"> <div class="stats-comparison-card__metric">
<span class="stats-comparison-card__metric-label">K/D semanal</span> <span class="stats-comparison-card__metric-label">KD semanal</span>
<span class="stats-comparison-card__metric-value">${safeDecimal(weeklyKd, 2, "0.00")}</span> <span class="stats-comparison-card__metric-value">${safeDecimal(weeklyKd, 2, "0.00")}</span>
</div> </div>
<div class="stats-comparison-card__metric"> <div class="stats-comparison-card__metric">
<span class="stats-comparison-card__metric-label">K/D mensual</span> <span class="stats-comparison-card__metric-label">KD mensual</span>
<span class="stats-comparison-card__metric-value">${safeDecimal(monthlyKd, 2, "0.00")}</span> <span class="stats-comparison-card__metric-value">${safeDecimal(monthlyKd, 2, "0.00")}</span>
</div> </div>
</div> </div>
@@ -775,7 +775,7 @@
} }
const rank = safeInt(ranking.ranking_position, 0); const rank = safeInt(ranking.ranking_position, 0);
const metric = escapeHtml(String(ranking.metric || annualMetric)); const metric = escapeHtml(labelForStatsMetric(ranking.metric || annualMetric));
return ` return `
<p><strong>Posici\u00f3n:</strong> ${rank}</p> <p><strong>Posici\u00f3n:</strong> ${rank}</p>
@@ -863,6 +863,18 @@
return `Ventana: ${windowStart} - ${windowEnd}`; return `Ventana: ${windowStart} - ${windowEnd}`;
} }
function labelForStatsMetric(metric) {
const labels = {
kills: "Kills",
deaths: "Muertes",
teamkills: "TK",
matches_considered: "Partidas",
kd_ratio: "KD",
kills_per_match: "Kills/partida",
};
return labels[String(metric || "").trim()] || "Kills";
}
function safeInt(value, fallback = 0) { function safeInt(value, fallback = 0) {
const parsed = Number(value); const parsed = Number(value);
if (!Number.isFinite(parsed)) { if (!Number.isFinite(parsed)) {

View File

@@ -199,7 +199,7 @@
<th>Jugador</th> <th>Jugador</th>
<th id="weekly-leaderboard-value-heading">Kills</th> <th id="weekly-leaderboard-value-heading">Kills</th>
<th>Partidas</th> <th>Partidas</th>
<th>Kills/partida</th> <th id="weekly-leaderboard-ratio-heading">Kills/partida</th>
</tr> </tr>
</thead> </thead>
<tbody id="weekly-leaderboard-body"></tbody> <tbody id="weekly-leaderboard-body"></tbody>

View File

@@ -87,11 +87,11 @@
<label class="stats-search-form__label" for="ranking-metric"> <label class="stats-search-form__label" for="ranking-metric">
M&eacute;trica M&eacute;trica
<select class="ranking-select" id="ranking-metric" name="metric"> <select class="ranking-select" id="ranking-metric" name="metric">
<option value="kills">Kills totales</option> <option value="kills">Kills</option>
<option value="deaths">Deaths</option> <option value="deaths">Muertes</option>
<option value="teamkills">Teamkills</option> <option value="teamkills">TK</option>
<option value="matches_considered">Partidas jugadas</option> <option value="matches_considered">Partidas</option>
<option value="kd_ratio">K/D</option> <option value="kd_ratio">KD</option>
<option value="kills_per_match">Kills/partida</option> <option value="kills_per_match">Kills/partida</option>
</select> </select>
</label> </label>
@@ -120,11 +120,11 @@
<th>Pos.</th> <th>Pos.</th>
<th>Jugador</th> <th>Jugador</th>
<th id="ranking-metric-heading">Valor activo</th> <th id="ranking-metric-heading">Valor activo</th>
<th>Kills</th>
<th>Deaths</th>
<th>Teamkills</th>
<th>Partidas</th> <th>Partidas</th>
<th>K/D</th> <th>Kills</th>
<th>Muertes</th>
<th>TK</th>
<th>KD</th>
<th id="ranking-kpp-heading">Kills/partida</th> <th id="ranking-kpp-heading">Kills/partida</th>
</tr> </tr>
</thead> </thead>