diff --git a/ai/tasks/done/TASK-248-add-real-kpm-to-public-tables-and-fix-kills-per-match-labels.md b/ai/tasks/done/TASK-248-add-real-kpm-to-public-tables-and-fix-kills-per-match-labels.md new file mode 100644 index 0000000..090321a --- /dev/null +++ b/ai/tasks/done/TASK-248-add-real-kpm-to-public-tables-and-fix-kills-per-match-labels.md @@ -0,0 +1,87 @@ +# TASK-248 - Add real KPM to public tables and fix kills per match labels + +## Summary + +This task extends the already validated real historical KPM to the main player table in `historico-partida.html` and removes misleading public `KPM` labels that were actually rendering `kills_per_match`. + +It does not change the real KPM formula already validated in production. + +## Files Read First + +- `frontend/historico-partida.html` +- `frontend/assets/js/historico-partida.js` +- `frontend/assets/css/historico.css` +- `frontend/historico.html` +- `frontend/assets/js/historico.js` +- `frontend/stats.html` +- `frontend/assets/js/stats.js` +- `frontend/ranking.html` +- `frontend/assets/js/ranking.js` +- `backend/app/rcon_historical_read_model.py` +- `docs/HISTORICAL_MATCH_KILLS_PER_MINUTE_ANALYSIS.md` + +## Problem + +The repository already exposed real KPM at player level in `/api/historical/matches/detail`, but the public UI still had two problems: + +1. `historico-partida` only showed real KPM in the expanded player panel, not in the main table. +2. Several public surfaces were calling `kills_per_match` by the visible label `KPM`, which is incorrect. + +## Decision + +Keep the validated real KPM model unchanged: + +- only `kpm_status = ready` renders a visible value +- no total-match-duration fallback +- no `event_span_fallback` as ready KPM + +Apply two UI rules: + +- historical match detail main table gets a real `KPM` column +- any visible `kills_per_match` label becomes `Kills/partida` + +Do not add public aggregated weekly/monthly/annual real KPM yet. That would need a separate backend and snapshot coverage contract so mixed datasets do not produce misleading results. + +## Implementation + +### Historical match detail + +- Added a sortable `KPM` option in the player table. +- Added a `KPM` column in the main player list. +- The cell stays empty unless `kpm_status == "ready"`. +- The expanded panel keeps its existing real KPM chip. + +### Historical weekly/monthly tables + +- Renamed the last leaderboard metric from `KPM` to `Kills/partida`. +- Kept the underlying `kills_per_match` meaning unchanged. + +### Stats and ranking + +- Annual stats table now labels `kills_per_match` as `Kills/partida`. +- Weekly/monthly comparison cards now label the same metric as `Kills/partida semanal` and `Kills/partida mensual`. +- Ranking already used `Kills/partida`, so no semantic rename was needed there. + +## Validation + +Executed: + +- `node --check frontend/assets/js/historico-partida.js` +- `node --check frontend/assets/js/historico.js` +- `node --check frontend/assets/js/stats.js` +- `node --check frontend/assets/js/ranking.js` + +Static validation performed: + +- confirmed `historico-partida.html` main table now shows `KPM` +- confirmed expanded player panel still shows real KPM +- confirmed rows with `insufficient_active_time` keep the main table cell empty +- confirmed historical weekly/monthly tables no longer show `KPM` for `kills_per_match` +- confirmed annual stats no longer show `KPM` for `kills_per_match` +- confirmed ranking still uses `Kills/partida` + +## Notes + +- No backend changes were required for this task. +- No scheduler, RCON or server configuration changes were made. +- Public aggregated real KPM for weekly/monthly/annual remains pending a dedicated coverage-safe snapshot design. diff --git a/docs/HISTORICAL_MATCH_KILLS_PER_MINUTE_ANALYSIS.md b/docs/HISTORICAL_MATCH_KILLS_PER_MINUTE_ANALYSIS.md index 8300832..20b26ef 100644 --- a/docs/HISTORICAL_MATCH_KILLS_PER_MINUTE_ANALYSIS.md +++ b/docs/HISTORICAL_MATCH_KILLS_PER_MINUTE_ANALYSIS.md @@ -159,6 +159,55 @@ For those rows: If the value is missing or below threshold, the panel stays clean and does not show a fake metric. +## Public Tables + +The historical match detail now exposes the same real KPM in two public places: + +- expanded player panel in `historico-partida.html` +- main players table in `historico-partida.html` + +The main table leaves the KPM cell empty when `kpm_status != "ready"`. + +This avoids false `0.00` values for: + +- legacy rows without active time +- rows below `HLL_KPM_MIN_ACTIVE_SECONDS` +- rows that only expose `event_span_fallback` + +## Kills Per Match Labels + +Several public tables were showing `kills_per_match` under the visible label `KPM`. + +That is no longer acceptable because: + +- `kills_per_match` means kills divided by matches considered +- real KPM means kills divided by active minutes from reliable connection intervals + +Public surfaces that now use `Kills/partida` instead of `KPM` for `kills_per_match`: + +- historical weekly and monthly leaderboard tables +- annual stats summary table +- annual stats comparison cards +- ranking metric selector and ranking table label + +## Aggregated Real KPM + +This repository does not yet expose a public weekly, monthly or annual aggregate KPM based only on: + +- `active_time_source = connection_intervals` +- `active_time_source = connection_intervals_carryover` + +That was intentionally left out of this change because the public leaderboard and ranking snapshots would need an explicit coverage contract before mixing: + +- rows with real active time +- older rows without it +- fallback rows blocked from `kpm_status = ready` + +Until that contract exists, public aggregated views keep: + +- `Kills/partida` for `kills_per_match` +- real KPM only at historical match detail player level + ## Limitations - This is observed active time from AdminLog connection evidence, not exact join/leave telemetry for every silent second. diff --git a/frontend/assets/css/historico.css b/frontend/assets/css/historico.css index b4e5954..3e9a77b 100644 --- a/frontend/assets/css/historico.css +++ b/frontend/assets/css/historico.css @@ -701,7 +701,7 @@ } .historical-table--players { - min-width: 920px; + min-width: 1000px; } .historical-table__position { diff --git a/frontend/assets/js/historico-partida.js b/frontend/assets/js/historico-partida.js index c1f9a2b..99d9cc9 100644 --- a/frontend/assets/js/historico-partida.js +++ b/frontend/assets/js/historico-partida.js @@ -340,6 +340,7 @@ function getVisiblePlayers(players, item, state) { } function comparePlayerEntries(a, b, item, state) { + void item; if (state.isDefaultSort) { return ( compareInactivePriority(a, b) || @@ -349,6 +350,19 @@ function comparePlayerEntries(a, b, item, state) { ); } + if (state.sort === "kpm") { + const readyPriority = compareReadyKpmPriority(a.player, b.player); + if (readyPriority) { + return readyPriority; + } + const direction = state.direction === "asc" ? 1 : -1; + const compared = compareNumericStat( + getReadyKpmValue(a.player), + getReadyKpmValue(b.player), + ); + return compared * direction || comparePlayerNames(a.player, b.player); + } + if (!["name", "team"].includes(state.sort)) { const inactivePriority = compareInactivePriority(a, b); if (inactivePriority) { @@ -362,6 +376,7 @@ function comparePlayerEntries(a, b, item, state) { } function comparePlayerSortValue(a, b, item, sort) { + void item; if (sort === "name") { return comparePlayerNames(a.player, b.player); } @@ -377,6 +392,15 @@ function comparePlayerSortValue(a, b, item, sort) { return compareNumericStat(a.player.kills, b.player.kills); } +function compareReadyKpmPriority(leftPlayer, rightPlayer) { + const leftReady = leftPlayer?.kpm_status === "ready"; + const rightReady = rightPlayer?.kpm_status === "ready"; + if (leftReady === rightReady) { + return 0; + } + return leftReady ? -1 : 1; +} + function compareInactivePriority(a, b) { return Number(a.inactive) - Number(b.inactive); } @@ -426,13 +450,14 @@ function renderPlayerRows(player, item, index, inactive = false) { ${escapeHtml(formatOptionalNumber(player.deaths))} ${escapeHtml(formatOptionalNumber(player.teamkills))} ${escapeHtml(formatKdRatio(player))} + ${escapeHtml(formatReadyKpmValue(player))} - + ${renderPlayerStatsPanel(player, item, { team, playerName })} @@ -887,6 +912,22 @@ function formatKdRatio(player) { return formatDecimal(getKdRatioValue(player), 2); } +function getReadyKpmValue(player) { + if (player?.kpm_status !== "ready") { + return Number.NaN; + } + const value = Number(player.kpm); + return Number.isFinite(value) ? value : Number.NaN; +} + +function formatReadyKpmValue(player) { + const value = getReadyKpmValue(player); + if (!Number.isFinite(value)) { + return ""; + } + return formatDecimal(value, 2); +} + function getKdRatioValue(player) { if (Number.isFinite(Number(player.kd_ratio))) { return Number(player.kd_ratio); diff --git a/frontend/assets/js/historico.js b/frontend/assets/js/historico.js index 324bf2e..2821349 100644 --- a/frontend/assets/js/historico.js +++ b/frontend/assets/js/historico.js @@ -616,7 +616,11 @@ function hydrateWeeklyLeaderboard( (item) => { const kills = resolveHistoricalKills(item, metricConfig); const matches = Number(item.matches_considered); - const kpm = formatHistoricalKpm(item?.kills_per_match, kills, matches); + const killsPerMatch = formatHistoricalKillsPerMatch( + item?.kills_per_match, + kills, + matches, + ); return ` @@ -624,7 +628,7 @@ function hydrateWeeklyLeaderboard( ${escapeHtml(item.player?.name || "Jugador no identificado")} ${escapeHtml(formatNumber(item.metric_value))} ${escapeHtml(formatNumber(item.matches_considered))} - ${escapeHtml(kpm)} + ${escapeHtml(killsPerMatch)} `; }, @@ -1720,7 +1724,7 @@ function resolveHistoricalKills(item, metricConfig) { return Number.NaN; } -function formatHistoricalKpm(rawKillsPerMatch, rawKills, rawMatches) { +function formatHistoricalKillsPerMatch(rawKillsPerMatch, rawKills, rawMatches) { const directValue = Number(rawKillsPerMatch); if (Number.isFinite(directValue)) { return formatDecimal(directValue, 2); diff --git a/frontend/assets/js/stats.js b/frontend/assets/js/stats.js index f6ba870..1fc2feb 100644 --- a/frontend/assets/js/stats.js +++ b/frontend/assets/js/stats.js @@ -361,7 +361,11 @@ 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 kpm = formatKpm(item.kills_per_match, kills, matches); + const killsPerMatch = formatKillsPerMatch( + item.kills_per_match, + kills, + matches, + ); const deaths = safeInt(item.deaths, 0); const teamkills = safeInt(item.teamkills, 0); const kd = safeDecimal(item.kd_ratio, 2, "0.00"); @@ -376,7 +380,7 @@ ${kills} ${matches} - ${kpm} + ${killsPerMatch} ${deaths} ${teamkills} ${kd} @@ -394,7 +398,7 @@ Jugador Kills Partidas - KPM + Kills/partida Muertes Teamkills K/D @@ -710,8 +714,8 @@ } function renderDeltaComparisonCard(weeklyData, monthlyData) { - const weeklyKpm = safeParseNumber(weeklyData?.kills_per_match); - const monthlyKpm = safeParseNumber(monthlyData?.kills_per_match); + const weeklyKillsPerMatch = safeParseNumber(weeklyData?.kills_per_match); + const monthlyKillsPerMatch = safeParseNumber(monthlyData?.kills_per_match); const weeklyKd = safeParseNumber(weeklyData?.kd_ratio); const monthlyKd = safeParseNumber(monthlyData?.kd_ratio); const killsDelta = safeInt(monthlyData?.kills, 0) - safeInt(weeklyData?.kills, 0); @@ -727,12 +731,12 @@
- KPM semanal - ${safeDecimal(weeklyKpm, 2, "0.00")} + Kills/partida semanal + ${safeDecimal(weeklyKillsPerMatch, 2, "0.00")}
- KPM mensual - ${safeDecimal(monthlyKpm, 2, "0.00")} + Kills/partida mensual + ${safeDecimal(monthlyKillsPerMatch, 2, "0.00")}
K/D semanal @@ -893,7 +897,7 @@ return Number.NaN; } - function formatKpm(rawKillsPerMatch, rawKills, rawMatches) { + function formatKillsPerMatch(rawKillsPerMatch, rawKills, rawMatches) { const directValue = safeParseNumber(rawKillsPerMatch); if (Number.isFinite(directValue)) { return safeDecimal(directValue, 2, "0.00"); diff --git a/frontend/historico-partida.html b/frontend/historico-partida.html index dcda54f..c792edf 100644 --- a/frontend/historico-partida.html +++ b/frontend/historico-partida.html @@ -138,6 +138,7 @@ + @@ -159,6 +160,7 @@ Muertes TK KD + KPM diff --git a/frontend/historico.html b/frontend/historico.html index f991b46..a3f052b 100644 --- a/frontend/historico.html +++ b/frontend/historico.html @@ -199,7 +199,7 @@ Jugador Kills Partidas - KPM + Kills/partida