Fix historical leaderboard labels, stats ranking table uniformity and publish Steam icon
This commit is contained in:
@@ -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.
|
||||
BIN
frontend/assets/img/brands/steam.png
Normal file
BIN
frontend/assets/img/brands/steam.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 284 KiB |
@@ -45,24 +45,32 @@ const LEADERBOARD_METRICS = Object.freeze([
|
||||
key: "kills",
|
||||
title: "Top kills",
|
||||
valueHeading: "Kills",
|
||||
ratioHeading: "Kills/partida",
|
||||
ratioMode: "kills",
|
||||
emptyMessage: "Sin datos historicos suficientes para mostrar este ranking de kills.",
|
||||
},
|
||||
{
|
||||
key: "deaths",
|
||||
title: "Top muertes",
|
||||
valueHeading: "Muertes",
|
||||
ratioHeading: "Muertes/partida",
|
||||
ratioMode: "deaths",
|
||||
emptyMessage: "Sin datos historicos suficientes para mostrar este ranking de muertes.",
|
||||
},
|
||||
{
|
||||
key: "matches_over_100_kills",
|
||||
title: "Top partidas con 100+ kills",
|
||||
valueHeading: "Partidas 100+",
|
||||
ratioHeading: null,
|
||||
ratioMode: null,
|
||||
emptyMessage: "Ningun jugador ha registrado partidas de 100+ kills en esta ventana.",
|
||||
},
|
||||
{
|
||||
key: "support",
|
||||
title: "Top puntos de soporte",
|
||||
valueHeading: "Soporte",
|
||||
ratioHeading: "Soporte/partida",
|
||||
ratioMode: "support",
|
||||
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 weeklyBodyNode = document.getElementById("weekly-leaderboard-body");
|
||||
const weeklyValueHeadingNode = document.getElementById("weekly-leaderboard-value-heading");
|
||||
const weeklyRatioHeadingNode = document.getElementById("weekly-leaderboard-ratio-heading");
|
||||
const weeklyWindowNoteNode = document.getElementById("weekly-window-note");
|
||||
const weeklySnapshotMetaNode = document.getElementById(
|
||||
"weekly-leaderboard-snapshot-meta",
|
||||
@@ -210,6 +219,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
weeklyBodyNode,
|
||||
weeklyTitleNode,
|
||||
weeklyValueHeadingNode,
|
||||
weeklyRatioHeadingNode,
|
||||
weeklyWindowNoteNode,
|
||||
weeklySnapshotMetaNode,
|
||||
activeMetricConfig,
|
||||
@@ -547,6 +557,7 @@ function hydrateWeeklyLeaderboard(
|
||||
bodyNode,
|
||||
titleNode,
|
||||
valueHeadingNode,
|
||||
ratioHeadingNode,
|
||||
noteNode,
|
||||
snapshotMetaNode,
|
||||
metricConfig,
|
||||
@@ -555,6 +566,7 @@ function hydrateWeeklyLeaderboard(
|
||||
const targetServerSlug = result.value?.data?.server_slug || activeServerSlug;
|
||||
const resolvedTimeframeKey = result.value?.data?.timeframe || timeframeKey;
|
||||
valueHeadingNode.textContent = metricConfig.valueHeading;
|
||||
syncLeaderboardRatioColumn(tableNode, ratioHeadingNode, bodyNode, metricConfig);
|
||||
if (result.status !== "fulfilled") {
|
||||
titleNode.textContent = buildLeaderboardTitle(
|
||||
metricConfig,
|
||||
@@ -614,13 +626,11 @@ function hydrateWeeklyLeaderboard(
|
||||
bodyNode.innerHTML = items
|
||||
.map(
|
||||
(item) => {
|
||||
const kills = resolveHistoricalKills(item, metricConfig);
|
||||
const matches = Number(item.matches_considered);
|
||||
const killsPerMatch = formatHistoricalKillsPerMatch(
|
||||
item?.kills_per_match,
|
||||
kills,
|
||||
matches,
|
||||
);
|
||||
const ratioValue = formatHistoricalRatio(item, metricConfig, matches);
|
||||
const ratioCell = metricConfig.ratioMode
|
||||
? `<td>${escapeHtml(ratioValue)}</td>`
|
||||
: "";
|
||||
|
||||
return `
|
||||
<tr>
|
||||
@@ -628,13 +638,14 @@ function hydrateWeeklyLeaderboard(
|
||||
<td>${escapeHtml(item.player?.name || "Jugador no identificado")}</td>
|
||||
<td>${escapeHtml(formatNumber(item.metric_value))}</td>
|
||||
<td>${escapeHtml(formatNumber(item.matches_considered))}</td>
|
||||
<td>${escapeHtml(killsPerMatch)}</td>
|
||||
${ratioCell}
|
||||
</tr>
|
||||
`;
|
||||
},
|
||||
)
|
||||
.join("");
|
||||
stateNode.hidden = true;
|
||||
syncLeaderboardRatioColumn(tableNode, ratioHeadingNode, bodyNode, metricConfig);
|
||||
tableNode.hidden = false;
|
||||
}
|
||||
|
||||
@@ -1724,13 +1735,43 @@ function resolveHistoricalKills(item, metricConfig) {
|
||||
return Number.NaN;
|
||||
}
|
||||
|
||||
function formatHistoricalKillsPerMatch(rawKillsPerMatch, rawKills, rawMatches) {
|
||||
const directValue = Number(rawKillsPerMatch);
|
||||
function syncLeaderboardRatioColumn(tableNode, ratioHeadingNode, bodyNode, metricConfig) {
|
||||
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)) {
|
||||
return formatDecimal(directValue, 2);
|
||||
}
|
||||
|
||||
const kills = Number(rawKills);
|
||||
const kills = Number(rawTotalValue);
|
||||
const matches = Number(rawMatches);
|
||||
if (!Number.isFinite(kills) || !Number.isFinite(matches) || matches <= 0) {
|
||||
return "-";
|
||||
|
||||
@@ -271,7 +271,7 @@
|
||||
if (statusCode === 400 && normalizedMessage.includes("metric")) {
|
||||
setRankingState("warning", "La m\u00e9trica solicitada no est\u00e1 soportada.");
|
||||
renderEmptyState(
|
||||
"Usa kills, deaths, teamkills, partidas jugadas, K/D o Kills/partida.",
|
||||
"Usa Kills, Muertes, TK, Partidas, KD o Kills/partida.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -397,10 +397,10 @@
|
||||
</div>
|
||||
</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.deaths, 0)}</td>
|
||||
<td>${safeInt(item.teamkills, 0)}</td>
|
||||
<td>${safeInt(item.matches_considered, 0)}</td>
|
||||
<td>${safeDecimal(item.kd_ratio, 2, "0.00")}</td>
|
||||
${hideKppColumn ? "" : `<td>${killsPerMatch}</td>`}
|
||||
</tr>
|
||||
@@ -454,10 +454,10 @@
|
||||
function labelForMetric(metric) {
|
||||
const labels = {
|
||||
kills: "Kills",
|
||||
deaths: "Deaths",
|
||||
teamkills: "Teamkills",
|
||||
matches_considered: "Partidas jugadas",
|
||||
kd_ratio: "K/D",
|
||||
deaths: "Muertes",
|
||||
teamkills: "TK",
|
||||
matches_considered: "Partidas",
|
||||
kd_ratio: "KD",
|
||||
kills_per_match: "Kills/partida",
|
||||
};
|
||||
return labels[metric] || "Kills";
|
||||
|
||||
@@ -359,8 +359,8 @@
|
||||
.map((item) => {
|
||||
const rank = safeInt(item.ranking_position, 0);
|
||||
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 kills = safeInt(firstFiniteValue(item.kills, item.metric_value), 0);
|
||||
const killsPerMatch = formatKillsPerMatch(
|
||||
item.kills_per_match,
|
||||
kills,
|
||||
@@ -378,12 +378,12 @@
|
||||
<strong>${playerName}</strong>
|
||||
</div>
|
||||
</td>
|
||||
<td class="stats-annual-metric">${kills}</td>
|
||||
<td>${matches}</td>
|
||||
<td>${killsPerMatch}</td>
|
||||
<td class="stats-annual-metric">${kills}</td>
|
||||
<td>${deaths}</td>
|
||||
<td>${teamkills}</td>
|
||||
<td>${kd}</td>
|
||||
<td>${killsPerMatch}</td>
|
||||
</tr>
|
||||
`;
|
||||
})
|
||||
@@ -396,12 +396,12 @@
|
||||
<tr>
|
||||
<th>Posici\u00f3n</th>
|
||||
<th>Jugador</th>
|
||||
<th>Kills</th>
|
||||
<th>Partidas</th>
|
||||
<th>Kills/partida</th>
|
||||
<th>Kills</th>
|
||||
<th>Muertes</th>
|
||||
<th>Teamkills</th>
|
||||
<th>K/D</th>
|
||||
<th>TK</th>
|
||||
<th>KD</th>
|
||||
<th>Kills/partida</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rowsMarkup}</tbody>
|
||||
@@ -574,8 +574,8 @@
|
||||
<article class="stats-summary-card">
|
||||
<p class="stats-summary-title">Ventana semanal</p>
|
||||
<p><strong>Kills:</strong> ${safeInt(weeklyData?.kills, 0)}</p>
|
||||
<p><strong>Deaths:</strong> ${safeInt(weeklyData?.deaths, 0)}</p>
|
||||
<p><strong>Teamkills:</strong> ${safeInt(weeklyData?.teamkills, 0)}</p>
|
||||
<p><strong>Muertes:</strong> ${safeInt(weeklyData?.deaths, 0)}</p>
|
||||
<p><strong>TK:</strong> ${safeInt(weeklyData?.teamkills, 0)}</p>
|
||||
</article>
|
||||
`;
|
||||
|
||||
@@ -590,8 +590,8 @@
|
||||
<article class="stats-summary-card">
|
||||
<p class="stats-summary-title">Ventana mensual</p>
|
||||
<p><strong>Kills:</strong> ${safeInt(monthlyData?.kills, 0)}</p>
|
||||
<p><strong>Deaths:</strong> ${safeInt(monthlyData?.deaths, 0)}</p>
|
||||
<p><strong>Teamkills:</strong> ${safeInt(monthlyData?.teamkills, 0)}</p>
|
||||
<p><strong>Muertes:</strong> ${safeInt(monthlyData?.deaths, 0)}</p>
|
||||
<p><strong>TK:</strong> ${safeInt(monthlyData?.teamkills, 0)}</p>
|
||||
</article>
|
||||
`;
|
||||
|
||||
@@ -697,16 +697,16 @@
|
||||
<span class="stats-comparison-card__metric-value">${kills}</span>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<p class="stats-comparison-card__detail">
|
||||
<strong>Kills por partida:</strong> ${killsPerMatch}
|
||||
<strong>Kills/partida:</strong> ${killsPerMatch}
|
||||
</p>
|
||||
<p class="stats-comparison-card__note">${escapeHtml(rankingState.detail)}</p>
|
||||
</article>
|
||||
@@ -739,11 +739,11 @@
|
||||
<span class="stats-comparison-card__metric-value">${safeDecimal(monthlyKillsPerMatch, 2, "0.00")}</span>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -775,7 +775,7 @@
|
||||
}
|
||||
|
||||
const rank = safeInt(ranking.ranking_position, 0);
|
||||
const metric = escapeHtml(String(ranking.metric || annualMetric));
|
||||
const metric = escapeHtml(labelForStatsMetric(ranking.metric || annualMetric));
|
||||
|
||||
return `
|
||||
<p><strong>Posici\u00f3n:</strong> ${rank}</p>
|
||||
@@ -863,6 +863,18 @@
|
||||
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) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
|
||||
@@ -199,7 +199,7 @@
|
||||
<th>Jugador</th>
|
||||
<th id="weekly-leaderboard-value-heading">Kills</th>
|
||||
<th>Partidas</th>
|
||||
<th>Kills/partida</th>
|
||||
<th id="weekly-leaderboard-ratio-heading">Kills/partida</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="weekly-leaderboard-body"></tbody>
|
||||
|
||||
@@ -87,11 +87,11 @@
|
||||
<label class="stats-search-form__label" for="ranking-metric">
|
||||
Métrica
|
||||
<select class="ranking-select" id="ranking-metric" name="metric">
|
||||
<option value="kills">Kills totales</option>
|
||||
<option value="deaths">Deaths</option>
|
||||
<option value="teamkills">Teamkills</option>
|
||||
<option value="matches_considered">Partidas jugadas</option>
|
||||
<option value="kd_ratio">K/D</option>
|
||||
<option value="kills">Kills</option>
|
||||
<option value="deaths">Muertes</option>
|
||||
<option value="teamkills">TK</option>
|
||||
<option value="matches_considered">Partidas</option>
|
||||
<option value="kd_ratio">KD</option>
|
||||
<option value="kills_per_match">Kills/partida</option>
|
||||
</select>
|
||||
</label>
|
||||
@@ -120,11 +120,11 @@
|
||||
<th>Pos.</th>
|
||||
<th>Jugador</th>
|
||||
<th id="ranking-metric-heading">Valor activo</th>
|
||||
<th>Kills</th>
|
||||
<th>Deaths</th>
|
||||
<th>Teamkills</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>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
Reference in New Issue
Block a user