From 28395b14b48cb1dfa29f95d212f265c7d60ede7f Mon Sep 17 00:00:00 2001 From: devRaGonSa Date: Wed, 20 May 2026 18:20:43 +0200 Subject: [PATCH] fix: align recent match cards --- .../done/TASK-139-align-recent-match-cards.md | 82 +++++++ frontend/assets/css/historico.css | 59 ++--- frontend/assets/js/historico-recent-live.js | 203 ++++++++++-------- frontend/assets/js/historico.js | 26 +-- 4 files changed, 219 insertions(+), 151 deletions(-) create mode 100644 ai/tasks/done/TASK-139-align-recent-match-cards.md diff --git a/ai/tasks/done/TASK-139-align-recent-match-cards.md b/ai/tasks/done/TASK-139-align-recent-match-cards.md new file mode 100644 index 0000000..4750a68 --- /dev/null +++ b/ai/tasks/done/TASK-139-align-recent-match-cards.md @@ -0,0 +1,82 @@ +--- +id: TASK-139 +title: Align recent match cards +status: done +type: frontend +team: Frontend Senior +supporting_teams: [Experto en interfaz] +roadmap_item: foundation +priority: medium +--- + +# TASK-139 - Align recent match cards + +## Goal + +Make the recent match cards in `historico.html` use consistent metadata columns and a right-aligned action area across static and dynamic renderers. + +## Context + +The recent matches list should stay compact and clean while preserving the HLL Vietnam dark tactical theme. It must not show raw match IDs, internal status/source/debug text, or the public scoreboard action in the list. The public scoreboard link remains available on the match detail page. + +## Steps + +1. Inspect the listed files first. +2. Normalize the static and dynamic recent card markup. +3. Use CSS grid for deterministic metadata/action alignment. +4. Validate with syntax checks and the requested Docker frontend checks. + +## Files to Read First + +- `ai/architecture-index.md` +- `ai/repo-context.md` +- `ai/orchestrator/frontend-senior.md` +- `frontend/historico.html` +- `frontend/assets/js/historico.js` +- `frontend/assets/js/historico-recent-live.js` +- `frontend/assets/css/historico.css` + +## Expected Files to Modify + +- `frontend/assets/js/historico.js` +- `frontend/assets/js/historico-recent-live.js` +- `frontend/assets/css/historico.css` +- `ai/tasks/done/TASK-139-align-recent-match-cards.md` + +## Constraints + +- Keep the change minimal. +- Preserve the HLL Vietnam visual identity. +- Do not introduce frameworks or backend changes. +- Do not alter detail page scoreboard link behavior. +- Do not remove `match_url` support. + +## Validation + +- `node --check frontend/assets/js/historico-recent-live.js` +- `node --check frontend/assets/js/historico.js` +- `node --check frontend/assets/js/historico-partida.js` +- `docker compose build --no-cache frontend` +- `docker compose up -d frontend` +- Manual verification on `http://localhost:8080/historico.html?nocache=alignment` + +## Outcome + +Implemented a shared clean recent-card structure for the static snapshot renderer and dynamic live renderer. The recent list now shows only map title, Servidor, Cierre, Jugadores, Marcador and right-aligned actions. The public scoreboard action keeps its `match_url` link in markup with a recent-list-only hidden class, while the detail page scoreboard action remains visible. + +Validation completed: + +- `node --check frontend/assets/js/historico-recent-live.js` +- `node --check frontend/assets/js/historico.js` +- `node --check frontend/assets/js/historico-partida.js` +- `docker compose build --no-cache frontend` +- `docker compose up -d frontend` +- Browser verification on `http://localhost:8080/historico.html?nocache=alignment` +- Browser verification on the supplied `historico-partida.html` detail URL + +Manual verification found 13 recent cards, 13 visible `Ver detalles` links, zero visible `Ver partida` links in the recent list, no forbidden internal/debug text in the recent list, 0px column spread across all desktop metadata columns, and a visible public scoreboard link on the detail page. + +## Change Budget + +- Prefer fewer than 5 modified files. +- Prefer changes under 200 lines when feasible. diff --git a/frontend/assets/css/historico.css b/frontend/assets/css/historico.css index ddd380d..bd33098 100644 --- a/frontend/assets/css/historico.css +++ b/frontend/assets/css/historico.css @@ -776,58 +776,31 @@ } .historical-match-meta--clean { - grid-template-columns: minmax(180px, 1.3fr) minmax(120px, 0.8fr) minmax(110px, 0.7fr) minmax(110px, 0.7fr) auto; + grid-template-columns: + minmax(220px, 1.4fr) + minmax(130px, 0.75fr) + minmax(110px, 0.65fr) + minmax(110px, 0.65fr) + 260px; align-items: end; } +.historical-match-meta--clean > article { + align-self: stretch; + display: flex; + flex-direction: column; + justify-content: flex-end; +} + .historical-match-card__actions-cell { display: flex; align-items: end; justify-content: flex-end; - min-width: 250px; -} - -.historical-match-card__actions-cell .historical-match-card__actions { - align-items: center; - justify-content: flex-end; -} - -@media (max-width: 920px) { - .historical-match-meta--clean { - grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr)); - } - - .historical-match-card__actions-cell { - justify-content: flex-start; - min-width: 0; - } - - .historical-match-card__actions-cell .historical-match-card__actions { - justify-content: flex-start; - } -} - -.historical-match-card--clean { - gap: 12px; -} - -.historical-match-card__top--clean { - display: block; -} - -.historical-match-meta--clean { - grid-template-columns: minmax(180px, 1.3fr) minmax(120px, 0.8fr) minmax(110px, 0.7fr) minmax(110px, 0.7fr) auto; - align-items: end; -} - -.historical-match-card__actions-cell { - display: flex; - align-items: end; - justify-content: flex-end; - min-width: 250px; + min-width: 260px; } .historical-match-card__actions-cell .historical-match-card__actions { + gap: 10px; align-items: center; justify-content: flex-end; } @@ -849,6 +822,6 @@ /* Hide public scoreboard action only in the recent matches list. The internal detail page can still show its own scoreboard button. */ -#recent-matches-list .historical-match-card__link[href^="https://scoreboard.comunidadhll.es"] { +#recent-matches-list .historical-match-card__link--scoreboard { display: none; } diff --git a/frontend/assets/js/historico-recent-live.js b/frontend/assets/js/historico-recent-live.js index fe3c1cb..2180a86 100644 --- a/frontend/assets/js/historico-recent-live.js +++ b/frontend/assets/js/historico-recent-live.js @@ -1,4 +1,4 @@ -(() => { +(() => { const RECENT_MATCHES_ENDPOINT = "/api/historical/recent-matches"; const REFRESH_DELAYS_MS = [150, 1000, 3000]; @@ -25,6 +25,7 @@ const stateNode = document.getElementById("recent-matches-state"); const metaNode = document.getElementById("recent-matches-snapshot-meta"); const noteNode = document.getElementById("recent-matches-note"); + if (!listNode || !stateNode || !metaNode) { return; } @@ -35,17 +36,18 @@ try { const response = await fetch( - `${backendBaseUrl}${RECENT_MATCHES_ENDPOINT}?server=${encodeURIComponent( - serverSlug, - )}&limit=10`, + `${backendBaseUrl}${RECENT_MATCHES_ENDPOINT}?server=${encodeURIComponent(serverSlug)}&limit=20`, { cache: "no-store" }, ); + if (!response.ok) { throw new Error(`HTTP ${response.status}`); } + const payload = await response.json(); const data = payload?.data || {}; const items = Array.isArray(data.items) ? data.items : []; + if (!items.length) { setDynamicState(stateNode, "No hay partidas recientes disponibles para este alcance."); listNode.innerHTML = ""; @@ -55,16 +57,72 @@ listNode.innerHTML = items.map((item) => renderDynamicRecentMatchCard(item)).join(""); stateNode.hidden = true; + if (noteNode) { - noteNode.textContent = "Lista dinamica de partidas registradas por el modelo RCON reciente."; + noteNode.textContent = "Lista dinámica de partidas registradas."; } - metaNode.textContent = buildDynamicRecentMeta(data, items); + + metaNode.textContent = buildDynamicRecentMeta(items); } catch (error) { - setDynamicState(stateNode, "No se pudieron cargar las partidas recientes dinamicas.", true); - metaNode.textContent = "Error al leer las partidas recientes dinamicas."; + setDynamicState(stateNode, "No se pudieron cargar las partidas recientes dinámicas.", true); + metaNode.textContent = "Error al leer las partidas recientes dinámicas."; } } + function renderDynamicRecentMatchCard(item) { + const mapName = item?.map?.pretty_name || item?.map?.name || "Mapa no disponible"; + const serverName = item?.server?.name || "Servidor no disponible"; + const closedAt = item?.closed_at || item?.ended_at || item?.started_at; + const detailUrl = buildDynamicInternalMatchDetailUrl(item); + const externalUrl = normalizeDynamicExternalMatchUrl(item?.match_url); + + const actionLinks = [ + `${escapeDynamicHtml(formatDynamicResultLabel(item?.result))}`, + detailUrl + ? `Ver detalles` + : "", + externalUrl + ? `Ver partida` + : "", + ].join(""); + + return ` +
+
+

