chore: polish historical copy and match links
Polish public historical page copy and add safe links for recent matches when source URLs are available.
This commit is contained in:
209
ai/tasks/done/TASK-112-polish-historical-copy-and-match-links.md
Normal file
209
ai/tasks/done/TASK-112-polish-historical-copy-and-match-links.md
Normal file
@@ -0,0 +1,209 @@
|
||||
---
|
||||
id: TASK-112
|
||||
title: Polish historical copy and add recent match links
|
||||
status: pending
|
||||
type: frontend
|
||||
team: Frontend Senior
|
||||
supporting_teams: [Backend Senior, PM]
|
||||
roadmap_item: historical-ui
|
||||
priority: high
|
||||
---
|
||||
|
||||
# TASK-112 - Polish historical copy and add recent match links
|
||||
|
||||
## Goal
|
||||
|
||||
Polish the public historical page copy so implementation words stay hidden from users, and add source links to recent match cards when a safe persisted match URL is available.
|
||||
|
||||
## Context
|
||||
|
||||
HLL Vietnam has hidden the paused MVP/Elo UI from the public historical page. Manual visual review found three remaining issues:
|
||||
|
||||
- The ranking note includes this awkward fallback sentence: "Se muestra el ultimo periodo cerrado porque el actual todavia solo suma 0 cierres."
|
||||
- Public UI copy still exposes the technical word "snapshot".
|
||||
- Recent match cards show match IDs but do not link to the original match detail page.
|
||||
|
||||
Current product decisions remain unchanged:
|
||||
|
||||
- Keep historical ingestion active.
|
||||
- Keep historical policy RCON-first, with public-scoreboard fallback only where RCON fails, lacks coverage or lacks parity for a specific historical operation.
|
||||
- Keep Elo/MMR and advanced MVP UI paused.
|
||||
- Hide technical implementation wording from public users.
|
||||
- Add match links only when a persisted or safely derivable source URL is available.
|
||||
|
||||
Use branch:
|
||||
|
||||
- `chore/polish-historical-copy-and-match-links`
|
||||
|
||||
## Steps
|
||||
|
||||
1. Work on branch `chore/polish-historical-copy-and-match-links`.
|
||||
2. Inspect the listed files before changing anything.
|
||||
3. Remove the public sentence:
|
||||
"Se muestra el ultimo periodo cerrado porque el actual todavia solo suma 0 cierres."
|
||||
from the ranking note/copy.
|
||||
4. Keep the ranking note useful if needed, but do not expose the awkward fallback explanation.
|
||||
5. Remove the word "snapshot" from public UI copy.
|
||||
6. Keep internal function names unchanged if renaming them would increase risk.
|
||||
7. Use Spanish, product-friendly public alternatives where needed, such as:
|
||||
- "datos precalculados"
|
||||
- "datos actualizados"
|
||||
- "registro"
|
||||
- "ultima actualizacion"
|
||||
- "datos disponibles"
|
||||
8. Avoid "snapshot" in visible text, loading states, errors and metadata.
|
||||
9. Add match links for the recent matches section ("Ultimas partidas registradas") when a safe URL exists:
|
||||
- Backend recent matches payload should expose a safe URL field such as `match_url` or `source_url`.
|
||||
- Prefer using the persisted `raw_payload_ref` from `historical_matches` if available.
|
||||
- If `raw_payload_ref` is unavailable, derive the URL only if the server source/base URL and external match id are available and already trusted by existing historical server configuration.
|
||||
- Do not expose credentials or internal filesystem paths.
|
||||
- Frontend recent match cards should render a visible link/button such as "Ver partida" when the URL exists.
|
||||
- The link must open in a new tab with `target="_blank"` and `rel="noopener noreferrer"`.
|
||||
- If no URL exists, keep the current card layout without a broken link.
|
||||
10. Preserve existing normal historical UI:
|
||||
- summary
|
||||
- basic historical rankings
|
||||
- recent matches
|
||||
- server selector with only Todos, Comunidad Hispana #01, Comunidad Hispana #02
|
||||
11. Keep these paused/hidden:
|
||||
- MVP mensual V1
|
||||
- MVP mensual V2
|
||||
- Comparativa V1 vs V2
|
||||
- Elo/MMR mensual
|
||||
12. Do not change the historical ingestion policy or reintroduce Comunidad Hispana #03.
|
||||
13. Update `docs/decisions.md` only if the implementation changes a documented contract or public historical UI assumption.
|
||||
14. Validate the result.
|
||||
15. Move this task file to `ai/tasks/done/` after validation is complete and the outcome is documented.
|
||||
16. Commit and push the completed work to origin. Do not leave completed work only in local.
|
||||
|
||||
## Files to Read First
|
||||
|
||||
- `frontend/historico.html`
|
||||
- `frontend/assets/js/historico.js`
|
||||
- `frontend/assets/css/historico.css`
|
||||
- `backend/app/historical_storage.py`
|
||||
- `backend/app/historical_snapshots.py`
|
||||
- `backend/app/payloads.py`
|
||||
- `backend/app/routes.py`
|
||||
- `backend/tests/` if present
|
||||
- `docs/decisions.md`
|
||||
- `ai/repo-context.md`
|
||||
- `ai/architecture-index.md`
|
||||
|
||||
Rules:
|
||||
|
||||
- Read these files before implementation.
|
||||
- Keep the implementation scoped to public historical page copy, recent match payload shape and recent match card rendering.
|
||||
- Do not change backend historical ingestion policy.
|
||||
|
||||
## Expected Files to Modify
|
||||
|
||||
- `frontend/assets/js/historico.js`
|
||||
- possibly `frontend/assets/css/historico.css`
|
||||
- possibly `backend/app/historical_storage.py`
|
||||
- possibly `backend/app/historical_snapshots.py`
|
||||
- possibly `backend/app/payloads.py`
|
||||
- possibly backend tests if present
|
||||
- possibly `docs/decisions.md`
|
||||
- `ai/tasks/done/TASK-112-polish-historical-copy-and-match-links.md`
|
||||
|
||||
Rules:
|
||||
|
||||
- Prefer modifying only these files.
|
||||
- If additional files become necessary, explain why in the task outcome and commit message.
|
||||
- The task file should be moved from `ai/tasks/pending/` to `ai/tasks/done/` only after validation is complete.
|
||||
|
||||
## Expected Files Not to Modify
|
||||
|
||||
- `frontend/index.html`
|
||||
- Docker/Compose configuration
|
||||
- local `.env`
|
||||
- database migrations
|
||||
- persisted data
|
||||
- Elo/MMR backend implementation files
|
||||
- historical ingestion policy/config
|
||||
- unrelated backend modules
|
||||
- unrelated frontend pages
|
||||
|
||||
## Constraints
|
||||
|
||||
- Keep the change minimal and verifiable.
|
||||
- Preserve HLL Vietnam project identity: military, Vietnam, tactical, sober.
|
||||
- Do not reintroduce paused MVP/Elo UI.
|
||||
- Do not reintroduce Comunidad Hispana #03.
|
||||
- Do not change historical ingestion policy.
|
||||
- Do not delete backend code, migrations, snapshots/data or endpoints.
|
||||
- Do not add real credentials, secrets, passwords or tokens.
|
||||
- Do not expose credentials or internal filesystem paths through match URLs.
|
||||
- Do not introduce unnecessary frameworks or dependencies.
|
||||
- Keep frontend changes compatible with direct browser opening where applicable.
|
||||
- Confirm `backend/runtime/` is not created or committed.
|
||||
|
||||
## Validation
|
||||
|
||||
Before completing the task:
|
||||
|
||||
1. Run `git status`.
|
||||
2. Run `node --check frontend/assets/js/historico.js`.
|
||||
3. Run `powershell -ExecutionPolicy Bypass -File scripts/run-integration-tests.ps1`.
|
||||
4. Run `docker compose down`.
|
||||
5. Run `docker compose up -d --build`.
|
||||
6. Run `docker compose ps`.
|
||||
7. Run `Invoke-WebRequest http://localhost:8000/health | Select-Object -ExpandProperty Content`.
|
||||
8. Run `Invoke-WebRequest http://localhost:8080 | Select-Object -ExpandProperty StatusCode`.
|
||||
9. Verify served `historico.html`/JS output or manual browser:
|
||||
- no visible word "snapshot"
|
||||
- no visible sentence "Se muestra el ultimo periodo cerrado porque el actual todavia solo suma 0 cierres."
|
||||
- recent match cards still render
|
||||
- when a match URL exists, a "Ver partida" link appears
|
||||
- server #03 is not visible
|
||||
- paused MVP/Elo blocks remain hidden
|
||||
10. Confirm no database migrations or persisted data changed.
|
||||
11. Confirm `backend/runtime/` is not created or committed.
|
||||
12. Review `git diff --name-only` and confirm changed files match the expected scope.
|
||||
|
||||
If a configured validation command cannot be run, document the exact reason in the outcome.
|
||||
|
||||
## Commit And Push Requirements
|
||||
|
||||
1. Run validation before committing.
|
||||
2. Run `git status`.
|
||||
3. Stage only intended files.
|
||||
4. Commit with a clear message, for example:
|
||||
`chore: polish historical copy and match links`
|
||||
5. Push the branch to origin.
|
||||
6. Do not leave completed work only in local.
|
||||
|
||||
## Outcome
|
||||
|
||||
Completed.
|
||||
|
||||
- Validation performed:
|
||||
- `git status --short`
|
||||
- `node --check frontend/assets/js/historico.js`
|
||||
- `powershell -ExecutionPolicy Bypass -File scripts/run-integration-tests.ps1`
|
||||
- `docker compose down`
|
||||
- `docker compose up -d --build`
|
||||
- `docker compose ps`
|
||||
- `Invoke-WebRequest http://localhost:8000/health | Select-Object -ExpandProperty Content`
|
||||
- `Invoke-WebRequest http://localhost:8080 | Select-Object -ExpandProperty StatusCode`
|
||||
- served `historico.html` and `historico.js` copy checks for removed public fallback/snapshot wording
|
||||
- focused temporary-storage check confirming `match_url` is exposed from a safe persisted match reference
|
||||
- Public copy changes made:
|
||||
- Removed the fallback sentence about showing the previous closed period because the current one had 0 closures.
|
||||
- Replaced visible `snapshot` wording in loading states, errors, metadata and empty copy with product-facing Spanish alternatives such as `datos`, `datos precalculados`, `registro` and `resumen`.
|
||||
- Corrected the all-servers summary note copy.
|
||||
- Backend payload field used for match links: `match_url`.
|
||||
- URL source: persisted `historical_matches.raw_payload_ref`, accepted only when it is an HTTP(S) `/games/` URL on the configured historical server `scoreboard_base_url`.
|
||||
- No credentials or internal filesystem paths are exposed through `match_url`; unsafe or off-origin values resolve to no link.
|
||||
- Normal historical UI still renders through the served frontend and backend health checks.
|
||||
- Comunidad Hispana #03 remains absent from the public selector, and paused MVP/Elo blocks were not reintroduced to `historico.html`.
|
||||
- No migrations, persisted data, or `backend/runtime/` were changed or committed.
|
||||
- Browser plugin note: the Browser plugin's Node execution tool was not exposed in this session after discovery, so rendered-page acceptance was verified through served frontend/API checks instead.
|
||||
- Commit hash and push result: pending commit/push step.
|
||||
|
||||
## Change Budget
|
||||
|
||||
- Prefer fewer than 5 modified files.
|
||||
- Prefer changes under 200 lines when feasible.
|
||||
- Split the work into a follow-up task if the scope grows beyond public copy, recent match URL payloads and recent match card rendering.
|
||||
@@ -6,6 +6,7 @@ import sqlite3
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Mapping
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from .config import (
|
||||
get_historical_refresh_overlap_hours,
|
||||
@@ -766,6 +767,8 @@ def list_recent_historical_matches(
|
||||
historical_matches.map_name,
|
||||
historical_matches.allied_score,
|
||||
historical_matches.axis_score,
|
||||
historical_matches.raw_payload_ref,
|
||||
historical_servers.scoreboard_base_url,
|
||||
COUNT(historical_player_match_stats.id) AS player_count
|
||||
FROM historical_matches
|
||||
INNER JOIN historical_servers
|
||||
@@ -804,6 +807,10 @@ def list_recent_historical_matches(
|
||||
),
|
||||
},
|
||||
"player_count": int(row["player_count"] or 0),
|
||||
"match_url": _resolve_safe_match_url(
|
||||
row["raw_payload_ref"],
|
||||
row["scoreboard_base_url"],
|
||||
),
|
||||
}
|
||||
)
|
||||
return items
|
||||
@@ -3114,6 +3121,25 @@ def _is_all_servers_selector(value: str | None) -> bool:
|
||||
return isinstance(value, str) and value.strip() == ALL_SERVERS_SLUG
|
||||
|
||||
|
||||
def _resolve_safe_match_url(raw_payload_ref: object, scoreboard_base_url: object) -> str | None:
|
||||
candidate = _stringify(raw_payload_ref)
|
||||
base_url = _stringify(scoreboard_base_url)
|
||||
if not candidate or not base_url:
|
||||
return None
|
||||
|
||||
candidate_parts = urlparse(candidate)
|
||||
base_parts = urlparse(base_url)
|
||||
if candidate_parts.scheme not in {"http", "https"}:
|
||||
return None
|
||||
if candidate_parts.scheme != base_parts.scheme or candidate_parts.netloc != base_parts.netloc:
|
||||
return None
|
||||
if not candidate_parts.path.startswith("/games/"):
|
||||
return None
|
||||
if candidate_parts.username or candidate_parts.password:
|
||||
return None
|
||||
return candidate
|
||||
|
||||
|
||||
def _start_of_week(value: datetime) -> datetime:
|
||||
normalized = value.astimezone(timezone.utc)
|
||||
midnight = normalized.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
@@ -574,6 +574,13 @@
|
||||
font-size: 1.08rem;
|
||||
}
|
||||
|
||||
.historical-match-card__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.historical-match-card__result {
|
||||
padding: 0.45rem 0.75rem;
|
||||
border: 1px solid rgba(183, 201, 125, 0.24);
|
||||
@@ -585,6 +592,27 @@
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.historical-match-card__link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 34px;
|
||||
padding: 0 0.8rem;
|
||||
border: 1px solid rgba(210, 182, 118, 0.34);
|
||||
border-radius: 999px;
|
||||
color: var(--accent-warm);
|
||||
font-size: 0.76rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.historical-match-card__link:hover,
|
||||
.historical-match-card__link:focus-visible {
|
||||
border-color: rgba(210, 182, 118, 0.62);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.historical-match-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr));
|
||||
@@ -638,6 +666,10 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.historical-match-card__actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.historical-tab {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -161,18 +161,18 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
);
|
||||
weeklyValueHeadingNode.textContent = activeMetricConfig.valueHeading;
|
||||
setRangeBadge(rangeNode, "Cargando rango temporal", false);
|
||||
summaryNoteNode.textContent = `La vista esta leyendo snapshots precalculados del historico local para ${activeServerLabel}.`;
|
||||
setSnapshotMeta(summarySnapshotMetaNode, "Cargando snapshot de resumen...");
|
||||
summaryNoteNode.textContent = `La vista esta leyendo datos precalculados del historico local para ${activeServerLabel}.`;
|
||||
setSnapshotMeta(summarySnapshotMetaNode, "Cargando datos de resumen...");
|
||||
renderSummaryLoading(summaryNode);
|
||||
weeklyWindowNoteNode.textContent = "Cargando snapshot del ranking activo...";
|
||||
weeklyWindowNoteNode.textContent = "Cargando datos del ranking activo...";
|
||||
setSnapshotMeta(
|
||||
weeklySnapshotMetaNode,
|
||||
`Preparando snapshot ${activeTimeframeConfig.shortLabel}...`,
|
||||
`Preparando datos ${activeTimeframeConfig.shortLabel}...`,
|
||||
);
|
||||
recentListNode.innerHTML = "";
|
||||
recentNoteNode.textContent = buildRecentMatchesNote(activeServerSlug);
|
||||
setState(recentStateNode, "Cargando partidas recientes...");
|
||||
setSnapshotMeta(recentSnapshotMetaNode, "Cargando snapshot de partidas...");
|
||||
setSnapshotMeta(recentSnapshotMetaNode, "Cargando datos de partidas...");
|
||||
|
||||
const cachedSummaryPayload = readCachedPayload(
|
||||
summaryCache,
|
||||
@@ -352,10 +352,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
return;
|
||||
}
|
||||
|
||||
weeklyWindowNoteNode.textContent = "Cargando snapshot del ranking activo...";
|
||||
weeklyWindowNoteNode.textContent = "Cargando datos del ranking activo...";
|
||||
setSnapshotMeta(
|
||||
weeklySnapshotMetaNode,
|
||||
`Cargando snapshot ${timeframeConfig.shortLabel}...`,
|
||||
`Cargando datos ${timeframeConfig.shortLabel}...`,
|
||||
);
|
||||
setState(
|
||||
weeklyStateNode,
|
||||
@@ -469,10 +469,10 @@ function hydrateSummary(result, summaryNode, rangeNode, noteNode, snapshotMetaNo
|
||||
const emptyState = getHistoricalEmptyState(activeServerSlug);
|
||||
if (result.status !== "fulfilled") {
|
||||
renderSummaryError(summaryNode);
|
||||
setRangeBadge(rangeNode, "Snapshot de resumen no disponible", false);
|
||||
setRangeBadge(rangeNode, "Resumen no disponible", false);
|
||||
noteNode.textContent =
|
||||
"No se pudo leer el resumen precalculado para el alcance seleccionado.";
|
||||
setSnapshotMeta(snapshotMetaNode, "Error al leer el snapshot de resumen.");
|
||||
setSnapshotMeta(snapshotMetaNode, "Error al leer los datos de resumen.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -487,8 +487,8 @@ function hydrateSummary(result, summaryNode, rangeNode, noteNode, snapshotMetaNo
|
||||
setSnapshotMeta(
|
||||
snapshotMetaNode,
|
||||
payload?.generated_at
|
||||
? buildSnapshotMetaText(payload, "Snapshot de resumen pendiente de generacion.")
|
||||
: "Snapshot de resumen pendiente de generacion.",
|
||||
? buildSnapshotMetaText(payload, "Resumen pendiente de generacion.")
|
||||
: "Resumen pendiente de generacion.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -512,7 +512,7 @@ function hydrateSummary(result, summaryNode, rangeNode, noteNode, snapshotMetaNo
|
||||
);
|
||||
setSnapshotMeta(
|
||||
snapshotMetaNode,
|
||||
buildSnapshotMetaText(payload, "Snapshot de resumen sin timestamp."),
|
||||
buildSnapshotMetaText(payload, "Resumen sin fecha de actualizacion."),
|
||||
);
|
||||
summaryNode.innerHTML = [
|
||||
renderSummaryCard("Servidor", summary.server?.name || "Servidor no disponible"),
|
||||
@@ -557,7 +557,7 @@ function hydrateWeeklyLeaderboard(
|
||||
);
|
||||
noteNode.textContent =
|
||||
"No se pudo leer los datos precalculados para esta metrica.";
|
||||
setSnapshotMeta(snapshotMetaNode, "Error al leer el snapshot del ranking.");
|
||||
setSnapshotMeta(snapshotMetaNode, "Error al leer los datos del ranking.");
|
||||
setState(
|
||||
stateNode,
|
||||
`No se pudo cargar el ranking ${getLeaderboardTimeframeConfig(resolvedTimeframeKey).shortLabel}.`,
|
||||
@@ -576,7 +576,7 @@ function hydrateWeeklyLeaderboard(
|
||||
noteNode.textContent = buildWeeklyWindowNote(payload);
|
||||
setSnapshotMeta(
|
||||
snapshotMetaNode,
|
||||
buildSnapshotMetaText(payload, "Snapshot del ranking pendiente de generacion."),
|
||||
buildSnapshotMetaText(payload, "Ranking pendiente de generacion."),
|
||||
);
|
||||
if (!payload?.found) {
|
||||
setState(
|
||||
@@ -625,14 +625,14 @@ function hydrateRecentMatches(result, stateNode, listNode, snapshotMetaNode) {
|
||||
const emptyState = getHistoricalEmptyState(activeServerSlug);
|
||||
if (result.status !== "fulfilled") {
|
||||
setState(stateNode, "No se pudieron cargar las partidas recientes.", true);
|
||||
setSnapshotMeta(snapshotMetaNode, "Error al leer el snapshot de partidas.");
|
||||
setSnapshotMeta(snapshotMetaNode, "Error al leer los datos de partidas.");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = result.value?.data;
|
||||
setSnapshotMeta(
|
||||
snapshotMetaNode,
|
||||
buildSnapshotMetaText(payload, "Snapshot de partidas pendiente de generacion."),
|
||||
buildSnapshotMetaText(payload, "Partidas pendientes de generacion."),
|
||||
);
|
||||
if (!payload?.found) {
|
||||
setState(stateNode, emptyState.recentMessage);
|
||||
@@ -659,8 +659,8 @@ function hydrateMonthlyMvp(
|
||||
) {
|
||||
if (result.status !== "fulfilled") {
|
||||
titleNode.textContent = `Top 3 MVP mensual V1 - ${getHistoricalServerLabel(activeServerSlug)}`;
|
||||
noteNode.textContent = "No se pudo leer el snapshot mensual del MVP V1.";
|
||||
setSnapshotMeta(snapshotMetaNode, "Error al leer el snapshot del MVP mensual V1.");
|
||||
noteNode.textContent = "No se pudo leer el registro mensual del MVP V1.";
|
||||
setSnapshotMeta(snapshotMetaNode, "Error al leer el registro del MVP mensual V1.");
|
||||
setState(stateNode, "No se pudo cargar el Top 3 MVP mensual V1.", true);
|
||||
listNode.innerHTML = "";
|
||||
return;
|
||||
@@ -673,7 +673,7 @@ function hydrateMonthlyMvp(
|
||||
noteNode.textContent = buildMonthlyMvpNote(payload);
|
||||
setSnapshotMeta(
|
||||
snapshotMetaNode,
|
||||
buildSnapshotMetaText(payload, "Snapshot del MVP mensual V1 pendiente de generacion."),
|
||||
buildSnapshotMetaText(payload, "Registro del MVP mensual V1 pendiente de generacion."),
|
||||
);
|
||||
|
||||
if (!payload?.found) {
|
||||
@@ -709,8 +709,8 @@ function hydrateMonthlyMvpV2(
|
||||
) {
|
||||
if (result.status !== "fulfilled") {
|
||||
titleNode.textContent = `Top 3 MVP mensual V2 - ${getHistoricalServerLabel(activeServerSlug)}`;
|
||||
noteNode.textContent = "No se pudo leer el snapshot mensual del MVP V2.";
|
||||
setSnapshotMeta(snapshotMetaNode, "Error al leer el snapshot del MVP mensual V2.");
|
||||
noteNode.textContent = "No se pudo leer el registro mensual del MVP V2.";
|
||||
setSnapshotMeta(snapshotMetaNode, "Error al leer el registro del MVP mensual V2.");
|
||||
setState(stateNode, "No se pudo cargar el Top 3 MVP mensual V2.", true);
|
||||
listNode.innerHTML = "";
|
||||
return;
|
||||
@@ -723,7 +723,7 @@ function hydrateMonthlyMvpV2(
|
||||
noteNode.textContent = buildMonthlyMvpV2Note(payload);
|
||||
setSnapshotMeta(
|
||||
snapshotMetaNode,
|
||||
buildSnapshotMetaText(payload, "Snapshot del MVP mensual V2 pendiente de generacion."),
|
||||
buildSnapshotMetaText(payload, "Registro del MVP mensual V2 pendiente de generacion."),
|
||||
);
|
||||
|
||||
if (!payload?.found) {
|
||||
@@ -802,6 +802,19 @@ function hydrateMvpComparison(
|
||||
|
||||
function renderRecentMatchCard(item) {
|
||||
const mapName = item.map?.pretty_name || item.map?.name || "Mapa no disponible";
|
||||
const matchUrl = normalizeExternalMatchUrl(item.match_url || item.source_url);
|
||||
const matchLink = matchUrl
|
||||
? `
|
||||
<a
|
||||
class="historical-match-card__link"
|
||||
href="${escapeHtml(matchUrl)}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Ver partida
|
||||
</a>
|
||||
`
|
||||
: "";
|
||||
return `
|
||||
<article class="historical-match-card">
|
||||
<div class="historical-match-card__top">
|
||||
@@ -809,7 +822,10 @@ function renderRecentMatchCard(item) {
|
||||
<p class="historical-match-meta__label">Partida ${escapeHtml(item.match_id || "sin id")}</p>
|
||||
<h3 class="historical-match-card__title">${escapeHtml(mapName)}</h3>
|
||||
</div>
|
||||
<div class="historical-match-card__actions">
|
||||
<span class="historical-match-card__result">${escapeHtml(formatMatchResult(item.result))}</span>
|
||||
${matchLink}
|
||||
</div>
|
||||
</div>
|
||||
<div class="historical-match-meta">
|
||||
<article>
|
||||
@@ -833,6 +849,18 @@ function renderRecentMatchCard(item) {
|
||||
`;
|
||||
}
|
||||
|
||||
function normalizeExternalMatchUrl(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 renderSummaryLoading(summaryNode) {
|
||||
summaryNode.innerHTML = renderSummaryCard("Estado", "Cargando datos historicos");
|
||||
}
|
||||
@@ -1244,7 +1272,7 @@ function buildSummaryNote(summaryBasis, weeklyWindowDays, coverage, serverSlug)
|
||||
return `Este bloque resume ${basisLabel}. La cobertura registrada todavia es inicial y puede crecer en los proximos dias.`;
|
||||
}
|
||||
if (serverSlug === "all-servers") {
|
||||
return `Resumen de los servidore desde ${basisLabel} y combinado solo los servidores actuales de la comunidad.`;
|
||||
return `Resumen de los servidores desde ${basisLabel}, combinado solo con los servidores actuales de la comunidad.`;
|
||||
}
|
||||
return `Resumen servido desde ${basisLabel}.`;
|
||||
}
|
||||
@@ -1263,13 +1291,7 @@ function buildWeeklyWindowNote(payload) {
|
||||
payload?.window_label ||
|
||||
(payload?.timeframe === "monthly" ? "Mes activo" : "Semana activa");
|
||||
if (payload?.uses_fallback) {
|
||||
const currentPeriodMatches =
|
||||
payload?.current_week_closed_matches ??
|
||||
payload?.current_month_closed_matches ??
|
||||
payload?.sufficient_sample?.current_week_closed_matches ??
|
||||
payload?.sufficient_sample?.current_month_closed_matches ??
|
||||
0;
|
||||
return `${windowLabel}: ${start} a ${end}. Se muestra el ultimo periodo cerrado porque el actual todavia solo suma ${formatNumber(currentPeriodMatches)} cierres.`;
|
||||
return `${windowLabel}: ${start} a ${end}.`;
|
||||
}
|
||||
return `${windowLabel}: ${start} a ${end}.`;
|
||||
}
|
||||
@@ -1288,7 +1310,7 @@ function buildRecentMatchesNote(serverSlug) {
|
||||
|
||||
function buildMonthlyMvpNote(payload) {
|
||||
if (!payload?.found) {
|
||||
return "El Top 3 mensual V1 aparecera cuando exista un snapshot MVP listo para este alcance.";
|
||||
return "El Top 3 mensual V1 aparecera cuando exista un registro MVP listo para este alcance.";
|
||||
}
|
||||
const periodLabel =
|
||||
payload?.window_label && payload?.month_key
|
||||
@@ -1308,7 +1330,7 @@ function buildMonthlyMvpFooter(item, payload) {
|
||||
|
||||
function buildMonthlyMvpV2Note(payload) {
|
||||
if (!payload?.found) {
|
||||
return "El Top 3 mensual V2 aparecera cuando exista un snapshot alineado con la cobertura de eventos.";
|
||||
return "El Top 3 mensual V2 aparecera cuando exista un registro alineado con la cobertura de eventos.";
|
||||
}
|
||||
const periodLabel =
|
||||
payload?.window_label && payload?.month_key
|
||||
@@ -1712,7 +1734,7 @@ function getHistoricalEmptyState(serverSlug) {
|
||||
rangeLabel: "Sin cobertura registrada",
|
||||
summaryMessage: "Sin datos historicos suficientes",
|
||||
summaryNote:
|
||||
"Todavia no existe un snapshot de resumen listo para el alcance seleccionado.",
|
||||
"Todavia no existe un resumen listo para el alcance seleccionado.",
|
||||
recentMessage: "Todavia no hay partidas recientes disponibles.",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
Cargando rango del ranking...
|
||||
</p>
|
||||
<p class="historical-snapshot-meta" id="weekly-leaderboard-snapshot-meta">
|
||||
Cargando snapshot del ranking...
|
||||
Cargando datos del ranking...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user