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 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)
|
||||||
|
|||||||
@@ -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%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
<span class="historical-match-card__result">${escapeHtml(formatMatchResult(item.result))}</span>
|
<div class="historical-match-card__actions">
|
||||||
|
<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.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user