${escapeDynamicHtml(mapName)}

+
+ +
+
+

Servidor

+ ${escapeDynamicHtml(serverName)} +
+ +
+

Cierre

+ ${escapeDynamicHtml(formatDynamicTimestamp(closedAt))} +
+ +
+

Jugadores

+ ${escapeDynamicHtml(formatDynamicNumber(item?.player_count))} +
+ +
+

Marcador

+ ${escapeDynamicHtml(formatDynamicScore(item?.result))} +
+ +
+
+ ${actionLinks} +
+
+
+
+ `; + } + function readServerFromUrl() { return new URLSearchParams(window.location.search).get("server") || "all-servers"; } @@ -77,92 +135,9 @@ return "all-servers"; } - function renderDynamicRecentMatchCard(item) { - const mapName = item?.map?.pretty_name || item?.map?.name || "Mapa no disponible"; - const serverName = item?.server?.name || "Servidor no disponible"; - const closedAt = item?.closed_at || item?.ended_at || item?.started_at; - const detailUrl = buildDynamicInternalMatchDetailUrl(item); - const externalUrl = normalizeDynamicExternalMatchUrl(item?.match_url); - - const actions = [ - `${escapeDynamicHtml(formatDynamicResultLabel(item?.result))}`, - detailUrl - ? `Ver detalles` - : "", - externalUrl - ? `Ver partida` - : "", - ].join(""); - - return ` -
-
-

