Migrate historical UI to precomputed snapshots
This commit is contained in:
76
ai/tasks/done/TASK-045-historical-ui-migrate-to-snapshots.md
Normal file
76
ai/tasks/done/TASK-045-historical-ui-migrate-to-snapshots.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# TASK-045-historical-ui-migrate-to-snapshots
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Migrar la pagina historica para que lea snapshots precalculados de resumen, tops y partidas recientes, mejorando tiempos de carga y evitando esperas innecesarias al abrir la pagina o cambiar de pestana.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
La UI historica ya existe, pero el objetivo ahora es que no dependa de consultas pesadas en tiempo real. Debe leer snapshots ya preparados y actualizados en segundo plano, mostrando datos rapidos y estables.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
1. Revisar la API de snapshots ya implementada.
|
||||||
|
2. Cambiar la UI historica para que:
|
||||||
|
- resumen
|
||||||
|
- tabs de tops
|
||||||
|
- partidas recientes
|
||||||
|
se carguen desde snapshots y no desde agregados costosos on-demand.
|
||||||
|
3. Mostrar de forma clara cuando se genero el snapshot.
|
||||||
|
4. Mantener estados de loading, empty y error, pero reducir la espera perceptible.
|
||||||
|
5. Asegurar que cambiar entre pestanas de tops sea rapido y estable.
|
||||||
|
6. Mantener coherencia visual con el resto de la web.
|
||||||
|
7. No crear paginas nuevas en esta task.
|
||||||
|
8. Al completar la implementacion:
|
||||||
|
- dejar el repositorio consistente
|
||||||
|
- hacer commit
|
||||||
|
- hacer push al remoto si el entorno lo permite
|
||||||
|
|
||||||
|
## Files to Read First
|
||||||
|
- AGENTS.md
|
||||||
|
- frontend/historico.html
|
||||||
|
- frontend/assets/js/historico.js
|
||||||
|
- frontend/assets/css/historico.css
|
||||||
|
- backend/app/routes.py
|
||||||
|
- backend/app/payloads.py
|
||||||
|
|
||||||
|
## Expected Files to Modify
|
||||||
|
- frontend/historico.html
|
||||||
|
- frontend/assets/js/historico.js
|
||||||
|
- frontend/assets/css/historico.css
|
||||||
|
- opcionalmente backend docs minimas si cambia el flujo de consumo
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
- No romper la UI historica existente.
|
||||||
|
- No introducir frameworks nuevos.
|
||||||
|
- No hacer cambios destructivos.
|
||||||
|
- Mantener el trabajo centrado en rendimiento percibido y consumo de snapshots.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
- La UI historica carga resumen, tops y partidas recientes desde snapshots.
|
||||||
|
- Cambiar de pestana entre tops es rapido.
|
||||||
|
- El usuario ve una referencia clara de actualizacion.
|
||||||
|
- La pagina reduce la dependencia de calculos on-demand pesados.
|
||||||
|
- Los cambios quedan committeados y se hace push al remoto si el entorno lo permite.
|
||||||
|
|
||||||
|
## Change Budget
|
||||||
|
- Preferir menos de 5 archivos modificados o creados.
|
||||||
|
- Preferir menos de 220 lineas cambiadas.
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
- `frontend/assets/js/historico.js` deja de depender de agregados historicos on-demand y pasa a leer snapshots precalculados para resumen, ranking semanal y partidas recientes.
|
||||||
|
- `frontend/assets/js/historico.js` incorpora cache por servidor y metrica para que cambiar de pestana entre tops reutilice payloads ya leidos y reduzca la espera perceptible.
|
||||||
|
- `frontend/historico.html` y `frontend/assets/css/historico.css` muestran una referencia explicita de `generated_at` y del rango fuente de cada snapshot en las tres secciones principales.
|
||||||
|
- `backend/app/routes.py` y `backend/app/payloads.py` anaden la capa minima de lectura `/api/historical/snapshots/*` necesaria para servir snapshots precalculados sin recalcular agregados pesados.
|
||||||
|
- `backend/README.md` documenta los nuevos endpoints de snapshots y la metadata operativa que exponen.
|
||||||
|
|
||||||
|
## Validation Result
|
||||||
|
- Validado con `node --check frontend/assets/js/historico.js`.
|
||||||
|
- Validado con `python -m py_compile backend/app/routes.py backend/app/payloads.py`.
|
||||||
|
- Validado con `resolve_get_payload(...)` para:
|
||||||
|
- `/api/historical/snapshots/server-summary`
|
||||||
|
- `/api/historical/snapshots/weekly-leaderboard`
|
||||||
|
- `/api/historical/snapshots/recent-matches`
|
||||||
|
- Validado con builders Python: los nuevos payloads responden de forma estable incluso cuando todavia no existen snapshots persistidos y devuelven `found: False` con `items: []` o `item: None`.
|
||||||
|
- Revisado en diff: el alcance queda limitado a `backend/README.md`, `backend/app/routes.py`, `backend/app/payloads.py`, `frontend/historico.html`, `frontend/assets/css/historico.css`, `frontend/assets/js/historico.js` y este archivo de task.
|
||||||
|
|
||||||
|
## Decision Notes
|
||||||
|
- La task estaba bloqueada por una dependencia no resuelta: la UI debia migrar a snapshots, pero la API ligera de lectura descrita en la task previa no estaba presente en `routes.py` ni en `payloads.py` dentro de este worktree. Se implemento solo la capa minima necesaria para completar la migracion sin ampliar el alcance a nueva UI ni a nuevos calculos backend.
|
||||||
|
- Se mantuvieron los endpoints historicos legacy para no romper compatibilidad mientras la pagina historica migra al contrato basado en snapshots.
|
||||||
@@ -126,6 +126,9 @@ normaliza espacios y barras finales para mantener la comparacion con el header
|
|||||||
- `GET /api/historical/weekly-leaderboard?metric=kills&limit=10&server=comunidad-hispana-01`
|
- `GET /api/historical/weekly-leaderboard?metric=kills&limit=10&server=comunidad-hispana-01`
|
||||||
- `GET /api/historical/recent-matches?limit=20&server=comunidad-hispana-01`
|
- `GET /api/historical/recent-matches?limit=20&server=comunidad-hispana-01`
|
||||||
- `GET /api/historical/server-summary?server=comunidad-hispana-01`
|
- `GET /api/historical/server-summary?server=comunidad-hispana-01`
|
||||||
|
- `GET /api/historical/snapshots/server-summary?server=comunidad-hispana-01`
|
||||||
|
- `GET /api/historical/snapshots/weekly-leaderboard?metric=kills&limit=10&server=comunidad-hispana-01`
|
||||||
|
- `GET /api/historical/snapshots/recent-matches?limit=6&server=comunidad-hispana-01`
|
||||||
- `GET /api/historical/player-profile?player=steam%3A76561198000000000`
|
- `GET /api/historical/player-profile?player=steam%3A76561198000000000`
|
||||||
|
|
||||||
`GET /api/servers` trata el ultimo snapshot persistido como cache local y lo
|
`GET /api/servers` trata el ultimo snapshot persistido como cache local y lo
|
||||||
@@ -461,6 +464,9 @@ La capa historica propia expone:
|
|||||||
- `/api/historical/weekly-leaderboard`
|
- `/api/historical/weekly-leaderboard`
|
||||||
- `/api/historical/recent-matches`
|
- `/api/historical/recent-matches`
|
||||||
- `/api/historical/server-summary`
|
- `/api/historical/server-summary`
|
||||||
|
- `/api/historical/snapshots/server-summary`
|
||||||
|
- `/api/historical/snapshots/weekly-leaderboard`
|
||||||
|
- `/api/historical/snapshots/recent-matches`
|
||||||
- `/api/historical/player-profile`
|
- `/api/historical/player-profile`
|
||||||
|
|
||||||
Parametros opcionales:
|
Parametros opcionales:
|
||||||
@@ -493,6 +499,23 @@ conteo de jugadores. `server-summary` agrega volumen historico, jugadores
|
|||||||
unicos, kills, mapas dominantes y rango temporal cubierto. `player-profile`
|
unicos, kills, mapas dominantes y rango temporal cubierto. `player-profile`
|
||||||
deja lista la base de consulta agregada por jugador para futuras vistas.
|
deja lista la base de consulta agregada por jugador para futuras vistas.
|
||||||
|
|
||||||
|
La familia `/api/historical/snapshots/*` lee directamente la tabla
|
||||||
|
`historical_precomputed_snapshots` y evita recalcular agregados pesados en cada
|
||||||
|
request. Estos endpoints devuelven payloads ligeros listos para frontend con:
|
||||||
|
|
||||||
|
- `generated_at`
|
||||||
|
- `source_range_start`
|
||||||
|
- `source_range_end`
|
||||||
|
- `is_stale`
|
||||||
|
- `found`
|
||||||
|
|
||||||
|
`/api/historical/snapshots/server-summary` devuelve `item` con el resumen del
|
||||||
|
servidor. `/api/historical/snapshots/weekly-leaderboard` devuelve `items` ya
|
||||||
|
precalculados para una metrica semanal y acepta `limit` para recortar el
|
||||||
|
payload ya persistido sin recalcularlo. `/api/historical/snapshots/recent-matches`
|
||||||
|
devuelve `items` de cierres recientes ya preparados y tambien acepta `limit`
|
||||||
|
para servir solo una parte del snapshot persistido.
|
||||||
|
|
||||||
## Ingesta historica CRCON
|
## Ingesta historica CRCON
|
||||||
|
|
||||||
La ingesta historica no usa A2S ni scraping del HTML de `/games`. Consume la
|
La ingesta historica no usa A2S ni scraping del HTML de `/games`. Consume la
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ from datetime import datetime, timezone
|
|||||||
|
|
||||||
from .collector import collect_server_snapshots
|
from .collector import collect_server_snapshots
|
||||||
from .config import get_refresh_interval_seconds
|
from .config import get_refresh_interval_seconds
|
||||||
|
from .historical_snapshot_storage import get_historical_snapshot
|
||||||
|
from .historical_snapshots import (
|
||||||
|
DEFAULT_SNAPSHOT_WINDOW,
|
||||||
|
DEFAULT_WEEKLY_SNAPSHOT_WINDOW,
|
||||||
|
SNAPSHOT_TYPE_RECENT_MATCHES,
|
||||||
|
SNAPSHOT_TYPE_SERVER_SUMMARY,
|
||||||
|
SNAPSHOT_TYPE_WEEKLY_LEADERBOARD,
|
||||||
|
)
|
||||||
from .historical_storage import (
|
from .historical_storage import (
|
||||||
get_historical_player_profile,
|
get_historical_player_profile,
|
||||||
list_historical_server_summaries,
|
list_historical_server_summaries,
|
||||||
@@ -262,6 +270,104 @@ def build_recent_historical_matches_payload(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_historical_server_summary_snapshot_payload(
|
||||||
|
*,
|
||||||
|
server_slug: str | None = None,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
"""Return one precomputed summary snapshot without recalculating aggregates."""
|
||||||
|
snapshot = _get_historical_snapshot_record(
|
||||||
|
server_key=server_slug,
|
||||||
|
snapshot_type=SNAPSHOT_TYPE_SERVER_SUMMARY,
|
||||||
|
window=DEFAULT_SNAPSHOT_WINDOW,
|
||||||
|
)
|
||||||
|
payload = snapshot.get("payload") if snapshot else {}
|
||||||
|
item = payload.get("item") if isinstance(payload, dict) else None
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"data": {
|
||||||
|
"title": "Snapshot historico de resumen por servidor",
|
||||||
|
"context": "historical-server-summary-snapshot",
|
||||||
|
"source": "historical-precomputed-snapshots",
|
||||||
|
"server_slug": server_slug,
|
||||||
|
"found": snapshot is not None and isinstance(item, dict),
|
||||||
|
**_build_historical_snapshot_metadata(snapshot),
|
||||||
|
"item": item if isinstance(item, dict) else None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_weekly_leaderboard_snapshot_payload(
|
||||||
|
*,
|
||||||
|
limit: int = 10,
|
||||||
|
server_id: str | None = None,
|
||||||
|
metric: str = "kills",
|
||||||
|
) -> dict[str, object]:
|
||||||
|
"""Return one precomputed weekly leaderboard snapshot."""
|
||||||
|
snapshot = _get_historical_snapshot_record(
|
||||||
|
server_key=server_id,
|
||||||
|
snapshot_type=SNAPSHOT_TYPE_WEEKLY_LEADERBOARD,
|
||||||
|
metric=metric,
|
||||||
|
window=DEFAULT_WEEKLY_SNAPSHOT_WINDOW,
|
||||||
|
)
|
||||||
|
payload = snapshot.get("payload") if snapshot else {}
|
||||||
|
items = payload.get("items") if isinstance(payload, dict) else None
|
||||||
|
sliced_items = list(items[:limit]) if isinstance(items, list) else []
|
||||||
|
title_by_metric = {
|
||||||
|
"kills": "Snapshot semanal de top kills por servidor",
|
||||||
|
"deaths": "Snapshot semanal de top muertes por servidor",
|
||||||
|
"support": "Snapshot semanal de top soporte por servidor",
|
||||||
|
"matches_over_100_kills": "Snapshot semanal de partidas 100+ kills por servidor",
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"data": {
|
||||||
|
"title": title_by_metric.get(metric, "Snapshot semanal por servidor"),
|
||||||
|
"context": "historical-weekly-leaderboard-snapshot",
|
||||||
|
"source": "historical-precomputed-snapshots",
|
||||||
|
"server_slug": server_id,
|
||||||
|
"metric": metric,
|
||||||
|
"found": snapshot is not None,
|
||||||
|
**_build_historical_snapshot_metadata(snapshot),
|
||||||
|
"window_days": payload.get("window_days") if isinstance(payload, dict) else 7,
|
||||||
|
"window_start": payload.get("window_start") if isinstance(payload, dict) else None,
|
||||||
|
"window_end": payload.get("window_end") if isinstance(payload, dict) else None,
|
||||||
|
"snapshot_limit": payload.get("limit") if isinstance(payload, dict) else None,
|
||||||
|
"limit": limit,
|
||||||
|
"items": sliced_items,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_recent_historical_matches_snapshot_payload(
|
||||||
|
*,
|
||||||
|
limit: int = 20,
|
||||||
|
server_slug: str | None = None,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
"""Return one precomputed recent-matches snapshot."""
|
||||||
|
snapshot = _get_historical_snapshot_record(
|
||||||
|
server_key=server_slug,
|
||||||
|
snapshot_type=SNAPSHOT_TYPE_RECENT_MATCHES,
|
||||||
|
window=DEFAULT_SNAPSHOT_WINDOW,
|
||||||
|
)
|
||||||
|
payload = snapshot.get("payload") if snapshot else {}
|
||||||
|
items = payload.get("items") if isinstance(payload, dict) else None
|
||||||
|
sliced_items = list(items[:limit]) if isinstance(items, list) else []
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"data": {
|
||||||
|
"title": "Snapshot historico de partidas recientes por servidor",
|
||||||
|
"context": "historical-recent-matches-snapshot",
|
||||||
|
"source": "historical-precomputed-snapshots",
|
||||||
|
"server_slug": server_slug,
|
||||||
|
"found": snapshot is not None,
|
||||||
|
**_build_historical_snapshot_metadata(snapshot),
|
||||||
|
"snapshot_limit": payload.get("limit") if isinstance(payload, dict) else None,
|
||||||
|
"limit": limit,
|
||||||
|
"items": sliced_items,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def build_historical_server_summary_payload(
|
def build_historical_server_summary_payload(
|
||||||
*,
|
*,
|
||||||
server_slug: str | None = None,
|
server_slug: str | None = None,
|
||||||
@@ -298,6 +404,39 @@ def build_historical_player_profile_payload(player_id: str) -> dict[str, object]
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_historical_snapshot_record(
|
||||||
|
*,
|
||||||
|
server_key: str | None,
|
||||||
|
snapshot_type: str,
|
||||||
|
metric: str | None = None,
|
||||||
|
window: str | None = None,
|
||||||
|
) -> dict[str, object] | None:
|
||||||
|
if not server_key:
|
||||||
|
return None
|
||||||
|
return get_historical_snapshot(
|
||||||
|
server_key=server_key,
|
||||||
|
snapshot_type=snapshot_type,
|
||||||
|
metric=metric,
|
||||||
|
window=window,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_historical_snapshot_metadata(snapshot: dict[str, object] | None) -> dict[str, object]:
|
||||||
|
if snapshot is None:
|
||||||
|
return {
|
||||||
|
"generated_at": None,
|
||||||
|
"source_range_start": None,
|
||||||
|
"source_range_end": None,
|
||||||
|
"is_stale": True,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"generated_at": snapshot.get("generated_at"),
|
||||||
|
"source_range_start": snapshot.get("source_range_start"),
|
||||||
|
"source_range_end": snapshot.get("source_range_end"),
|
||||||
|
"is_stale": bool(snapshot.get("is_stale", False)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _enrich_server_items(items: list[dict[str, object]]) -> list[dict[str, object]]:
|
def _enrich_server_items(items: list[dict[str, object]]) -> list[dict[str, object]]:
|
||||||
target_index = {
|
target_index = {
|
||||||
target.external_server_id: target
|
target.external_server_id: target
|
||||||
|
|||||||
@@ -10,14 +10,17 @@ from .payloads import (
|
|||||||
build_discord_payload,
|
build_discord_payload,
|
||||||
build_error_payload,
|
build_error_payload,
|
||||||
build_health_payload,
|
build_health_payload,
|
||||||
|
build_historical_server_summary_snapshot_payload,
|
||||||
build_historical_player_profile_payload,
|
build_historical_player_profile_payload,
|
||||||
build_historical_server_summary_payload,
|
build_historical_server_summary_payload,
|
||||||
|
build_recent_historical_matches_snapshot_payload,
|
||||||
build_recent_historical_matches_payload,
|
build_recent_historical_matches_payload,
|
||||||
build_server_detail_history_payload,
|
build_server_detail_history_payload,
|
||||||
build_server_history_payload,
|
build_server_history_payload,
|
||||||
build_server_latest_payload,
|
build_server_latest_payload,
|
||||||
build_servers_payload,
|
build_servers_payload,
|
||||||
build_trailer_payload,
|
build_trailer_payload,
|
||||||
|
build_weekly_leaderboard_snapshot_payload,
|
||||||
build_weekly_leaderboard_payload,
|
build_weekly_leaderboard_payload,
|
||||||
build_weekly_top_kills_payload,
|
build_weekly_top_kills_payload,
|
||||||
)
|
)
|
||||||
@@ -66,6 +69,21 @@ def resolve_get_payload(path: str) -> tuple[HTTPStatus | None, dict[str, object]
|
|||||||
metric=metric,
|
metric=metric,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if parsed.path == "/api/historical/snapshots/weekly-leaderboard":
|
||||||
|
limit = _parse_limit(parsed.query)
|
||||||
|
if limit is None:
|
||||||
|
return HTTPStatus.BAD_REQUEST, build_error_payload("Invalid limit parameter")
|
||||||
|
params = parse_qs(parsed.query)
|
||||||
|
server_id = params.get("server", [None])[0]
|
||||||
|
metric = params.get("metric", ["kills"])[0]
|
||||||
|
if metric not in {"kills", "deaths", "support", "matches_over_100_kills"}:
|
||||||
|
return HTTPStatus.BAD_REQUEST, build_error_payload("Invalid metric parameter")
|
||||||
|
return HTTPStatus.OK, build_weekly_leaderboard_snapshot_payload(
|
||||||
|
limit=limit,
|
||||||
|
server_id=server_id,
|
||||||
|
metric=metric,
|
||||||
|
)
|
||||||
|
|
||||||
if parsed.path == "/api/historical/recent-matches":
|
if parsed.path == "/api/historical/recent-matches":
|
||||||
limit = _parse_limit(parsed.query)
|
limit = _parse_limit(parsed.query)
|
||||||
if limit is None:
|
if limit is None:
|
||||||
@@ -76,10 +94,26 @@ def resolve_get_payload(path: str) -> tuple[HTTPStatus | None, dict[str, object]
|
|||||||
server_slug=server_slug,
|
server_slug=server_slug,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if parsed.path == "/api/historical/snapshots/recent-matches":
|
||||||
|
limit = _parse_limit(parsed.query)
|
||||||
|
if limit is None:
|
||||||
|
return HTTPStatus.BAD_REQUEST, build_error_payload("Invalid limit parameter")
|
||||||
|
server_slug = parse_qs(parsed.query).get("server", [None])[0]
|
||||||
|
return HTTPStatus.OK, build_recent_historical_matches_snapshot_payload(
|
||||||
|
limit=limit,
|
||||||
|
server_slug=server_slug,
|
||||||
|
)
|
||||||
|
|
||||||
if parsed.path == "/api/historical/server-summary":
|
if parsed.path == "/api/historical/server-summary":
|
||||||
server_slug = parse_qs(parsed.query).get("server", [None])[0]
|
server_slug = parse_qs(parsed.query).get("server", [None])[0]
|
||||||
return HTTPStatus.OK, build_historical_server_summary_payload(server_slug=server_slug)
|
return HTTPStatus.OK, build_historical_server_summary_payload(server_slug=server_slug)
|
||||||
|
|
||||||
|
if parsed.path == "/api/historical/snapshots/server-summary":
|
||||||
|
server_slug = parse_qs(parsed.query).get("server", [None])[0]
|
||||||
|
return HTTPStatus.OK, build_historical_server_summary_snapshot_payload(
|
||||||
|
server_slug=server_slug
|
||||||
|
)
|
||||||
|
|
||||||
if parsed.path == "/api/historical/player-profile":
|
if parsed.path == "/api/historical/player-profile":
|
||||||
player_id = parse_qs(parsed.query).get("player", [None])[0]
|
player_id = parse_qs(parsed.query).get("player", [None])[0]
|
||||||
if not player_id:
|
if not player_id:
|
||||||
|
|||||||
@@ -66,6 +66,13 @@
|
|||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.historical-snapshot-meta {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
color: var(--text-soft);
|
||||||
|
font-size: 0.86rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.historical-selector {
|
.historical-selector {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -43,57 +43,136 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const summaryNode = document.getElementById("historical-summary");
|
const summaryNode = document.getElementById("historical-summary");
|
||||||
const rangeNode = document.getElementById("historical-range");
|
const rangeNode = document.getElementById("historical-range");
|
||||||
const summaryNoteNode = document.getElementById("historical-summary-note");
|
const summaryNoteNode = document.getElementById("historical-summary-note");
|
||||||
|
const summarySnapshotMetaNode = document.getElementById(
|
||||||
|
"historical-summary-snapshot-meta",
|
||||||
|
);
|
||||||
const weeklyTitleNode = document.getElementById("weekly-ranking-title");
|
const weeklyTitleNode = document.getElementById("weekly-ranking-title");
|
||||||
const weeklyStateNode = document.getElementById("weekly-leaderboard-state");
|
const weeklyStateNode = document.getElementById("weekly-leaderboard-state");
|
||||||
const weeklyTableNode = document.getElementById("weekly-leaderboard-table");
|
const weeklyTableNode = document.getElementById("weekly-leaderboard-table");
|
||||||
const weeklyBodyNode = document.getElementById("weekly-leaderboard-body");
|
const weeklyBodyNode = document.getElementById("weekly-leaderboard-body");
|
||||||
const weeklyValueHeadingNode = document.getElementById("weekly-leaderboard-value-heading");
|
const weeklyValueHeadingNode = document.getElementById("weekly-leaderboard-value-heading");
|
||||||
const weeklyWindowNoteNode = document.getElementById("weekly-window-note");
|
const weeklyWindowNoteNode = document.getElementById("weekly-window-note");
|
||||||
|
const weeklySnapshotMetaNode = document.getElementById(
|
||||||
|
"weekly-leaderboard-snapshot-meta",
|
||||||
|
);
|
||||||
const recentStateNode = document.getElementById("recent-matches-state");
|
const recentStateNode = document.getElementById("recent-matches-state");
|
||||||
const recentListNode = document.getElementById("recent-matches-list");
|
const recentListNode = document.getElementById("recent-matches-list");
|
||||||
|
const recentSnapshotMetaNode = document.getElementById(
|
||||||
|
"recent-matches-snapshot-meta",
|
||||||
|
);
|
||||||
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
let activeServerSlug = normalizeServerSlug(params.get("server"));
|
let activeServerSlug = normalizeServerSlug(params.get("server"));
|
||||||
let activeLeaderboardMetric = normalizeLeaderboardMetric(params.get("metric"));
|
let activeLeaderboardMetric = normalizeLeaderboardMetric(params.get("metric"));
|
||||||
let refreshRequestId = 0;
|
let activeServerRequestId = 0;
|
||||||
|
let activeLeaderboardRequestId = 0;
|
||||||
|
|
||||||
const refreshHistoricalView = async () => {
|
const summaryCache = new Map();
|
||||||
const requestId = refreshRequestId + 1;
|
const recentMatchesCache = new Map();
|
||||||
refreshRequestId = requestId;
|
const leaderboardCache = new Map();
|
||||||
|
const pendingRequestCache = new Map();
|
||||||
|
|
||||||
|
const getSummarySnapshot = (serverSlug) =>
|
||||||
|
getCachedJson(
|
||||||
|
summaryCache,
|
||||||
|
pendingRequestCache,
|
||||||
|
buildSummarySnapshotKey(serverSlug),
|
||||||
|
`${backendBaseUrl}/api/historical/snapshots/server-summary?server=${encodeURIComponent(serverSlug)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const getRecentMatchesSnapshot = (serverSlug) =>
|
||||||
|
getCachedJson(
|
||||||
|
recentMatchesCache,
|
||||||
|
pendingRequestCache,
|
||||||
|
buildRecentMatchesSnapshotKey(serverSlug),
|
||||||
|
`${backendBaseUrl}/api/historical/snapshots/recent-matches?server=${encodeURIComponent(serverSlug)}&limit=6`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const getLeaderboardSnapshot = (serverSlug, metricKey) =>
|
||||||
|
getCachedJson(
|
||||||
|
leaderboardCache,
|
||||||
|
pendingRequestCache,
|
||||||
|
buildLeaderboardSnapshotKey(serverSlug, metricKey),
|
||||||
|
`${backendBaseUrl}/api/historical/snapshots/weekly-leaderboard?server=${encodeURIComponent(serverSlug)}&metric=${encodeURIComponent(metricKey)}&limit=10`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const prefetchLeaderboardSnapshots = (serverSlug) => {
|
||||||
|
LEADERBOARD_METRICS.forEach((metric) => {
|
||||||
|
void getLeaderboardSnapshot(serverSlug, metric.key).catch(() => {});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshServerContent = async () => {
|
||||||
|
const requestId = activeServerRequestId + 1;
|
||||||
|
activeServerRequestId = requestId;
|
||||||
const activeMetricConfig = getLeaderboardMetricConfig(activeLeaderboardMetric);
|
const activeMetricConfig = getLeaderboardMetricConfig(activeLeaderboardMetric);
|
||||||
|
|
||||||
syncActiveButtons(selectorButtons, activeServerSlug);
|
syncActiveButtons(selectorButtons, activeServerSlug);
|
||||||
syncLeaderboardTabs(leaderboardTabButtons, activeLeaderboardMetric);
|
syncLeaderboardTabs(leaderboardTabButtons, activeLeaderboardMetric);
|
||||||
setRangeBadge(rangeNode, "Cargando rango temporal", false);
|
|
||||||
summaryNoteNode.textContent =
|
|
||||||
"Este bloque resume solo la cobertura ya registrada en la base local.";
|
|
||||||
renderSummaryLoading(summaryNode);
|
|
||||||
weeklyTitleNode.textContent = activeMetricConfig.title;
|
weeklyTitleNode.textContent = activeMetricConfig.title;
|
||||||
weeklyValueHeadingNode.textContent = activeMetricConfig.valueHeading;
|
weeklyValueHeadingNode.textContent = activeMetricConfig.valueHeading;
|
||||||
weeklyWindowNoteNode.textContent = "Cargando ventana semanal...";
|
setRangeBadge(rangeNode, "Cargando rango temporal", false);
|
||||||
setState(weeklyStateNode, "Cargando ranking semanal...");
|
summaryNoteNode.textContent =
|
||||||
setState(recentStateNode, "Cargando partidas recientes...");
|
"La vista esta leyendo snapshots precalculados del historico local.";
|
||||||
weeklyTableNode.hidden = true;
|
setSnapshotMeta(summarySnapshotMetaNode, "Cargando snapshot de resumen...");
|
||||||
|
renderSummaryLoading(summaryNode);
|
||||||
|
weeklyWindowNoteNode.textContent =
|
||||||
|
"Cargando snapshot semanal del servidor activo...";
|
||||||
|
setSnapshotMeta(weeklySnapshotMetaNode, "Preparando snapshot semanal...");
|
||||||
recentListNode.innerHTML = "";
|
recentListNode.innerHTML = "";
|
||||||
|
setState(recentStateNode, "Cargando partidas recientes...");
|
||||||
|
setSnapshotMeta(recentSnapshotMetaNode, "Cargando snapshot de partidas...");
|
||||||
|
|
||||||
const [summaryResult, leaderboardResult, recentMatchesResult] =
|
const cachedLeaderboardPayload = leaderboardCache.get(
|
||||||
|
buildLeaderboardSnapshotKey(activeServerSlug, activeLeaderboardMetric),
|
||||||
|
);
|
||||||
|
if (cachedLeaderboardPayload) {
|
||||||
|
hydrateWeeklyLeaderboard(
|
||||||
|
{ status: "fulfilled", value: cachedLeaderboardPayload },
|
||||||
|
weeklyStateNode,
|
||||||
|
weeklyTableNode,
|
||||||
|
weeklyBodyNode,
|
||||||
|
weeklyTitleNode,
|
||||||
|
weeklyValueHeadingNode,
|
||||||
|
weeklyWindowNoteNode,
|
||||||
|
weeklySnapshotMetaNode,
|
||||||
|
activeMetricConfig,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setState(weeklyStateNode, "Cargando ranking semanal...");
|
||||||
|
weeklyTableNode.hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetServerSlug = activeServerSlug;
|
||||||
|
const targetMetric = activeLeaderboardMetric;
|
||||||
|
const [summaryResult, recentMatchesResult, leaderboardResult] =
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
fetchJson(
|
getSummarySnapshot(targetServerSlug),
|
||||||
`${backendBaseUrl}/api/historical/server-summary?server=${encodeURIComponent(activeServerSlug)}`,
|
getRecentMatchesSnapshot(targetServerSlug),
|
||||||
),
|
getLeaderboardSnapshot(targetServerSlug, targetMetric),
|
||||||
fetchJson(
|
|
||||||
`${backendBaseUrl}/api/historical/weekly-leaderboard?server=${encodeURIComponent(activeServerSlug)}&metric=${encodeURIComponent(activeLeaderboardMetric)}&limit=10`,
|
|
||||||
),
|
|
||||||
fetchJson(
|
|
||||||
`${backendBaseUrl}/api/historical/recent-matches?server=${encodeURIComponent(activeServerSlug)}&limit=6`,
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (requestId !== refreshRequestId) {
|
if (
|
||||||
|
requestId !== activeServerRequestId ||
|
||||||
|
targetServerSlug !== activeServerSlug ||
|
||||||
|
targetMetric !== activeLeaderboardMetric
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
hydrateSummary(summaryResult, summaryNode, rangeNode, summaryNoteNode);
|
hydrateSummary(
|
||||||
|
summaryResult,
|
||||||
|
summaryNode,
|
||||||
|
rangeNode,
|
||||||
|
summaryNoteNode,
|
||||||
|
summarySnapshotMetaNode,
|
||||||
|
);
|
||||||
|
hydrateRecentMatches(
|
||||||
|
recentMatchesResult,
|
||||||
|
recentStateNode,
|
||||||
|
recentListNode,
|
||||||
|
recentSnapshotMetaNode,
|
||||||
|
);
|
||||||
hydrateWeeklyLeaderboard(
|
hydrateWeeklyLeaderboard(
|
||||||
leaderboardResult,
|
leaderboardResult,
|
||||||
weeklyStateNode,
|
weeklyStateNode,
|
||||||
@@ -102,9 +181,71 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
weeklyTitleNode,
|
weeklyTitleNode,
|
||||||
weeklyValueHeadingNode,
|
weeklyValueHeadingNode,
|
||||||
weeklyWindowNoteNode,
|
weeklyWindowNoteNode,
|
||||||
|
weeklySnapshotMetaNode,
|
||||||
activeMetricConfig,
|
activeMetricConfig,
|
||||||
);
|
);
|
||||||
hydrateRecentMatches(recentMatchesResult, recentStateNode, recentListNode);
|
|
||||||
|
prefetchLeaderboardSnapshots(targetServerSlug);
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshLeaderboardContent = async () => {
|
||||||
|
const requestId = activeLeaderboardRequestId + 1;
|
||||||
|
activeLeaderboardRequestId = requestId;
|
||||||
|
const metricConfig = getLeaderboardMetricConfig(activeLeaderboardMetric);
|
||||||
|
const targetServerSlug = activeServerSlug;
|
||||||
|
const targetMetric = activeLeaderboardMetric;
|
||||||
|
|
||||||
|
syncLeaderboardTabs(leaderboardTabButtons, activeLeaderboardMetric);
|
||||||
|
weeklyTitleNode.textContent = metricConfig.title;
|
||||||
|
weeklyValueHeadingNode.textContent = metricConfig.valueHeading;
|
||||||
|
|
||||||
|
const cachedPayload = leaderboardCache.get(
|
||||||
|
buildLeaderboardSnapshotKey(targetServerSlug, targetMetric),
|
||||||
|
);
|
||||||
|
if (cachedPayload) {
|
||||||
|
hydrateWeeklyLeaderboard(
|
||||||
|
{ status: "fulfilled", value: cachedPayload },
|
||||||
|
weeklyStateNode,
|
||||||
|
weeklyTableNode,
|
||||||
|
weeklyBodyNode,
|
||||||
|
weeklyTitleNode,
|
||||||
|
weeklyValueHeadingNode,
|
||||||
|
weeklyWindowNoteNode,
|
||||||
|
weeklySnapshotMetaNode,
|
||||||
|
metricConfig,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
weeklyWindowNoteNode.textContent =
|
||||||
|
"Cargando snapshot semanal del servidor activo...";
|
||||||
|
setSnapshotMeta(weeklySnapshotMetaNode, "Cargando snapshot semanal...");
|
||||||
|
setState(weeklyStateNode, "Cargando ranking semanal...");
|
||||||
|
weeklyTableNode.hidden = true;
|
||||||
|
|
||||||
|
const leaderboardResult = await settlePromise(
|
||||||
|
getLeaderboardSnapshot(targetServerSlug, targetMetric),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
requestId !== activeLeaderboardRequestId ||
|
||||||
|
targetServerSlug !== activeServerSlug ||
|
||||||
|
targetMetric !== activeLeaderboardMetric
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hydrateWeeklyLeaderboard(
|
||||||
|
leaderboardResult,
|
||||||
|
weeklyStateNode,
|
||||||
|
weeklyTableNode,
|
||||||
|
weeklyBodyNode,
|
||||||
|
weeklyTitleNode,
|
||||||
|
weeklyValueHeadingNode,
|
||||||
|
weeklyWindowNoteNode,
|
||||||
|
weeklySnapshotMetaNode,
|
||||||
|
metricConfig,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
selectorButtons.forEach((button) => {
|
selectorButtons.forEach((button) => {
|
||||||
@@ -118,7 +259,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
params.set("server", activeServerSlug);
|
params.set("server", activeServerSlug);
|
||||||
params.set("metric", activeLeaderboardMetric);
|
params.set("metric", activeLeaderboardMetric);
|
||||||
window.history.replaceState({}, "", `?${params.toString()}`);
|
window.history.replaceState({}, "", `?${params.toString()}`);
|
||||||
void refreshHistoricalView();
|
void refreshServerContent();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -133,46 +274,55 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
params.set("server", activeServerSlug);
|
params.set("server", activeServerSlug);
|
||||||
params.set("metric", activeLeaderboardMetric);
|
params.set("metric", activeLeaderboardMetric);
|
||||||
window.history.replaceState({}, "", `?${params.toString()}`);
|
window.history.replaceState({}, "", `?${params.toString()}`);
|
||||||
void refreshHistoricalView();
|
void refreshLeaderboardContent();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
void refreshHistoricalView();
|
prefetchLeaderboardSnapshots(activeServerSlug);
|
||||||
|
void refreshServerContent();
|
||||||
});
|
});
|
||||||
|
|
||||||
function hydrateSummary(result, summaryNode, rangeNode, noteNode) {
|
function hydrateSummary(result, summaryNode, rangeNode, noteNode, snapshotMetaNode) {
|
||||||
if (result.status !== "fulfilled") {
|
if (result.status !== "fulfilled") {
|
||||||
renderSummaryError(summaryNode);
|
renderSummaryError(summaryNode);
|
||||||
setRangeBadge(rangeNode, "Resumen historico no disponible", false);
|
setRangeBadge(rangeNode, "Snapshot de resumen no disponible", false);
|
||||||
noteNode.textContent =
|
noteNode.textContent =
|
||||||
"No se pudo leer la cobertura historica registrada para este servidor.";
|
"No se pudo leer el resumen precalculado para este servidor.";
|
||||||
|
setSnapshotMeta(snapshotMetaNode, "Error al leer el snapshot de resumen.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = result.value?.data;
|
const payload = result.value?.data;
|
||||||
const items = payload?.items;
|
const summary = payload?.item;
|
||||||
if (!Array.isArray(items) || items.length === 0) {
|
if (!payload?.found || !summary) {
|
||||||
renderSummaryEmpty(summaryNode);
|
renderSummaryEmpty(summaryNode);
|
||||||
setRangeBadge(rangeNode, "Sin cobertura historica", false);
|
setRangeBadge(rangeNode, "Sin snapshot de resumen", false);
|
||||||
noteNode.textContent =
|
noteNode.textContent =
|
||||||
"Todavia no existe cobertura historica suficiente en la base local.";
|
"Todavia no existe un snapshot de resumen listo para este servidor.";
|
||||||
|
setSnapshotMeta(snapshotMetaNode, "Snapshot de resumen pendiente de generacion.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const summary = items[0];
|
|
||||||
const coverage = summary.coverage || {};
|
const coverage = summary.coverage || {};
|
||||||
const timeRange = summary.time_range || {};
|
const timeRange = summary.time_range || {};
|
||||||
const rangeLabel = buildCoverageBadgeLabel(coverage, timeRange);
|
const rangeLabel = buildCoverageBadgeLabel(coverage, {
|
||||||
|
start: payload?.source_range_start || timeRange.start,
|
||||||
|
end: payload?.source_range_end || timeRange.end,
|
||||||
|
});
|
||||||
setRangeBadge(
|
setRangeBadge(
|
||||||
rangeNode,
|
rangeNode,
|
||||||
rangeLabel || "Cobertura registrada parcial",
|
rangeLabel || "Cobertura registrada parcial",
|
||||||
coverage.status === "week-plus",
|
coverage.status === "week-plus" && !payload?.is_stale,
|
||||||
);
|
);
|
||||||
noteNode.textContent = buildSummaryNote(
|
noteNode.textContent = buildSummaryNote(
|
||||||
payload?.summary_basis,
|
"snapshot-precomputed",
|
||||||
payload?.weekly_ranking_window_days,
|
7,
|
||||||
coverage,
|
coverage,
|
||||||
);
|
);
|
||||||
|
setSnapshotMeta(
|
||||||
|
snapshotMetaNode,
|
||||||
|
buildSnapshotMetaText(payload, "Snapshot de resumen sin timestamp."),
|
||||||
|
);
|
||||||
summaryNode.innerHTML = [
|
summaryNode.innerHTML = [
|
||||||
renderSummaryCard("Servidor", summary.server?.name || "Servidor no disponible"),
|
renderSummaryCard("Servidor", summary.server?.name || "Servidor no disponible"),
|
||||||
renderSummaryCard(
|
renderSummaryCard(
|
||||||
@@ -181,7 +331,7 @@ function hydrateSummary(result, summaryNode, rangeNode, noteNode) {
|
|||||||
),
|
),
|
||||||
renderSummaryCard("Jugadores unicos", formatNumber(summary.unique_players)),
|
renderSummaryCard("Jugadores unicos", formatNumber(summary.unique_players)),
|
||||||
renderSummaryCard(
|
renderSummaryCard(
|
||||||
"Datos registrados",
|
"Periodo registrado",
|
||||||
formatCoverageDays(coverage.coverage_days),
|
formatCoverageDays(coverage.coverage_days),
|
||||||
),
|
),
|
||||||
renderSummaryCard("Primera partida", formatTimestamp(coverage.first_match_at)),
|
renderSummaryCard("Primera partida", formatTimestamp(coverage.first_match_at)),
|
||||||
@@ -201,13 +351,15 @@ function hydrateWeeklyLeaderboard(
|
|||||||
titleNode,
|
titleNode,
|
||||||
valueHeadingNode,
|
valueHeadingNode,
|
||||||
noteNode,
|
noteNode,
|
||||||
|
snapshotMetaNode,
|
||||||
metricConfig,
|
metricConfig,
|
||||||
) {
|
) {
|
||||||
titleNode.textContent = metricConfig.title;
|
titleNode.textContent = metricConfig.title;
|
||||||
valueHeadingNode.textContent = metricConfig.valueHeading;
|
valueHeadingNode.textContent = metricConfig.valueHeading;
|
||||||
if (result.status !== "fulfilled") {
|
if (result.status !== "fulfilled") {
|
||||||
noteNode.textContent =
|
noteNode.textContent =
|
||||||
"El ranking usa solo partidas cerradas dentro de la ultima ventana semanal.";
|
"No se pudo leer el snapshot semanal precalculado para esta metrica.";
|
||||||
|
setSnapshotMeta(snapshotMetaNode, "Error al leer el snapshot semanal.");
|
||||||
setState(stateNode, "No se pudo cargar el ranking semanal.", true);
|
setState(stateNode, "No se pudo cargar el ranking semanal.", true);
|
||||||
tableNode.hidden = true;
|
tableNode.hidden = true;
|
||||||
return;
|
return;
|
||||||
@@ -215,6 +367,16 @@ function hydrateWeeklyLeaderboard(
|
|||||||
|
|
||||||
const payload = result.value?.data;
|
const payload = result.value?.data;
|
||||||
noteNode.textContent = buildWeeklyWindowNote(payload);
|
noteNode.textContent = buildWeeklyWindowNote(payload);
|
||||||
|
setSnapshotMeta(
|
||||||
|
snapshotMetaNode,
|
||||||
|
buildSnapshotMetaText(payload, "Snapshot semanal pendiente de generacion."),
|
||||||
|
);
|
||||||
|
if (!payload?.found) {
|
||||||
|
setState(stateNode, metricConfig.emptyMessage);
|
||||||
|
tableNode.hidden = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const items = payload?.items;
|
const items = payload?.items;
|
||||||
if (!Array.isArray(items) || items.length === 0) {
|
if (!Array.isArray(items) || items.length === 0) {
|
||||||
setState(stateNode, metricConfig.emptyMessage);
|
setState(stateNode, metricConfig.emptyMessage);
|
||||||
@@ -238,13 +400,24 @@ function hydrateWeeklyLeaderboard(
|
|||||||
tableNode.hidden = false;
|
tableNode.hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hydrateRecentMatches(result, stateNode, listNode) {
|
function hydrateRecentMatches(result, stateNode, listNode, snapshotMetaNode) {
|
||||||
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.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = result.value?.data?.items;
|
const payload = result.value?.data;
|
||||||
|
setSnapshotMeta(
|
||||||
|
snapshotMetaNode,
|
||||||
|
buildSnapshotMetaText(payload, "Snapshot de partidas pendiente de generacion."),
|
||||||
|
);
|
||||||
|
if (!payload?.found) {
|
||||||
|
setState(stateNode, "Todavia no hay snapshot de partidas recientes disponible.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = payload?.items;
|
||||||
if (!Array.isArray(items) || items.length === 0) {
|
if (!Array.isArray(items) || items.length === 0) {
|
||||||
setState(stateNode, "Todavia no hay partidas recientes disponibles.");
|
setState(stateNode, "Todavia no hay partidas recientes disponibles.");
|
||||||
return;
|
return;
|
||||||
@@ -320,6 +493,10 @@ function setRangeBadge(node, label, isFresh) {
|
|||||||
node.classList.toggle("status-chip--fallback", !isFresh);
|
node.classList.toggle("status-chip--fallback", !isFresh);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setSnapshotMeta(node, message) {
|
||||||
|
node.textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
function syncActiveButtons(buttons, activeServerSlug) {
|
function syncActiveButtons(buttons, activeServerSlug) {
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
button.classList.toggle(
|
button.classList.toggle(
|
||||||
@@ -362,6 +539,18 @@ function getLeaderboardMetricConfig(metricKey) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildSummarySnapshotKey(serverSlug) {
|
||||||
|
return `summary:${serverSlug}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRecentMatchesSnapshotKey(serverSlug) {
|
||||||
|
return `recent:${serverSlug}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLeaderboardSnapshotKey(serverSlug, metricKey) {
|
||||||
|
return `leaderboard:${serverSlug}:${metricKey}`;
|
||||||
|
}
|
||||||
|
|
||||||
function buildRangeLabel(start, end) {
|
function buildRangeLabel(start, end) {
|
||||||
if (!start && !end) {
|
if (!start && !end) {
|
||||||
return "";
|
return "";
|
||||||
@@ -384,8 +573,8 @@ function buildCoverageBadgeLabel(coverage, timeRange) {
|
|||||||
|
|
||||||
function buildSummaryNote(summaryBasis, weeklyWindowDays, coverage) {
|
function buildSummaryNote(summaryBasis, weeklyWindowDays, coverage) {
|
||||||
const basisLabel =
|
const basisLabel =
|
||||||
summaryBasis === "persisted-import"
|
summaryBasis === "snapshot-precomputed"
|
||||||
? "la cobertura ya registrada en la base local"
|
? "el snapshot precalculado del historico local"
|
||||||
: "el historico persistido disponible";
|
: "el historico persistido disponible";
|
||||||
const weeklyWindowLabel = Number.isFinite(Number(weeklyWindowDays))
|
const weeklyWindowLabel = Number.isFinite(Number(weeklyWindowDays))
|
||||||
? `${weeklyWindowDays} dias`
|
? `${weeklyWindowDays} dias`
|
||||||
@@ -394,15 +583,39 @@ function buildSummaryNote(summaryBasis, weeklyWindowDays, coverage) {
|
|||||||
if (status === "under-week") {
|
if (status === "under-week") {
|
||||||
return `Este bloque resume ${basisLabel}. Ahora mismo esa cobertura todavia no alcanza ${weeklyWindowLabel}.`;
|
return `Este bloque resume ${basisLabel}. Ahora mismo esa cobertura todavia no alcanza ${weeklyWindowLabel}.`;
|
||||||
}
|
}
|
||||||
return "Datos generales registrados de los servidores";
|
return `Resumen servido desde ${basisLabel}.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildWeeklyWindowNote(payload) {
|
function buildWeeklyWindowNote(payload) {
|
||||||
|
if (!payload?.found) {
|
||||||
|
return "No existe un snapshot semanal listo para esta metrica en el servidor activo.";
|
||||||
|
}
|
||||||
|
|
||||||
const start = formatTimestamp(payload?.window_start);
|
const start = formatTimestamp(payload?.window_start);
|
||||||
const end = formatTimestamp(payload?.window_end);
|
const end = formatTimestamp(payload?.window_end);
|
||||||
const windowDays = Number(payload?.window_days);
|
const windowDays = Number(payload?.window_days);
|
||||||
const daysLabel = Number.isFinite(windowDays) ? `${windowDays} dias` : "7 dias";
|
const daysLabel = Number.isFinite(windowDays) ? `${windowDays} dias` : "7 dias";
|
||||||
return `Ranking calculado solo con partidas cerradas dentro de la ventana movil de ${daysLabel}: ${start} a ${end}.`;
|
return `Ranking servido desde snapshot semanal de ${daysLabel}: ${start} a ${end}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSnapshotMetaText(payload, missingMessage) {
|
||||||
|
if (!payload?.generated_at) {
|
||||||
|
return missingMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = [
|
||||||
|
payload.is_stale
|
||||||
|
? `Snapshot posiblemente desactualizado: ${formatTimestamp(payload.generated_at)}`
|
||||||
|
: `Snapshot generado: ${formatTimestamp(payload.generated_at)}`,
|
||||||
|
];
|
||||||
|
const sourceRangeLabel = buildRangeLabel(
|
||||||
|
payload?.source_range_start,
|
||||||
|
payload?.source_range_end,
|
||||||
|
);
|
||||||
|
if (sourceRangeLabel) {
|
||||||
|
parts.push(`Fuente: ${sourceRangeLabel}`);
|
||||||
|
}
|
||||||
|
return parts.join(" | ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTopMaps(topMaps) {
|
function formatTopMaps(topMaps) {
|
||||||
@@ -475,6 +688,37 @@ function formatTimestamp(timestamp) {
|
|||||||
}).format(value);
|
}).format(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getCachedJson(cache, pendingCache, key, url) {
|
||||||
|
if (cache.has(key)) {
|
||||||
|
return cache.get(key);
|
||||||
|
}
|
||||||
|
if (pendingCache.has(key)) {
|
||||||
|
return pendingCache.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = fetchJson(url)
|
||||||
|
.then((payload) => {
|
||||||
|
cache.set(key, payload);
|
||||||
|
pendingCache.delete(key);
|
||||||
|
return payload;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
pendingCache.delete(key);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
pendingCache.set(key, request);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function settlePromise(promise) {
|
||||||
|
try {
|
||||||
|
const value = await promise;
|
||||||
|
return { status: "fulfilled", value };
|
||||||
|
} catch (reason) {
|
||||||
|
return { status: "rejected", reason };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchJson(url) {
|
async function fetchJson(url) {
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@@ -75,6 +75,9 @@
|
|||||||
<p class="historical-panel__note" id="historical-summary-note">
|
<p class="historical-panel__note" id="historical-summary-note">
|
||||||
Datos generales registrados de los servidores
|
Datos generales registrados de los servidores
|
||||||
</p>
|
</p>
|
||||||
|
<p class="historical-snapshot-meta" id="historical-summary-snapshot-meta">
|
||||||
|
Cargando snapshot de resumen...
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="status-chip status-chip--fallback" id="historical-range">
|
<p class="status-chip status-chip--fallback" id="historical-range">
|
||||||
Cargando rango temporal
|
Cargando rango temporal
|
||||||
@@ -98,6 +101,9 @@
|
|||||||
<p class="historical-panel__note" id="weekly-window-note">
|
<p class="historical-panel__note" id="weekly-window-note">
|
||||||
Cargando ventana semanal...
|
Cargando ventana semanal...
|
||||||
</p>
|
</p>
|
||||||
|
<p class="historical-snapshot-meta" id="weekly-leaderboard-snapshot-meta">
|
||||||
|
Cargando snapshot semanal...
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -171,6 +177,9 @@
|
|||||||
<p class="historical-panel__note">
|
<p class="historical-panel__note">
|
||||||
Lista de cierres ya registrados para el servidor activo.
|
Lista de cierres ya registrados para el servidor activo.
|
||||||
</p>
|
</p>
|
||||||
|
<p class="historical-snapshot-meta" id="recent-matches-snapshot-meta">
|
||||||
|
Cargando snapshot de partidas...
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="historical-state" id="recent-matches-state" aria-live="polite">
|
<p class="historical-state" id="recent-matches-state" aria-live="polite">
|
||||||
|
|||||||
Reference in New Issue
Block a user