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:
devRaGonSa
2026-05-19 10:12:52 +02:00
committed by GitHub
parent bf3ba630fc
commit d2de0597b6
5 changed files with 324 additions and 35 deletions

View 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.

View File

@@ -6,6 +6,7 @@ import sqlite3
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
from typing import Mapping from typing import Mapping
from urllib.parse import urlparse
from .config import ( from .config import (
get_historical_refresh_overlap_hours, get_historical_refresh_overlap_hours,
@@ -766,6 +767,8 @@ def list_recent_historical_matches(
historical_matches.map_name, historical_matches.map_name,
historical_matches.allied_score, historical_matches.allied_score,
historical_matches.axis_score, historical_matches.axis_score,
historical_matches.raw_payload_ref,
historical_servers.scoreboard_base_url,
COUNT(historical_player_match_stats.id) AS player_count COUNT(historical_player_match_stats.id) AS player_count
FROM historical_matches FROM historical_matches
INNER JOIN historical_servers INNER JOIN historical_servers
@@ -804,6 +807,10 @@ def list_recent_historical_matches(
), ),
}, },
"player_count": int(row["player_count"] or 0), "player_count": int(row["player_count"] or 0),
"match_url": _resolve_safe_match_url(
row["raw_payload_ref"],
row["scoreboard_base_url"],
),
} }
) )
return items 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 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: def _start_of_week(value: datetime) -> datetime:
normalized = value.astimezone(timezone.utc) normalized = value.astimezone(timezone.utc)
midnight = normalized.replace(hour=0, minute=0, second=0, microsecond=0) midnight = normalized.replace(hour=0, minute=0, second=0, microsecond=0)

View File

@@ -574,6 +574,13 @@
font-size: 1.08rem; font-size: 1.08rem;
} }
.historical-match-card__actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 10px;
}
.historical-match-card__result { .historical-match-card__result {
padding: 0.45rem 0.75rem; padding: 0.45rem 0.75rem;
border: 1px solid rgba(183, 201, 125, 0.24); border: 1px solid rgba(183, 201, 125, 0.24);
@@ -585,6 +592,27 @@
text-transform: uppercase; 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 { .historical-match-meta {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr)); grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr));
@@ -638,6 +666,10 @@
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.historical-match-card__actions {
justify-content: flex-start;
}
.historical-tab { .historical-tab {
width: 100%; width: 100%;
} }

View File