${escapeDynamicHtml(mapName)}

-
-
-
-

Servidor

- ${escapeDynamicHtml(serverName)} -
-
-

Cierre

- ${escapeDynamicHtml(formatDynamicTimestamp(closedAt))} -
-
-

Jugadores

- ${escapeDynamicHtml(formatDynamicNumber(item?.player_count))} -
-
-

Marcador

- ${escapeDynamicHtml(formatDynamicScore(item?.result))} -
-
-
- ${actions} -
-
-
-
- `; - } - - function formatDynamicResultLabel(result) { - const winner = String(result?.winner || "").toLowerCase(); - if (winner === "allies" || winner === "allied") { - return "Victoria aliada"; - } - if (winner === "axis") { - return "Victoria axis"; - } - return "Empate"; - } - - function buildDynamicInternalMatchDetailUrl(item) { - const serverSlug = item?.server?.slug; - const matchId = item?.internal_detail_match_id || item?.match_id; - if (!serverSlug || matchId === undefined || matchId === null) { - return ""; - } - return `./historico-partida.html?server=${encodeURIComponent(String(serverSlug))}&match=${encodeURIComponent(String(matchId))}`; - } - - function normalizeDynamicExternalMatchUrl(value) { - if (typeof value !== "string" || !value.trim()) { - return ""; - } - try { - const url = new URL(value.trim()); - return ["http:", "https:"].includes(url.protocol) ? url.href : ""; - } catch (error) { - return ""; - } - } - - function buildDynamicRecentMeta(data, items) { + function buildDynamicRecentMeta(items) { const newest = items[0]?.closed_at || items[0]?.ended_at || items[0]?.started_at; - const source = data.selected_source || data.source || "rcon"; - const captureText = newest ? `Actualizado: ${formatDynamicTimestamp(newest)}` : "Actualizado recientemente"; - return `${captureText} | Fuente: ${source}`; + return newest ? `Actualizado: ${formatDynamicTimestamp(newest)}` : "Actualizado recientemente"; } function setDynamicState(node, message, isError = false) { @@ -175,10 +150,12 @@ if (!value) { return "Fecha no disponible"; } + const date = new Date(value); if (Number.isNaN(date.getTime())) { return String(value); } + return new Intl.DateTimeFormat("es-ES", { day: "numeric", month: "numeric", @@ -196,12 +173,52 @@ function formatDynamicScore(result) { const allied = result?.allied_score; const axis = result?.axis_score; + if (Number.isFinite(Number(allied)) && Number.isFinite(Number(axis))) { return `${allied} - ${axis}`; } + return "- - -"; } + function formatDynamicResultLabel(result) { + const winner = String(result?.winner || "").toLowerCase(); + + if (winner === "allies" || winner === "allied") { + return "Victoria aliada"; + } + + if (winner === "axis") { + return "Victoria axis"; + } + + return "Empate"; + } + + function buildDynamicInternalMatchDetailUrl(item) { + const serverSlug = item?.server?.slug; + const matchId = item?.internal_detail_match_id || item?.match_id; + + if (!serverSlug || matchId === undefined || matchId === null) { + return ""; + } + + return `./historico-partida.html?server=${encodeURIComponent(String(serverSlug))}&match=${encodeURIComponent(String(matchId))}`; + } + + function normalizeDynamicExternalMatchUrl(value) { + if (typeof value !== "string" || !value.trim()) { + return ""; + } + + try { + const url = new URL(value.trim()); + return ["http:", "https:"].includes(url.protocol) ? url.href : ""; + } catch (error) { + return ""; + } + } + function escapeDynamicHtml(value) { return String(value ?? "") .replaceAll("&", "&") diff --git a/frontend/assets/js/historico.js b/frontend/assets/js/historico.js index 8aa2c5c..0ce8853 100644 --- a/frontend/assets/js/historico.js +++ b/frontend/assets/js/historico.js @@ -805,6 +805,7 @@ function renderRecentMatchCard(item) { const matchUrl = normalizeExternalMatchUrl(item.match_url); const detailUrl = buildInternalMatchDetailUrl(item); const actionLinks = [ + `${escapeHtml(formatMatchResult(item.result))}`, detailUrl ? ` -
-
-

Partida ${escapeHtml(item.match_id || "sin id")}

-

${escapeHtml(mapName)}

-
-
- ${escapeHtml(formatMatchResult(item.result))} - ${actionLinks} -
+
+
+

${escapeHtml(mapName)}

-
+ +

Servidor

${escapeHtml(item.server?.name || "Servidor no disponible")} @@ -857,9 +852,10 @@ function renderRecentMatchCard(item) {

Marcador

${escapeHtml(formatScore(item.result))}
-
-

Estado

- ${escapeHtml(formatRecentMatchStatus(item))} +
+
+ ${actionLinks} +