From ca0874061918898a1089351eae2f566a139e4abb Mon Sep 17 00:00:00 2001 From: devRaGonSa Date: Tue, 9 Jun 2026 16:29:25 +0200 Subject: [PATCH] Load annual ranking by default and add KPM columns --- ...al-ranking-default-load-and-kpm-columns.md | 171 ++++++++++++++++++ .../css/historico-scoreboard-detail.css | 11 ++ frontend/assets/css/styles.css | 71 ++++++-- frontend/assets/img/brands/helo-system.png | Bin 0 -> 6624 bytes frontend/assets/img/brands/hllor.webp | Bin 0 -> 6950 bytes frontend/assets/img/brands/hllrecords.png | Bin 0 -> 9233 bytes frontend/assets/js/historico-partida.js | 44 ++++- frontend/assets/js/historico.js | 42 ++++- frontend/assets/js/main.js | 6 +- frontend/assets/js/ranking.js | 43 ++++- frontend/assets/js/stats.js | 116 ++++++++---- frontend/historico.html | 1 + frontend/ranking.html | 4 +- frontend/stats.html | 35 ++-- 14 files changed, 456 insertions(+), 88 deletions(-) create mode 100644 ai/tasks/done/TASK-207-annual-ranking-default-load-and-kpm-columns.md create mode 100644 frontend/assets/img/brands/helo-system.png create mode 100644 frontend/assets/img/brands/hllor.webp create mode 100644 frontend/assets/img/brands/hllrecords.png diff --git a/ai/tasks/done/TASK-207-annual-ranking-default-load-and-kpm-columns.md b/ai/tasks/done/TASK-207-annual-ranking-default-load-and-kpm-columns.md new file mode 100644 index 0000000..e0b7e60 --- /dev/null +++ b/ai/tasks/done/TASK-207-annual-ranking-default-load-and-kpm-columns.md @@ -0,0 +1,171 @@ +--- +id: TASK-207 +title: Annual ranking default load and KPM columns +status: done +type: frontend +team: Frontend Senior +supporting_teams: + - Experto en interfaz +roadmap_item: foundation +priority: medium +--- + +# TASK-207 - Annual ranking default load and KPM columns + +## Goal + +Ajustar el ranking anual publico en `stats` para que muestre la temporada fija 2026 con carga automatica inicial y anadir la columna `KPM` en los listados publicos multi-registro donde existan datos suficientes sin tocar backend ni endpoints. + +## Context + +Tras `TASK-206`, la presentacion publica del frontend ya es consistente, pero el bloque anual de `stats` sigue requiriendo seleccion manual de anio y una accion extra para cargar un ranking que ahora debe mostrarse como lectura fija de temporada. Ademas, las tablas publicas con varios jugadores no muestran de forma uniforme `KPM` aunque ya existen datos o pueden derivarse desde `kills` y `matches_considered`. + +Preserve the current product identity: Spanish-speaking HLL Vietnam community, military/Vietnam/tactical/sober visual direction and controlled repository evolution. + +## Steps + +1. Crear la task y moverla a `ai/tasks/in-progress`. +2. Fijar el ranking anual de `stats` a la temporada 2026 sin selector editable de anio ni boton obligatorio de carga. +3. Cargar automaticamente el ranking anual al abrir `stats.html`, manteniendo estados de carga y error controlados. +4. Cambiar la tabla anual para mostrar `Kills` y anadir `KPM`, usando `kills_per_match` cuando exista o calculandolo desde `kills / matches_considered`. +5. Revisar `ranking.html` y `historico.html` para unificar la etiqueta visual a `KPM` y anadir la columna donde aplique sin duplicarla. +6. Validar por inspeccion, revisar `git diff --name-only`, documentar exclusiones y mover la task a `ai/tasks/done` al terminar. + +## Files to Read First + +- `AGENTS.md` +- `ai/repo-context.md` +- `ai/architecture-index.md` +- `frontend/stats.html` +- `frontend/ranking.html` +- `frontend/historico.html` +- `frontend/assets/js/stats.js` +- `frontend/assets/js/ranking.js` +- `frontend/assets/js/historico.js` +- `frontend/assets/css/styles.css` +- `ai/tasks/done/TASK-206-public-frontend-visual-consistency-and-annual-ranking-layout.md` + +## Expected Files to Modify + +- `ai/tasks/done/TASK-207-annual-ranking-default-load-and-kpm-columns.md` +- `frontend/stats.html` +- `frontend/ranking.html` +- `frontend/historico.html` +- `frontend/assets/js/stats.js` +- `frontend/assets/js/ranking.js` +- `frontend/assets/js/historico.js` +- `frontend/assets/css/styles.css` + +## Constraints + +- Keep the change minimal. +- Preserve HLL Vietnam project identity. +- Do not introduce unnecessary frameworks or dependencies. +- Do not implement backend functionality unless the task explicitly requires it. +- Do not expand Elo/MMR, historical workers or RCON server #03 handling unless the task explicitly requires it. +- Do not overwrite repository-specific context with generic platform template text. +- No ejecutar `ai-platform run`. +- No modificar `backend/`, endpoints, logica de datos, generacion de snapshots ni scripts backend. +- No tocar `ai/system-metrics.md`. +- No tocar `frontend/assets/img/weapons/`, assets/SVGs de armas ni copias relacionadas. +- No reactivar Elo/MMR ni reintroducir Comunidad Hispana #03. + +## Validation + +Before completing the task ensure: + +- `stats.html` no muestra selector editable de anio para el ranking anual +- `stats.html` muestra de forma simple la temporada fija 2026 +- el ranking anual se carga automaticamente al abrir la pagina +- ya no hace falta pulsar un boton para la carga inicial del ranking anual +- la tabla anual muestra `Kills` y `KPM` y ya no muestra `Valor / Kills` +- `KPM` usa `kills_per_match` si existe o `kills / matches_considered` con 2 decimales cuando haya datos suficientes +- `ranking.html` usa la etiqueta visual `KPM` sin duplicar columnas +- `historico.html` y `stats.html` solo muestran `KPM` adicional donde aplique por datos disponibles +- `git diff --name-only` matches the expected scope +- no unrelated files were modified +- integration tests are run when relevant and configured + +## Outcome + +Archivos modificados: + +- `frontend/stats.html` +- `frontend/ranking.html` +- `frontend/historico.html` +- `frontend/assets/js/stats.js` +- `frontend/assets/js/ranking.js` +- `frontend/assets/js/historico.js` +- `frontend/assets/js/historico-partida.js` +- `frontend/assets/js/main.js` +- `frontend/assets/css/styles.css` +- `frontend/assets/css/historico-scoreboard-detail.css` +- `ai/tasks/in-progress/TASK-207-annual-ranking-default-load-and-kpm-columns.md` + +Temporada anual fija: + +- Se elimino el selector editable de anio y cualquier control de carga manual del bloque anual en `stats`. +- La interfaz muestra la temporada fija con un bloque visual simple `Temporada 2026`. +- El titulo del bloque anual queda orientado a la lectura fija: `Top 20 temporada 2026`. + +Carga automatica del ranking anual: + +- `stats.js` deja de leer un input de anio y fija `annualDefaultYear = 2026`. +- Tras confirmar `health`, la pagina dispara automaticamente la carga del ranking anual usando `year=2026`, `metric=kills`, `limit=20` y `server_id=all`. +- Se mantienen mensajes de carga, vacio y error controlados; ya no hace falta pulsar `Cargar ranking anual`. + +KPM anadido y unificado: + +- `stats` anual ahora muestra columnas `Posicion`, `Jugador`, `Kills`, `Partidas`, `KPM`, `Muertes`, `Teamkills` y `K/D`. +- `ranking.html` cambia la etiqueta visual de `kills_per_match` a `KPM` tanto en el selector como en la tabla. +- `ranking.js` evita duplicar `KPM` cuando la metrica activa ya es `kills_per_match`, ocultando la columna secundaria en ese caso. +- `historico.html` y `historico.js` anaden una columna `KPM` al ranking multi-registro; si no hay datos suficientes se muestra `-`. + +Busqueda de jugador endurecida: + +- `stats` ahora exige un minimo de 4 caracteres utiles antes de permitir la busqueda. +- El boton `Buscar jugador` queda deshabilitado hasta cumplir el minimo y la validacion tambien se aplica al pulsar o enviar con Enter. +- Si el texto no alcanza el minimo, el frontend muestra `Introduce al menos 4 caracteres para buscar un jugador.` y no lanza la llamada a `/api/stats/players/search`. + +Logos de clanes ampliados: + +- Se aumento la presencia visual general de los logos de la seccion de clanes mediante layout y CSS, sin tocar imagenes. +- `BxB` recibe un ajuste especifico adicional para que deje de verse pequeno dentro de su tarjeta. +- El ajuste mantiene responsive y no reemplaza assets. + +Brands en botones externos: + +- El detalle de jugador dentro de `historico-partida` ahora usa los logos disponibles en `frontend/assets/img/brands/` para `Hellor`, `HLL Records` y `Helo`. +- `Steam` conserva boton solo con texto porque no existe logo brand disponible en esa carpeta. +- Los enlaces actuales no cambian; solo se enriquece su presentacion visual con logos decorativos cuando hay asset. + +Calculo de KPM: + +- Si el backend ya entrega `kills_per_match`, el frontend usa ese valor. +- Si no existe `kills_per_match` y hay `kills` mas `matches_considered`, el frontend calcula `kills / matches_considered`. +- Si faltan datos o `matches_considered <= 0`, el frontend muestra `-`. +- La comprobacion pedida `1222 / 32` da `38.19`. + +Validaciones realizadas: + +- `git status --short --untracked-files=all` ejecutado antes de modificar. +- Confirmacion previa de cambios no relacionados en `ai/system-metrics.md` y en assets/SVGs de armas; no se tocaron. +- Lectura previa de `AGENTS.md`, `ai/repo-context.md`, `ai/architecture-index.md`, `frontend/stats.html`, `frontend/ranking.html`, `frontend/historico.html`, `frontend/assets/js/stats.js`, `frontend/assets/js/ranking.js`, `frontend/assets/js/historico.js`, `frontend/assets/css/styles.css` y `TASK-206`. +- Lectura adicional para esta ampliacion de `frontend/historico-partida.html`, `frontend/assets/js/historico-partida.js`, `frontend/assets/js/main.js`, `frontend/assets/css/historico-scoreboard-detail.css`, rutas existentes en `frontend/assets/img/brands/` y rutas existentes de la seccion de clanes. +- Parseo sintactico local con `node --check frontend/assets/js/stats.js`, `node --check frontend/assets/js/historico.js`, `node --check frontend/assets/js/ranking.js` y `node --check frontend/assets/js/historico-partida.js`. +- Verificacion especifica por inspeccion del minimo de 4 caracteres, de la presencia de logos de clan ampliados y de la asignacion de brands a botones externos sin rutas rotas. +- Verificacion por inspeccion de presencia/eliminacion de textos y columnas clave con `rg`. +- Revision de `git diff --name-only` filtrado a los archivos esperados para confirmar alcance acotado. +- No existe una validacion frontend automatizada configurada para este alcance; la validacion queda documentada por inspeccion y parseo local. + +Confirmaciones de alcance: + +- No se tocaron `backend/`, endpoints, logica de datos, snapshots ni scripts backend. +- No se tocaron `ai/system-metrics.md`, `frontend/assets/img/weapons/` ni SVGs de armas. +- No se reactivo Elo/MMR ni se reintrodujo Comunidad Hispana #03. + +## Change Budget + +- Prefer fewer than 5 modified files. +- Prefer changes under 200 lines when feasible. +- Split the work into follow-up tasks if limits are exceeded. diff --git a/frontend/assets/css/historico-scoreboard-detail.css b/frontend/assets/css/historico-scoreboard-detail.css index 180074f..5ca6e85 100644 --- a/frontend/assets/css/historico-scoreboard-detail.css +++ b/frontend/assets/css/historico-scoreboard-detail.css @@ -267,6 +267,7 @@ display: inline-flex; min-height: 32px; align-items: center; + gap: 8px; padding: 0 11px; border: 1px solid rgba(210, 182, 118, 0.34); border-radius: 999px; @@ -278,6 +279,16 @@ text-transform: uppercase; } +.historical-player-profile-link__brand { + width: auto; + height: auto; + max-width: 20px; + max-height: 20px; + object-fit: contain; + flex: 0 0 auto; + filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.28)); +} + .historical-player-profile-links a:hover, .historical-player-profile-links a:focus-visible { border-color: rgba(210, 182, 118, 0.62); diff --git a/frontend/assets/css/styles.css b/frontend/assets/css/styles.css index bc127f4..fa76f3f 100644 --- a/frontend/assets/css/styles.css +++ b/frontend/assets/css/styles.css @@ -477,6 +477,14 @@ h2 { box-shadow: 0 18px 36px rgba(110, 127, 72, 0.45); } +.discord-button:disabled { + cursor: not-allowed; + opacity: 0.58; + filter: saturate(0.7); + box-shadow: none; + transform: none; +} + .content { width: 100%; margin-top: -28px; @@ -883,14 +891,14 @@ h2 { .clan-card__brand { display: grid; - grid-template-columns: minmax(92px, 112px) minmax(0, 1fr); + grid-template-columns: minmax(124px, 154px) minmax(0, 1fr); align-items: center; - gap: 18px; + gap: 20px; } .clan-card__logo { - min-height: 92px; - padding: 12px; + min-height: 124px; + padding: 14px; display: grid; place-items: center; border: 1px dashed rgba(183, 201, 125, 0.3); @@ -898,33 +906,45 @@ h2 { background: linear-gradient(180deg, rgba(19, 24, 16, 0.82), rgba(10, 13, 9, 0.66)); } +.clan-card__logo--standard { + min-height: 128px; +} + .clan-card__logo--wide { - padding-inline: 8px; + padding-inline: 10px; } .clan-card__logo--shield { - padding-inline: 18px; + padding-inline: 20px; } .clan-card--bxb .clan-card__brand { - grid-template-columns: minmax(112px, 138px) minmax(0, 1fr); + grid-template-columns: minmax(146px, 184px) minmax(0, 1fr); } .clan-card__logo--bxb { - min-height: 108px; - padding: 10px; + min-height: 148px; + padding: 12px; } .clan-card__logo img { width: auto; height: auto; max-width: 100%; - max-height: 84px; + max-height: 108px; object-fit: contain; } +.clan-card__logo--wide img { + max-height: 112px; +} + +.clan-card__logo--shield img { + max-height: 116px; +} + .clan-card__logo--bxb img { - max-height: 100px; + max-height: 132px; } .clan-card__logo-placeholder { @@ -1111,6 +1131,30 @@ h2 { gap: 16px; } +.stats-annual-season { + display: inline-flex; + align-items: baseline; + gap: 10px; + margin: 18px 0 14px; + padding: 0.8rem 1rem; + border: 1px solid rgba(183, 201, 125, 0.24); + border-radius: 14px; + background: linear-gradient(180deg, rgba(30, 36, 27, 0.78), rgba(12, 15, 11, 0.9)); +} + +.stats-annual-season__label { + color: var(--muted); + font-size: 0.74rem; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.stats-annual-season__value { + color: var(--accent-strong); + font-size: 1.12rem; + letter-spacing: 0.08em; +} + .stats-annual-meta { display: grid; gap: 12px; @@ -1497,6 +1541,11 @@ h2 { font-weight: 700; } +.ranking-table th[hidden], +.ranking-table td[hidden] { + display: none; +} + .ranking-player { display: grid; gap: 4px; diff --git a/frontend/assets/img/brands/helo-system.png b/frontend/assets/img/brands/helo-system.png new file mode 100644 index 0000000000000000000000000000000000000000..8788e73681129f0e6a9e4d765a39ecc485e6cc92 GIT binary patch literal 6624 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 literal 0 HcmV?d00001 diff --git a/frontend/assets/js/historico-partida.js b/frontend/assets/js/historico-partida.js index 25ad79e..b9389ac 100644 --- a/frontend/assets/js/historico-partida.js +++ b/frontend/assets/js/historico-partida.js @@ -40,6 +40,25 @@ document.addEventListener("DOMContentLoaded", () => { void loadMatchDetail({ backendBaseUrl, serverSlug, matchId, nodes }); }); +const EXTERNAL_PROFILE_BRANDS = Object.freeze({ + hellor: Object.freeze({ + label: "Hellor", + logoSrc: "./assets/img/brands/hllor.webp", + }), + hll_records: Object.freeze({ + label: "HLL Records", + logoSrc: "./assets/img/brands/hllrecords.png", + }), + helo: Object.freeze({ + label: "Helo", + logoSrc: "./assets/img/brands/helo-system.png", + }), + steam: Object.freeze({ + label: "Steam", + logoSrc: "", + }), +}); + async function loadMatchDetail({ backendBaseUrl, serverSlug, matchId, nodes }) { try { const payload = await fetchJson( @@ -513,13 +532,17 @@ function renderPlayerStatsPanel(player, item, context) { function renderExternalProfilesSection(player) { const links = [ - ["steam", "Steam"], - ["hellor", "Hellor"], - ["hll_records", "HLL Records"], - ["helo", "Helo"], + "steam", + "hellor", + "hll_records", + "helo", ] - .map(([key, label]) => [label, player.external_profile_links?.[key]]) - .filter(([, href]) => typeof href === "string" && href.trim()); + .map((key) => ({ + key, + brand: EXTERNAL_PROFILE_BRANDS[key] || { label: key, logoSrc: "" }, + href: player.external_profile_links?.[key], + })) + .filter((entry) => typeof entry.href === "string" && entry.href.trim()); return `
@@ -530,9 +553,14 @@ function renderExternalProfilesSection(player) { - ${metricValue} + ${kills} ${matches} + ${kpm} ${deaths} ${teamkills} ${kd} @@ -401,8 +386,9 @@ Posici\u00f3n Jugador - Valor / Kills + Kills Partidas + KPM Muertes Teamkills K/D @@ -891,6 +877,31 @@ return Number.isFinite(parsed) ? parsed : Number.NaN; } + function firstFiniteValue(...values) { + for (const value of values) { + const parsed = Number(value); + if (Number.isFinite(parsed)) { + return parsed; + } + } + return Number.NaN; + } + + function formatKpm(rawKillsPerMatch, rawKills, rawMatches) { + const directValue = safeParseNumber(rawKillsPerMatch); + if (Number.isFinite(directValue)) { + return safeDecimal(directValue, 2, "0.00"); + } + + const kills = safeParseNumber(rawKills); + const matches = safeParseNumber(rawMatches); + if (!Number.isFinite(kills) || !Number.isFinite(matches) || matches <= 0) { + return "-"; + } + + return safeDecimal(kills / matches, 2, "0.00"); + } + function safeParseJson(response) { if (!response) { return null; @@ -920,6 +931,35 @@ return Array.isArray(items) ? items : []; } + function isValidSearchQuery(query) { + return String(query || "").trim().length >= minimumSearchQueryLength; + } + + function syncSearchFormState() { + const query = String(searchInput?.value || "").trim(); + const isValid = isValidSearchQuery(query); + + if (searchSubmitButton) { + searchSubmitButton.disabled = !isValid; + } + + if (!searchStateNode) { + return; + } + + if (!query) { + setSearchState( + "neutral", + "Estado inicial: introduce al menos 4 caracteres para buscar un jugador.", + ); + return; + } + + if (!isValid) { + setSearchState("warning", messages.searchShortQueryHelp); + } + } + function escapeHtml(value) { return String(value) .replaceAll("&", "&") diff --git a/frontend/historico.html b/frontend/historico.html index 079051b..cd44855 100644 --- a/frontend/historico.html +++ b/frontend/historico.html @@ -199,6 +199,7 @@ Jugador Kills Partidas + KPM diff --git a/frontend/ranking.html b/frontend/ranking.html index ff9658d..0b26989 100644 --- a/frontend/ranking.html +++ b/frontend/ranking.html @@ -90,7 +90,7 @@ - +