@@ -161,18 +161,18 @@ document.addEventListener("DOMContentLoaded", () => {
); );
weeklyValueHeadingNode.textContent = activeMetricConfig.valueHeading; weeklyValueHeadingNode.textContent = activeMetricConfig.valueHeading;
setRangeBadge(rangeNode, "Cargando rango temporal", false); setRangeBadge(rangeNode, "Cargando rango temporal", false);
summaryNoteNode.textContent = `La vista esta leyendo snapshots precalculados del historico local para ${activeServerLabel}.`; summaryNoteNode.textContent = `La vista esta leyendo datos precalculados del historico local para ${activeServerLabel}.`;
setSnapshotMeta(summarySnapshotMetaNode, "Cargando snapshot de resumen..."); setSnapshotMeta(summarySnapshotMetaNode, "Cargando datos de resumen...");
renderSummaryLoading(summaryNode); renderSummaryLoading(summaryNode);
weeklyWindowNoteNode.textContent = "Cargando snapshot del ranking activo..."; weeklyWindowNoteNode.textContent = "Cargando datos del ranking activo...";
setSnapshotMeta( setSnapshotMeta(
weeklySnapshotMetaNode, weeklySnapshotMetaNode,
`Preparando snapshot ${activeTimeframeConfig.shortLabel}...`, `Preparando datos ${activeTimeframeConfig.shortLabel}...`,
); );
recentListNode.innerHTML = ""; recentListNode.innerHTML = "";
recentNoteNode.textContent = buildRecentMatchesNote(activeServerSlug); recentNoteNode.textContent = buildRecentMatchesNote(activeServerSlug);
setState(recentStateNode, "Cargando partidas recientes..."); setState(recentStateNode, "Cargando partidas recientes...");
setSnapshotMeta(recentSnapshotMetaNode, "Cargando snapshot de partidas..."); setSnapshotMeta(recentSnapshotMetaNode, "Cargando datos de partidas...");
const cachedSummaryPayload = readCachedPayload( const cachedSummaryPayload = readCachedPayload(
summaryCache, summaryCache,
@@ -352,10 +352,10 @@ document.addEventListener("DOMContentLoaded", () => {
return; return;
} }
weeklyWindowNoteNode.textContent = "Cargando snapshot del ranking activo..."; weeklyWindowNoteNode.textContent = "Cargando datos del ranking activo...";
setSnapshotMeta( setSnapshotMeta(
weeklySnapshotMetaNode, weeklySnapshotMetaNode,
`Cargando snapshot ${timeframeConfig.shortLabel}...`, `Cargando datos ${timeframeConfig.shortLabel}...`,
); );
setState( setState(
weeklyStateNode, weeklyStateNode,
@@ -469,10 +469,10 @@ function hydrateSummary(result, summaryNode, rangeNode, noteNode, snapshotMetaNo
const emptyState = getHistoricalEmptyState(activeServerSlug); const emptyState = getHistoricalEmptyState(activeServerSlug);
if (result.status !== "fulfilled") { if (result.status !== "fulfilled") {
renderSummaryError(summaryNode); renderSummaryError(summaryNode);
setRangeBadge(rangeNode, "Snapshot de resumen no disponible", false); setRangeBadge(rangeNode, "Resumen no disponible", false);
noteNode.textContent = noteNode.textContent =
"No se pudo leer el resumen precalculado para el alcance seleccionado."; "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; return;
} }
@@ -487,8 +487,8 @@ function hydrateSummary(result, summaryNode, rangeNode, noteNode, snapshotMetaNo
setSnapshotMeta( setSnapshotMeta(
snapshotMetaNode, snapshotMetaNode,
payload?.generated_at payload?.generated_at
? buildSnapshotMetaText(payload, "Snapshot de resumen pendiente de generacion.") ? buildSnapshotMetaText(payload, "Resumen pendiente de generacion.")
: "Snapshot de resumen pendiente de generacion.", : "Resumen pendiente de generacion.",
); );
return; return;
} }
@@ -512,7 +512,7 @@ function hydrateSummary(result, summaryNode, rangeNode, noteNode, snapshotMetaNo
); );
setSnapshotMeta( setSnapshotMeta(
snapshotMetaNode, snapshotMetaNode,
buildSnapshotMetaText(payload, "Snapshot de resumen sin timestamp."), buildSnapshotMetaText(payload, "Resumen sin fecha de actualizacion."),
); );
summaryNode.innerHTML = [ summaryNode.innerHTML = [
renderSummaryCard("Servidor", summary.server?.name || "Servidor no disponible"), renderSummaryCard("Servidor", summary.server?.name || "Servidor no disponible"),
@@ -557,7 +557,7 @@ function hydrateWeeklyLeaderboard(
); );
noteNode.textContent = noteNode.textContent =
"No se pudo leer los datos precalculados para esta metrica."; "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( setState(
stateNode, stateNode,
`No se pudo cargar el ranking ${getLeaderboardTimeframeConfig(resolvedTimeframeKey).shortLabel}.`, `No se pudo cargar el ranking ${getLeaderboardTimeframeConfig(resolvedTimeframeKey).shortLabel}.`,
@@ -576,7 +576,7 @@ function hydrateWeeklyLeaderboard(
noteNode.textContent = buildWeeklyWindowNote(payload); noteNode.textContent = buildWeeklyWindowNote(payload);
setSnapshotMeta( setSnapshotMeta(
snapshotMetaNode, snapshotMetaNode,
buildSnapshotMetaText(payload, "Snapshot del ranking pendiente de generacion."), buildSnapshotMetaText(payload, "Ranking pendiente de generacion."),
); );
if (!payload?.found) { if (!payload?.found) {
setState( setState(
@@ -625,14 +625,14 @@ function hydrateRecentMatches(result, stateNode, listNode, snapshotMetaNode) {
const emptyState = getHistoricalEmptyState(activeServerSlug); const emptyState = getHistoricalEmptyState(activeServerSlug);
if (result.status !== "fulfilled") { if (result.status !== "fulfilled") {
setState(stateNode, "No se pudieron cargar las partidas recientes.", true); 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; return;
} }
const payload = result.value?.data; const payload = result.value?.data;
setSnapshotMeta( setSnapshotMeta(
snapshotMetaNode, snapshotMetaNode,
buildSnapshotMetaText(payload, "Snapshot de partidas pendiente de generacion."), buildSnapshotMetaText(payload, "Partidas pendientes de generacion."),
); );
if (!payload?.found) { if (!payload?.found) {
setState(stateNode, emptyState.recentMessage); setState(stateNode, emptyState.recentMessage);
@@ -659,8 +659,8 @@ function hydrateMonthlyMvp(
) { ) {
if (result.status !== "fulfilled") { if (result.status !== "fulfilled") {
titleNode.textContent = `Top 3 MVP mensual V1 - ${getHistoricalServerLabel(activeServerSlug)}`; titleNode.textContent = `Top 3 MVP mensual V1 - ${getHistoricalServerLabel(activeServerSlug)}`;
noteNode.textContent = "No se pudo leer el snapshot mensual del MVP V1."; noteNode.textContent = "No se pudo leer el registro mensual del MVP V1.";
setSnapshotMeta(snapshotMetaNode, "Error al leer el snapshot del MVP mensual 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); setState(stateNode, "No se pudo cargar el Top 3 MVP mensual V1.", true);
listNode.innerHTML = ""; listNode.innerHTML = "";
return; return;
@@ -673,7 +673,7 @@ function hydrateMonthlyMvp(
noteNode.textContent = buildMonthlyMvpNote(payload); noteNode.textContent = buildMonthlyMvpNote(payload);
setSnapshotMeta( setSnapshotMeta(
snapshotMetaNode, snapshotMetaNode,
buildSnapshotMetaText(payload, "Snapshot del MVP mensual V1 pendiente de generacion."), buildSnapshotMetaText(payload, "Registro del MVP mensual V1 pendiente de generacion."),
); );
if (!payload?.found) { if (!payload?.found) {
@@ -709,8 +709,8 @@ function hydrateMonthlyMvpV2(
) { ) {
if (result.status !== "fulfilled") { if (result.status !== "fulfilled") {
titleNode.textContent = `Top 3 MVP mensual V2 - ${getHistoricalServerLabel(activeServerSlug)}`; titleNode.textContent = `Top 3 MVP mensual V2 - ${getHistoricalServerLabel(activeServerSlug)}`;
noteNode.textContent = "No se pudo leer el snapshot mensual del MVP V2."; noteNode.textContent = "No se pudo leer el registro mensual del MVP V2.";
setSnapshotMeta(snapshotMetaNode, "Error al leer el snapshot del MVP mensual 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); setState(stateNode, "No se pudo cargar el Top 3 MVP mensual V2.", true);
listNode.innerHTML = ""; listNode.innerHTML = "";
return; return;
@@ -723,7 +723,7 @@ function hydrateMonthlyMvpV2(
noteNode.textContent = buildMonthlyMvpV2Note(payload); noteNode.textContent = buildMonthlyMvpV2Note(payload);
setSnapshotMeta( setSnapshotMeta(
snapshotMetaNode, snapshotMetaNode,
buildSnapshotMetaText(payload, "Snapshot del MVP mensual V2 pendiente de generacion."), buildSnapshotMetaText(payload, "Registro del MVP mensual V2 pendiente de generacion."),
); );
if (!payload?.found) { if (!payload?.found) {
@@ -802,6 +802,19 @@ function hydrateMvpComparison(
function renderRecentMatchCard(item) { function renderRecentMatchCard(item) {
const mapName = item.map?.pretty_name || item.map?.name || "Mapa no disponible"; 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 ` return `
<article class="historical-match-card"> <article class="historical-match-card">
<div class="historical-match-card__top"> <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> <p class="historical-match-meta__label">Partida ${escapeHtml(item.match_id || "sin id")}</p>
<h3 class="historical-match-card__title">${escapeHtml(mapName)}</h3> <h3 class="historical-match-card__title">${escapeHtml(mapName)}</h3>
</div> </div>
<div class="historical-match-card__actions">
<span class="historical-match-card__result">${escapeHtml(formatMatchResult(item.result))}</span> <span class="historical-match-card__result">${escapeHtml(formatMatchResult(item.result))}</span>
${matchLink}
</div>
</div> </div>
<div class="historical-match-meta"> <div class="historical-match-meta">
<article> <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) { function renderSummaryLoading(summaryNode) {
summaryNode.innerHTML = renderSummaryCard("Estado", "Cargando datos historicos"); 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.`; return `Este bloque resume ${basisLabel}. La cobertura registrada todavia es inicial y puede crecer en los proximos dias.`;
} }
if (serverSlug === "all-servers") { 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}.`; return `Resumen servido desde ${basisLabel}.`;
} }
@@ -1263,13 +1291,7 @@ function buildWeeklyWindowNote(payload) {
payload?.window_label || payload?.window_label ||
(payload?.timeframe === "monthly" ? "Mes activo" : "Semana activa"); (payload?.timeframe === "monthly" ? "Mes activo" : "Semana activa");
if (payload?.uses_fallback) { if (payload?.uses_fallback) {
const currentPeriodMatches = return `${windowLabel}: ${start} a ${end}.`;
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) { function buildMonthlyMvpNote(payload) {
if (!payload?.found) { 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 = const periodLabel =
payload?.window_label && payload?.month_key payload?.window_label && payload?.month_key
@@ -1308,7 +1330,7 @@ function buildMonthlyMvpFooter(item, payload) {
function buildMonthlyMvpV2Note(payload) { function buildMonthlyMvpV2Note(payload) {
if (!payload?.found) { 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 = const periodLabel =
payload?.window_label && payload?.month_key payload?.window_label && payload?.month_key
@@ -1712,7 +1734,7 @@ function getHistoricalEmptyState(serverSlug) {
rangeLabel: "Sin cobertura registrada", rangeLabel: "Sin cobertura registrada",
summaryMessage: "Sin datos historicos suficientes", summaryMessage: "Sin datos historicos suficientes",
summaryNote: 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.", recentMessage: "Todavia no hay partidas recientes disponibles.",
}; };
} }

View File

@@ -109,7 +109,7 @@
Cargando rango del ranking... Cargando rango del ranking...
</p> </p>
<p class="historical-snapshot-meta" id="weekly-leaderboard-snapshot-meta"> <p class="historical-snapshot-meta" id="weekly-leaderboard-snapshot-meta">
Cargando snapshot del ranking... Cargando datos del ranking...
</p> </p>
</div> </div>
</div> </div>