diff --git a/ai/tasks/pending/TASK-127-frontend-recent-match-link-actions.md b/ai/tasks/done/TASK-127-frontend-recent-match-link-actions.md similarity index 71% rename from ai/tasks/pending/TASK-127-frontend-recent-match-link-actions.md rename to ai/tasks/done/TASK-127-frontend-recent-match-link-actions.md index 6e9cba4..f759ae0 100644 --- a/ai/tasks/pending/TASK-127-frontend-recent-match-link-actions.md +++ b/ai/tasks/done/TASK-127-frontend-recent-match-link-actions.md @@ -1,7 +1,7 @@ --- id: TASK-127 title: Update recent match link actions -status: pending +status: done type: frontend team: Frontend Senior supporting_teams: @@ -78,3 +78,19 @@ Recent match cards should always offer internal details when supported and only - Stage only intended files. - Commit the completed implementation. - Push the branch to origin. + +## Outcome + +Updated recent match cards to consume materialized RCON recent-match fields. Cards now show available scores, a friendly result status/source, an internal `Ver detalles` link built from `internal_detail_match_id` or `match_id`, and an external scoreboard link only when `match_url` is present. + +The rendering keeps paused Elo/MVP blocks out of the page, does not reintroduce Comunidad Hispana #03, and avoids visible "snapshot" wording. Missing-score RCON competitive-window rows render as in-progress instead of placeholder dashes. + +## Validation Result + +- Passed: `node --check frontend/assets/js/historico.js` +- Passed: `node --check frontend/assets/js/historico-recent-live.js` +- Passed: `docker compose up -d --build backend frontend` +- Browser-verified `http://localhost:8080/historico.html`. +- Confirmed recent cards show internal detail links and materialized RCON score `3 - 2`. +- Confirmed external scoreboard links are not rendered when `match_url` is absent. +- Confirmed no visible Elo/MVP/Comunidad Hispana #03/snapshot wording appears. diff --git a/ai/tasks/pending/TASK-128-build-simplified-internal-match-detail-page.md b/ai/tasks/done/TASK-128-build-simplified-internal-match-detail-page.md similarity index 68% rename from ai/tasks/pending/TASK-128-build-simplified-internal-match-detail-page.md rename to ai/tasks/done/TASK-128-build-simplified-internal-match-detail-page.md index 6b58c0a..cb92b3b 100644 --- a/ai/tasks/pending/TASK-128-build-simplified-internal-match-detail-page.md +++ b/ai/tasks/done/TASK-128-build-simplified-internal-match-detail-page.md @@ -1,7 +1,7 @@ --- id: TASK-128 title: Build simplified internal match detail page -status: pending +status: done type: frontend team: Frontend Senior supporting_teams: @@ -77,3 +77,20 @@ When a safe public scoreboard link is unavailable, users should still be able to - Stage only intended files. - Commit the completed implementation. - Push the branch to origin. + +## Outcome + +Reworked `frontend/historico-partida.html` and `frontend/assets/js/historico-partida.js` into a simplified internal scoreboard-style detail page backed by `/api/historical/matches/detail?server=...&match=...`. + +The page correctly URL-encodes materialized RCON match ids containing colons, displays match summary cards, source/confidence, optional external scoreboard action, player stats with K/D, top weapons, most-killed/death-by summaries, and timeline event counts. It handles missing player/timeline data with controlled empty states and does not expose raw player IDs. + +## Validation Result + +- Passed: `node --check frontend/assets/js/historico-partida.js` +- Passed: `docker compose up -d --build backend frontend` +- Browser-verified detail navigation from `http://localhost:8080/historico.html`. +- Browser-verified known materialized match detail renders at `http://localhost:8080/historico-partida.html?server=comunidad-hispana-02&match=comunidad-hispana-02%3A1779178461%3A1779183861%3Acarentanwarfare`. +- Confirmed AntonioPruna renders with 1 kill, 0 deaths and `M1 GARAND`. +- Confirmed the victim row renders 1 death and `death_by` AntonioPruna. +- Confirmed timeline/event counts render. +- Confirmed no visible Elo/MVP/Comunidad Hispana #03/snapshot wording appears. diff --git a/frontend/assets/css/historico.css b/frontend/assets/css/historico.css index f54853e..4c2c3e7 100644 --- a/frontend/assets/css/historico.css +++ b/frontend/assets/css/historico.css @@ -490,6 +490,10 @@ border-bottom: 0; } +.historical-table--players { + min-width: 920px; +} + .historical-table__position { color: var(--accent-warm); font-weight: 700; diff --git a/frontend/assets/js/historico-partida.js b/frontend/assets/js/historico-partida.js index b33754b..0c71b48 100644 --- a/frontend/assets/js/historico-partida.js +++ b/frontend/assets/js/historico-partida.js @@ -4,60 +4,36 @@ document.addEventListener("DOMContentLoaded", () => { const params = new URLSearchParams(window.location.search); const serverSlug = params.get("server") || ""; const matchId = params.get("match") || ""; - const titleNode = document.getElementById("match-detail-title"); - const summaryNode = document.getElementById("match-detail-summary"); - const noteNode = document.getElementById("match-detail-note"); - const stateNode = document.getElementById("match-detail-state"); - const gridNode = document.getElementById("match-detail-grid"); - const actionsNode = document.getElementById("match-detail-actions"); - const playersSectionNode = document.getElementById("match-detail-players-section"); - const playersNoteNode = document.getElementById("match-detail-players-note"); - const playersStateNode = document.getElementById("match-detail-players-state"); - const playersTableShellNode = document.getElementById("match-detail-players-table-shell"); - const playersBodyNode = document.getElementById("match-detail-players-body"); + const nodes = { + title: document.getElementById("match-detail-title"), + summary: document.getElementById("match-detail-summary"), + note: document.getElementById("match-detail-note"), + state: document.getElementById("match-detail-state"), + grid: document.getElementById("match-detail-grid"), + actions: document.getElementById("match-detail-actions"), + playersSection: document.getElementById("match-detail-players-section"), + playersNote: document.getElementById("match-detail-players-note"), + playersState: document.getElementById("match-detail-players-state"), + playersTableShell: document.getElementById("match-detail-players-table-shell"), + playersBody: document.getElementById("match-detail-players-body"), + timelineSection: document.getElementById("match-detail-timeline-section"), + timelineNote: document.getElementById("match-detail-timeline-note"), + timelineState: document.getElementById("match-detail-timeline-state"), + timelineGrid: document.getElementById("match-detail-timeline-grid"), + }; if (!serverSlug || !matchId) { - titleNode.textContent = "Partida no seleccionada"; - summaryNode.textContent = "Vuelve al historico y abre una partida registrada."; - noteNode.textContent = "Faltan parametros internos para cargar este detalle."; - setState(stateNode, "No hay una partida seleccionada.", true); + nodes.title.textContent = "Partida no seleccionada"; + nodes.summary.textContent = "Vuelve al historico y abre una partida registrada."; + nodes.note.textContent = "Faltan parametros internos para cargar este detalle."; + setState(nodes.state, "No hay una partida seleccionada.", true); return; } - void loadMatchDetail({ - backendBaseUrl, - serverSlug, - matchId, - titleNode, - summaryNode, - noteNode, - stateNode, - gridNode, - actionsNode, - playersSectionNode, - playersNoteNode, - playersStateNode, - playersTableShellNode, - playersBodyNode, - }); + void loadMatchDetail({ backendBaseUrl, serverSlug, matchId, nodes }); }); -async function loadMatchDetail({ - backendBaseUrl, - serverSlug, - matchId, - titleNode, - summaryNode, - noteNode, - stateNode, - gridNode, - actionsNode, - playersSectionNode, - playersNoteNode, - playersStateNode, - playersTableShellNode, - playersBodyNode, -}) { +async function loadMatchDetail({ backendBaseUrl, serverSlug, matchId, nodes }) { try { const payload = await fetchJson( `${backendBaseUrl}/api/historical/matches/detail?server=${encodeURIComponent( @@ -67,131 +43,122 @@ async function loadMatchDetail({ const data = payload?.data; const item = data?.item; if (!data?.found || !item) { - titleNode.textContent = "Detalle no disponible"; - summaryNode.textContent = + nodes.title.textContent = "Detalle no disponible"; + nodes.summary.textContent = "La partida existe como enlace interno, pero todavia no hay detalle suficiente para mostrar."; - noteNode.textContent = + nodes.note.textContent = "El historico local puede tener solo una ventana RCON parcial o ningun registro ampliado."; - setState(stateNode, "Detalle no disponible para esta partida."); + setState(nodes.state, "Detalle no disponible para esta partida."); return; } - renderMatchDetail(item, { - titleNode, - summaryNode, - noteNode, - stateNode, - gridNode, - actionsNode, - playersSectionNode, - playersNoteNode, - playersStateNode, - playersTableShellNode, - playersBodyNode, - }); + renderMatchDetail(item, nodes); } catch (error) { - titleNode.textContent = "Detalle no disponible"; - summaryNode.textContent = "No se pudo conectar con el backend local."; - noteNode.textContent = "Comprueba que el backend este levantado y vuelve a intentarlo."; - setState(stateNode, "Error al cargar el detalle de la partida.", true); + nodes.title.textContent = "Detalle no disponible"; + nodes.summary.textContent = "No se pudo conectar con el backend local."; + nodes.note.textContent = "Comprueba que el backend este levantado y vuelve a intentarlo."; + setState(nodes.state, "Error al cargar el detalle de la partida.", true); } } -function renderMatchDetail( - item, - { - titleNode, - summaryNode, - noteNode, - stateNode, - gridNode, - actionsNode, - playersSectionNode, - playersNoteNode, - playersStateNode, - playersTableShellNode, - playersBodyNode, - }, -) { +function renderMatchDetail(item, nodes) { const mapName = item.map?.pretty_name || item.map?.name || "Mapa no disponible"; - const serverName = item.server?.name || "Servidor no disponible"; - titleNode.textContent = mapName; - summaryNode.textContent = `${serverName} | Partida ${item.match_id || "sin id"}`; - noteNode.textContent = buildDetailNote(item); - gridNode.innerHTML = [ + const serverName = item.server?.name || item.server?.slug || "Servidor no disponible"; + nodes.title.textContent = mapName; + nodes.summary.textContent = `${serverName} | ${item.match_id || "partida sin id"}`; + nodes.note.textContent = buildDetailNote(item); + nodes.grid.innerHTML = [ renderDetailCard("Servidor", serverName), renderDetailCard("Mapa", mapName), + renderDetailCard("Modo", formatGameMode(item.game_mode || item.gamestate?.game_mode)), + renderDetailCard("Marcador", formatScore(item.result)), + renderDetailCard("Ganador", formatWinner(item.winner || item.result?.winner)), + renderDetailCard("Resultado", formatMatchResult(item.result)), renderDetailCard("Inicio", formatTimestamp(item.started_at)), renderDetailCard("Fin", formatTimestamp(item.closed_at || item.ended_at)), renderDetailCard("Duracion", formatDuration(item.duration_seconds)), - renderDetailCard("Jugadores media", formatNumber(item.player_count)), - renderDetailCard("Pico jugadores", formatOptionalNumber(item.peak_players)), - renderDetailCard("Muestras RCON", formatOptionalNumber(item.sample_count)), - renderDetailCard("Marcador", formatScore(item.result)), - renderDetailCard("Resultado", formatMatchResult(item.result)), - renderDetailCard("Base de captura", formatCaptureBasis(item.capture_basis)), - renderDetailCard("Capacidades", formatCapabilities(item.capabilities)), + renderDetailCard("Confianza", formatConfidence(item.confidence)), + renderDetailCard( + "Fuente", + formatSourceBasis(item.source_basis || item.result_source) || "No disponible", + ), + renderDetailCard("Base", formatCaptureBasis(item.capture_basis)), ].join(""); - renderPlayerSection(item, { - playersSectionNode, - playersNoteNode, - playersStateNode, - playersTableShellNode, - playersBodyNode, - }); - renderActions(item, actionsNode); - stateNode.hidden = true; - gridNode.hidden = false; + renderPlayerSection(item, nodes); + renderTimelineSection(item, nodes); + renderActions(item, nodes.actions); + nodes.state.hidden = true; + nodes.grid.hidden = false; } -function renderPlayerSection( - item, - { - playersSectionNode, - playersNoteNode, - playersStateNode, - playersTableShellNode, - playersBodyNode, - }, -) { +function renderPlayerSection(item, nodes) { const players = Array.isArray(item.players) ? item.players : []; - playersSectionNode.hidden = false; + nodes.playersSection.hidden = false; if (players.length === 0) { - playersNoteNode.textContent = + nodes.playersNote.textContent = "Esta partida no tiene estadisticas por jugador disponibles en el detalle interno."; setState( - playersStateNode, - item.capture_basis === "rcon-competitive-window" - ? "Las ventanas RCON actuales no incluyen desglose por jugador." - : "No hay filas de jugador registradas para esta partida.", + nodes.playersState, + "No hay filas de jugador registradas para esta partida.", ); - playersTableShellNode.hidden = true; - playersBodyNode.innerHTML = ""; + nodes.playersTableShell.hidden = true; + nodes.playersBody.innerHTML = ""; return; } - playersNoteNode.textContent = `${formatNumber(players.length)} jugadores con estadisticas locales.`; - playersStateNode.hidden = true; - playersBodyNode.innerHTML = players.map((player) => renderPlayerRow(player)).join(""); - playersTableShellNode.hidden = false; + nodes.playersNote.textContent = `${formatNumber(players.length)} jugadores con estadisticas locales.`; + nodes.playersState.hidden = true; + nodes.playersBody.innerHTML = players.map((player) => renderPlayerRow(player)).join(""); + nodes.playersTableShell.hidden = false; } function renderPlayerRow(player) { return `
Eventos
++ Revisando eventos registrados para esta partida. +
+