diff --git a/ai/tasks/done/TASK-046-historical-snapshots-generation-and-weekly-fallback.md b/ai/tasks/done/TASK-046-historical-snapshots-generation-and-weekly-fallback.md new file mode 100644 index 0000000..59f720e --- /dev/null +++ b/ai/tasks/done/TASK-046-historical-snapshots-generation-and-weekly-fallback.md @@ -0,0 +1,104 @@ +# TASK-046-historical-snapshots-generation-and-weekly-fallback + +## Goal +Completar la capa operativa de snapshots históricos para que la página histórica deje de mostrar estados vacíos cuando ya existe histórico bruto persistido, y añadir una regla de fallback semanal que use la semana cerrada anterior cuando la semana actual todavía no tenga muestra suficiente, especialmente a principio de semana. + +## Context +La página histórica ya consume endpoints de snapshots precalculados, pero en el estado actual del proyecto esos snapshots no están generados o no se están persistiendo de forma efectiva para el servidor activo. Como resultado, la UI muestra mensajes como “Sin snapshot de resumen” o “Sin datos históricos suficientes” aunque el bootstrap histórico ya haya cargado datos crudos en SQLite. + +Además, la experiencia esperada para los rankings semanales requiere una regla adicional: si la semana actual aún no tiene muestra suficiente —por ejemplo, en lunes o muy al inicio de la semana— debe mostrarse temporalmente la semana cerrada anterior hasta que la actual tenga datos suficientes. + +## Steps +1. Revisar la implementación actual de: + - histórico bruto persistido + - capa de snapshots + - API de snapshots + - UI histórica que consume snapshots +2. Identificar por qué, tras el bootstrap histórico, no se están generando o persistiendo snapshots disponibles para la UI. +3. Completar o corregir el flujo de generación de snapshots para: + - resumen de servidor + - weekly leaderboard + - recent matches +4. Asegurar que exista una forma clara de generar snapshots después del bootstrap y de refrescarlos periódicamente. +5. Validar que, una vez existe histórico bruto suficiente, la UI deje de mostrar mensajes de “sin snapshot” para el servidor activo. +6. Implementar en backend una regla de selección de leaderboard semanal con fallback: + - si la semana actual no tiene muestra suficiente + - y estamos en los primeros días de la semana o en una condición equivalente definida por el proyecto + - devolver la semana cerrada anterior como snapshot activo +7. Definir y documentar claramente qué significa “muestra suficiente” para este fallback semanal. +8. Hacer que el payload del snapshot semanal exponga de forma clara: + - el rango temporal real usado + - si el snapshot pertenece a la semana actual o a la semana cerrada anterior + - cualquier metadato útil para que la UI no engañe al usuario +9. Ajustar la UI solo en lo mínimo necesario para reflejar correctamente el rango real que se está mostrando. +10. Mantener intacta la separación entre: + - histórico bruto + - snapshots precalculados + - live status A2S +11. Al completar la implementación: + - dejar el repositorio consistente + - hacer commit + - hacer push al remoto si el entorno lo permite + +## Files to Read First +- AGENTS.md +- backend/README.md +- backend/app/config.py +- backend/app/historical_storage.py +- backend/app/historical_models.py +- backend/app/historical_runner.py +- backend/app/historical_snapshots.py +- backend/app/historical_snapshot_storage.py +- backend/app/payloads.py +- backend/app/routes.py +- frontend/historico.html +- frontend/assets/js/historico.js +- frontend/assets/css/historico.css + +## Expected Files to Modify +- backend/README.md +- backend/app/config.py +- backend/app/historical_runner.py +- backend/app/historical_snapshots.py +- backend/app/historical_snapshot_storage.py +- backend/app/payloads.py +- backend/app/routes.py +- opcionalmente frontend/assets/js/historico.js si hace falta reflejar el rango semanal real usado +- opcionalmente documentación técnica adicional + +## Constraints +- No usar A2S para esta lógica histórica. +- No volver a queries pesadas on-demand como solución principal. +- No crear páginas nuevas. +- No romper la UI histórica existente. +- No hacer cambios destructivos. +- Mantener el trabajo centrado en generación real de snapshots y fallback semanal coherente. + +## Validation +- La página histórica deja de mostrar “sin snapshot” cuando ya existe histórico bruto suficiente. +- Se generan snapshots de resumen, leaderboard semanal y partidas recientes. +- El leaderboard semanal puede usar la semana anterior cuando la actual aún no tiene muestra suficiente. +- El payload expone claramente el rango temporal real mostrado. +- La UI deja claro qué semana está viendo el usuario. +- Los cambios quedan committeados y se hace push al remoto si el entorno lo permite. + +## Change Budget +- Preferir menos de 7 archivos modificados o creados. +- Preferir menos de 260 líneas cambiadas. + +## Outcome +- La generacion de snapshots historicos deja de depender solo del runner periodico: `bootstrap` y `refresh` regeneran y persisten snapshots al terminar correctamente. +- Se consolidaron snapshots reales para `server-summary`, `weekly-leaderboard` y `recent-matches`. +- El ranking semanal paso de una ventana movil de 7 dias a una semana calendario UTC con fallback explicito a la semana cerrada anterior. +- Se definio "muestra suficiente" como minimo `3` partidas cerradas en la semana actual; el fallback puede aplicarse entre lunes y miercoles UTC si esa muestra aun no existe. +- El payload semanal ahora expone `window_start`, `window_end`, `window_kind`, `window_label`, `uses_fallback`, `selection_reason`, `current_week_closed_matches`, `previous_week_closed_matches` y `sufficient_sample`. +- La UI historica ajusta el texto de la ventana semanal para reflejar el rango real mostrado y avisar cuando se esta viendo temporalmente la semana cerrada anterior. +- Se supero ligeramente el presupuesto orientativo de lineas por mantener la logica de seleccion semanal y la documentacion operativa en un mismo cambio acotado. + +## Validation Notes +- `python -m py_compile backend/app/config.py backend/app/historical_storage.py backend/app/historical_snapshots.py backend/app/historical_runner.py backend/app/historical_ingestion.py backend/app/payloads.py` +- `generate_and_persist_historical_snapshots(server_key='comunidad-hispana-01')` persistio `6` snapshots para el servidor activo. +- Los builders de payload devolvieron `found: True` para `server-summary`, `weekly-leaderboard` y `recent-matches` en `comunidad-hispana-01`. +- Con la base SQLite local actual, el snapshot semanal activo devuelve `window_kind: previous-closed-week-fallback`, `uses_fallback: True`, `current_week_closed_matches: 0` y `previous_week_closed_matches: 44`, lo que valida el fallback real a fecha `2026-03-23`. +- Se valido el nuevo encadenado de ingesta -> regeneracion de snapshots con una ejecucion local stubbeada de `run_incremental_refresh(...)` sin depender de red; el resultado incluyo `snapshot_result.snapshot_count = 6`. +- `git diff --name-only` confirma que solo cambiaron archivos del backend historico, la documentacion backend y el JS minimo de la pagina historica. diff --git a/backend/README.md b/backend/README.md index 5a05836..09e9a3a 100644 --- a/backend/README.md +++ b/backend/README.md @@ -73,6 +73,8 @@ Variables opcionales: - `HLL_HISTORICAL_SNAPSHOT_REFRESH_INTERVAL_SECONDS` - `HLL_HISTORICAL_REFRESH_MAX_RETRIES` - `HLL_HISTORICAL_REFRESH_RETRY_DELAY_SECONDS` +- `HLL_HISTORICAL_WEEKLY_FALLBACK_MIN_MATCHES` +- `HLL_HISTORICAL_WEEKLY_FALLBACK_MAX_WEEKDAY` El `frontend/index.html` viene preparado para volver a consultar el bloque de servidores cada `120000` ms (`120s`) sin recargar la pagina completa. La landing @@ -476,15 +478,16 @@ Parametros opcionales: - `player` en `/api/historical/player-profile` aceptando `stable_player_key`, `steam_id` o `source_player_id` -La ventana temporal es siempre la ultima semana movil respecto al momento de la -request y solo considera partidas cerradas con `ended_at` para no mezclar -partidas aun en curso ni filas historicas transitorias. El payload devuelve -servidor, rango temporal, jugador, kills semanales, posicion y numero de -partidas consideradas. +La ventana temporal usa semana calendario UTC y solo considera partidas +cerradas con `ended_at` para no mezclar partidas aun en curso ni filas +historicas transitorias. El payload devuelve servidor, rango temporal, +jugador, kills semanales, posicion y numero de partidas consideradas. `weekly-leaderboard` generaliza ese bloque para varias metricas semanales por -servidor usando la misma ventana movil de 7 dias y el mismo filtro de partidas -cerradas. Metricas soportadas: +servidor usando el mismo filtro de partidas cerradas. Si la semana actual cae +entre lunes y miercoles UTC y todavia no acumula al menos `3` partidas +cerradas, el backend activa un fallback temporal a la semana cerrada anterior. +Metricas soportadas: - `kills` - `deaths` @@ -509,6 +512,15 @@ request. Estos endpoints devuelven payloads ligeros listos para frontend con: - `is_stale` - `freshness` - `found` +- `window_start` +- `window_end` +- `window_kind` +- `window_label` +- `uses_fallback` +- `selection_reason` +- `current_week_closed_matches` +- `previous_week_closed_matches` +- `sufficient_sample` `/api/historical/snapshots/server-summary` devuelve `item` con el resumen del servidor. `/api/historical/snapshots/weekly-leaderboard` devuelve `items` ya @@ -547,7 +559,9 @@ Flags utiles: La ejecucion `bootstrap` recorre paginas historicas hasta agotar resultados. La ejecucion `refresh` usa una ventana de solape sobre la ultima partida persistida por servidor para releer solo paginas recientes y absorber updates -tardios sin reimportar todo el historico. +tardios sin reimportar todo el historico. Cuando una ejecucion termina +correctamente, tambien recompone los snapshots historicos precalculados para el +servidor afectado o para todos los servidores si la ingesta fue global. El comando devuelve ademas un resumen de cobertura persistida por servidor. Esto ayuda a validar rapidamente cuantos matches reales quedaron importados, el rango @@ -610,6 +624,8 @@ Variables utiles del runner: - `HLL_HISTORICAL_SNAPSHOT_REFRESH_INTERVAL_SECONDS` - `HLL_HISTORICAL_REFRESH_MAX_RETRIES` - `HLL_HISTORICAL_REFRESH_RETRY_DELAY_SECONDS` +- `HLL_HISTORICAL_WEEKLY_FALLBACK_MIN_MATCHES` +- `HLL_HISTORICAL_WEEKLY_FALLBACK_MAX_WEEKDAY` Al inicializar la persistencia local, el backend normaliza tambien la identidad historica ya guardada: diff --git a/backend/app/config.py b/backend/app/config.py index 390cb42..0e443c4 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -19,6 +19,8 @@ DEFAULT_HISTORICAL_REFRESH_INTERVAL_SECONDS = 1800 DEFAULT_HISTORICAL_SNAPSHOT_REFRESH_INTERVAL_SECONDS = 900 DEFAULT_HISTORICAL_REFRESH_MAX_RETRIES = 2 DEFAULT_HISTORICAL_REFRESH_RETRY_DELAY_SECONDS = 30 +DEFAULT_HISTORICAL_WEEKLY_FALLBACK_MIN_MATCHES = 3 +DEFAULT_HISTORICAL_WEEKLY_FALLBACK_MAX_WEEKDAY = 2 DEFAULT_ALLOWED_ORIGINS = ( "null", "http://127.0.0.1:5500", @@ -189,6 +191,32 @@ def get_historical_refresh_retry_delay_seconds() -> int: return retry_delay_seconds +def get_historical_weekly_fallback_min_matches() -> int: + """Return the minimum closed matches required to trust the current week.""" + configured_value = os.getenv( + "HLL_HISTORICAL_WEEKLY_FALLBACK_MIN_MATCHES", + str(DEFAULT_HISTORICAL_WEEKLY_FALLBACK_MIN_MATCHES), + ) + min_matches = int(configured_value) + if min_matches <= 0: + raise ValueError("HLL_HISTORICAL_WEEKLY_FALLBACK_MIN_MATCHES must be positive.") + + return min_matches + + +def get_historical_weekly_fallback_max_weekday() -> int: + """Return the last weekday index where weekly fallback may still apply.""" + configured_value = os.getenv( + "HLL_HISTORICAL_WEEKLY_FALLBACK_MAX_WEEKDAY", + str(DEFAULT_HISTORICAL_WEEKLY_FALLBACK_MAX_WEEKDAY), + ) + max_weekday = int(configured_value) + if max_weekday < 0 or max_weekday > 6: + raise ValueError("HLL_HISTORICAL_WEEKLY_FALLBACK_MAX_WEEKDAY must be between 0 and 6.") + + return max_weekday + + def get_a2s_targets_payload() -> str | None: """Return the optional JSON payload that overrides local A2S targets.""" raw_payload = os.getenv(DEFAULT_A2S_TARGETS_ENV_VAR) diff --git a/backend/app/historical_ingestion.py b/backend/app/historical_ingestion.py index ddf2815..8b2d702 100644 --- a/backend/app/historical_ingestion.py +++ b/backend/app/historical_ingestion.py @@ -19,6 +19,7 @@ from .config import ( get_historical_crcon_request_timeout_seconds, get_historical_crcon_retry_delay_seconds, ) +from .historical_snapshots import generate_and_persist_historical_snapshots from .historical_storage import ( finalize_backfill_progress, finalize_ingestion_run, @@ -163,6 +164,7 @@ def _run_ingestion( archive_exhausted=bool(server_stats["archive_exhausted"]), ) active_runs.pop(str(server["slug"]), None) + snapshot_result = generate_and_persist_historical_snapshots(server_key=server_slug) except Exception as exc: for active_server_slug, run_id in active_runs.items(): finalize_ingestion_run( @@ -193,6 +195,7 @@ def _run_ingestion( "detail_workers": detail_workers or get_historical_crcon_detail_workers(), "servers": processed_servers, "coverage": list_historical_coverage_report(server_slug=server_slug), + "snapshot_result": snapshot_result, "totals": { "pages_processed": stats.pages_processed, "matches_seen": stats.matches_seen, diff --git a/backend/app/historical_runner.py b/backend/app/historical_runner.py index ac18ae2..dfd597d 100644 --- a/backend/app/historical_runner.py +++ b/backend/app/historical_runner.py @@ -14,8 +14,7 @@ from .config import ( get_historical_refresh_retry_delay_seconds, ) from .historical_ingestion import run_incremental_refresh -from .historical_snapshot_storage import persist_historical_snapshot_batch -from .historical_snapshots import build_all_historical_snapshots +from .historical_snapshots import generate_and_persist_historical_snapshots def run_periodic_historical_refresh( @@ -98,23 +97,12 @@ def generate_historical_snapshots( server_slug: str | None = None, ) -> dict[str, Any]: """Build and persist precomputed snapshots for one server or all servers.""" - generated_at = datetime.now(timezone.utc) - snapshots = build_all_historical_snapshots( + result = generate_and_persist_historical_snapshots( server_key=server_slug, - generated_at=generated_at, + generated_at=datetime.now(timezone.utc), ) - persisted_records = persist_historical_snapshot_batch(snapshots) - snapshots_by_server: dict[str, int] = {} - for record in persisted_records: - snapshots_by_server.setdefault(record.server_key, 0) - snapshots_by_server[record.server_key] += 1 - return { - "generated_at": generated_at.isoformat().replace("+00:00", "Z"), - "server_slug": server_slug, - "snapshot_count": len(persisted_records), - "servers_processed": len(snapshots_by_server), - "snapshots_by_server": snapshots_by_server, + **result, "refresh_interval_seconds": get_historical_refresh_interval_seconds(), } diff --git a/backend/app/historical_snapshots.py b/backend/app/historical_snapshots.py index a7c1bbb..13db91e 100644 --- a/backend/app/historical_snapshots.py +++ b/backend/app/historical_snapshots.py @@ -126,10 +126,44 @@ def build_all_historical_snapshots( recent_matches_limit=recent_matches_limit, db_path=db_path, ) - ) + ) return snapshots +def generate_and_persist_historical_snapshots( + *, + server_key: str | None = None, + generated_at: datetime | None = None, + leaderboard_limit: int = DEFAULT_WEEKLY_LEADERBOARD_LIMIT, + recent_matches_limit: int = DEFAULT_RECENT_MATCHES_LIMIT, + db_path: Path | None = None, +) -> dict[str, object]: + """Build and persist precomputed snapshots for one server or all servers.""" + from .historical_snapshot_storage import persist_historical_snapshot_batch + + generated_at_value = _as_utc(generated_at or datetime.now(timezone.utc)) + snapshots = build_all_historical_snapshots( + server_key=server_key, + generated_at=generated_at_value, + leaderboard_limit=leaderboard_limit, + recent_matches_limit=recent_matches_limit, + db_path=db_path, + ) + persisted_records = persist_historical_snapshot_batch(snapshots, db_path=db_path) + snapshots_by_server: dict[str, int] = {} + for record in persisted_records: + snapshots_by_server.setdefault(record.server_key, 0) + snapshots_by_server[record.server_key] += 1 + + return { + "generated_at": _to_iso(generated_at_value), + "server_slug": server_key, + "snapshot_count": len(persisted_records), + "servers_processed": len(snapshots_by_server), + "snapshots_by_server": snapshots_by_server, + } + + def _build_server_summary_snapshot( server_key: str, generated_at: datetime, diff --git a/backend/app/historical_storage.py b/backend/app/historical_storage.py index 9aaa85f..4114d74 100644 --- a/backend/app/historical_storage.py +++ b/backend/app/historical_storage.py @@ -7,7 +7,11 @@ from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Mapping -from .config import get_storage_path +from .config import ( + get_historical_weekly_fallback_max_weekday, + get_historical_weekly_fallback_min_matches, + get_storage_path, +) from .historical_models import HistoricalServerDefinition @@ -1098,16 +1102,26 @@ def list_weekly_leaderboard( ) -> dict[str, object]: """Return ranked weekly leaderboard totals from persisted historical match stats.""" resolved_path = initialize_historical_storage(db_path=db_path) - window_end = datetime.now(timezone.utc) - window_start = window_end - timedelta(days=DEFAULT_WEEKLY_WINDOW_DAYS) + current_time = datetime.now(timezone.utc) + current_week_start = _start_of_week(current_time) + previous_week_start = current_week_start - timedelta(days=DEFAULT_WEEKLY_WINDOW_DAYS) normalized_metric = metric.strip() if isinstance(metric, str) else "" if normalized_metric not in SUPPORTED_WEEKLY_LEADERBOARD_METRICS: raise ValueError(f"Unsupported weekly leaderboard metric: {metric}") + weekly_window = _select_weekly_window( + server_id=server_id, + current_time=current_time, + current_week_start=current_week_start, + previous_week_start=previous_week_start, + db_path=resolved_path, + ) + window_start = weekly_window["window_start"] + window_end = weekly_window["window_end"] where_clauses = [ "historical_matches.ended_at IS NOT NULL", "historical_matches.ended_at >= ?", - "historical_matches.ended_at <= ?", + "historical_matches.ended_at < ?", ] params: list[object] = [ window_start.isoformat().replace("+00:00", "Z"), @@ -1196,6 +1210,21 @@ def list_weekly_leaderboard( "metric": normalized_metric, "window_start": window_start.isoformat().replace("+00:00", "Z"), "window_end": window_end.isoformat().replace("+00:00", "Z"), + "window_days": DEFAULT_WEEKLY_WINDOW_DAYS, + "window_kind": weekly_window["window_kind"], + "window_label": weekly_window["window_label"], + "uses_fallback": weekly_window["uses_fallback"], + "selection_reason": weekly_window["selection_reason"], + "current_week_start": current_week_start.isoformat().replace("+00:00", "Z"), + "current_week_closed_matches": weekly_window["current_week_closed_matches"], + "previous_week_closed_matches": weekly_window["previous_week_closed_matches"], + "sufficient_sample": { + "minimum_closed_matches": weekly_window["minimum_closed_matches"], + "current_week_closed_matches": weekly_window["current_week_closed_matches"], + "current_week_has_sufficient_sample": weekly_window["current_week_has_sufficient_sample"], + "is_early_week": weekly_window["is_early_week"], + "fallback_max_weekday": weekly_window["fallback_max_weekday"], + }, "items": items, } @@ -2112,6 +2141,105 @@ def _calculate_coverage_days( return round(delta.total_seconds() / 86400, 2) +def _select_weekly_window( + *, + server_id: str | None, + current_time: datetime, + current_week_start: datetime, + previous_week_start: datetime, + db_path: Path, +) -> dict[str, object]: + min_matches = get_historical_weekly_fallback_min_matches() + fallback_max_weekday = get_historical_weekly_fallback_max_weekday() + current_week_closed_matches = _count_closed_matches_in_window( + server_id=server_id, + window_start=current_week_start, + window_end=current_time, + db_path=db_path, + ) + previous_week_closed_matches = _count_closed_matches_in_window( + server_id=server_id, + window_start=previous_week_start, + window_end=current_week_start, + db_path=db_path, + ) + is_early_week = current_time.weekday() <= fallback_max_weekday + current_week_has_sufficient_sample = current_week_closed_matches >= min_matches + uses_fallback = ( + is_early_week + and not current_week_has_sufficient_sample + and previous_week_closed_matches > 0 + ) + + if uses_fallback: + return { + "window_start": previous_week_start, + "window_end": current_week_start, + "window_kind": "previous-closed-week-fallback", + "window_label": "Semana cerrada anterior", + "uses_fallback": True, + "selection_reason": "insufficient-current-week-sample", + "minimum_closed_matches": min_matches, + "current_week_closed_matches": current_week_closed_matches, + "previous_week_closed_matches": previous_week_closed_matches, + "current_week_has_sufficient_sample": False, + "is_early_week": is_early_week, + "fallback_max_weekday": fallback_max_weekday, + } + + return { + "window_start": current_week_start, + "window_end": current_time, + "window_kind": "current-week", + "window_label": "Semana actual", + "uses_fallback": False, + "selection_reason": "current-week", + "minimum_closed_matches": min_matches, + "current_week_closed_matches": current_week_closed_matches, + "previous_week_closed_matches": previous_week_closed_matches, + "current_week_has_sufficient_sample": current_week_has_sufficient_sample, + "is_early_week": is_early_week, + "fallback_max_weekday": fallback_max_weekday, + } + + +def _count_closed_matches_in_window( + *, + server_id: str | None, + window_start: datetime, + window_end: datetime, + db_path: Path, +) -> int: + where_clauses = [ + "historical_matches.ended_at IS NOT NULL", + "historical_matches.ended_at >= ?", + "historical_matches.ended_at < ?", + ] + params: list[object] = [ + window_start.isoformat().replace("+00:00", "Z"), + window_end.isoformat().replace("+00:00", "Z"), + ] + if server_id: + normalized_server_id = server_id.strip() + where_clauses.append( + "(historical_servers.slug = ? OR CAST(historical_servers.server_number AS TEXT) = ?)" + ) + params.extend([normalized_server_id, normalized_server_id]) + + with _connect(db_path) as connection: + row = connection.execute( + f""" + SELECT COUNT(DISTINCT historical_matches.id) AS matches_count + FROM historical_matches + INNER JOIN historical_servers + ON historical_servers.id = historical_matches.historical_server_id + WHERE {" AND ".join(where_clauses)} + """, + params, + ).fetchone() + return int(row["matches_count"] or 0) if row is not None else 0 + + def _classify_coverage_status( matches_count: int, coverage_days: float | None, @@ -2125,6 +2253,12 @@ def _classify_coverage_status( return "week-plus" +def _start_of_week(value: datetime) -> datetime: + normalized = value.astimezone(timezone.utc) + midnight = normalized.replace(hour=0, minute=0, second=0, microsecond=0) + return midnight - timedelta(days=midnight.weekday()) + + def _get_nested(payload: Mapping[str, object], *path: str) -> object: current: object = payload for key in path: diff --git a/backend/app/payloads.py b/backend/app/payloads.py index 49768d9..4376c28 100644 --- a/backend/app/payloads.py +++ b/backend/app/payloads.py @@ -240,10 +240,18 @@ def build_weekly_leaderboard_payload( "title": title_by_metric.get(metric, "Ranking semanal por servidor"), "context": "historical-weekly-leaderboard", "metric": metric, - "summary_basis": "closed-matches-last-7-days", - "window_days": 7, + "summary_basis": "closed-matches-calendar-week", + "window_days": result.get("window_days", 7), "window_start": result["window_start"], "window_end": result["window_end"], + "window_kind": result.get("window_kind"), + "window_label": result.get("window_label"), + "uses_fallback": bool(result.get("uses_fallback")), + "selection_reason": result.get("selection_reason"), + "current_week_start": result.get("current_week_start"), + "current_week_closed_matches": result.get("current_week_closed_matches"), + "previous_week_closed_matches": result.get("previous_week_closed_matches"), + "sufficient_sample": result.get("sufficient_sample"), "limit": limit, "items": result["items"], }, @@ -331,6 +339,18 @@ def build_weekly_leaderboard_snapshot_payload( "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, + "window_kind": payload.get("window_kind") if isinstance(payload, dict) else None, + "window_label": payload.get("window_label") if isinstance(payload, dict) else None, + "uses_fallback": bool(payload.get("uses_fallback")) if isinstance(payload, dict) else False, + "selection_reason": payload.get("selection_reason") if isinstance(payload, dict) else None, + "current_week_start": payload.get("current_week_start") if isinstance(payload, dict) else None, + "current_week_closed_matches": ( + payload.get("current_week_closed_matches") if isinstance(payload, dict) else None + ), + "previous_week_closed_matches": ( + payload.get("previous_week_closed_matches") if isinstance(payload, dict) else None + ), + "sufficient_sample": payload.get("sufficient_sample") if isinstance(payload, dict) else None, "snapshot_limit": payload.get("limit") if isinstance(payload, dict) else None, "limit": limit, "items": sliced_items, diff --git a/frontend/assets/js/historico.js b/frontend/assets/js/historico.js index 70644a8..4d7c37a 100644 --- a/frontend/assets/js/historico.js +++ b/frontend/assets/js/historico.js @@ -6,13 +6,13 @@ const DEFAULT_HISTORICAL_SERVER = HISTORICAL_SERVER_SLUGS[0]; const LEADERBOARD_METRICS = Object.freeze([ { key: "kills", - title: "Top kills de los ultimos 7 dias", + title: "Top kills semanales", valueHeading: "Kills", emptyMessage: "Sin datos historicos suficientes para mostrar el top de kills semanal.", }, { key: "deaths", - title: "Top muertes de los ultimos 7 dias", + title: "Top muertes semanales", valueHeading: "Muertes", emptyMessage: "Sin datos historicos suficientes para mostrar el top de muertes semanal.", }, @@ -24,7 +24,7 @@ const LEADERBOARD_METRICS = Object.freeze([ }, { key: "support", - title: "Top puntos de soporte de los ultimos 7 dias", + title: "Top puntos de soporte semanales", valueHeading: "Soporte", emptyMessage: "Sin datos historicos suficientes para mostrar el top de soporte semanal.", }, @@ -593,9 +593,17 @@ function buildWeeklyWindowNote(payload) { const start = formatTimestamp(payload?.window_start); const end = formatTimestamp(payload?.window_end); - const windowDays = Number(payload?.window_days); - const daysLabel = Number.isFinite(windowDays) ? `${windowDays} dias` : "7 dias"; - return `Ranking servido desde snapshot semanal de ${daysLabel}: ${start} a ${end}.`; + const windowLabel = payload?.window_label || "Semana activa"; + if (payload?.uses_fallback) { + const minimumMatches = + payload?.sufficient_sample?.minimum_closed_matches || payload?.minimum_closed_matches || 0; + const currentWeekMatches = + payload?.current_week_closed_matches ?? + payload?.sufficient_sample?.current_week_closed_matches ?? + 0; + return `${windowLabel}: ${start} a ${end}. Se muestra temporalmente porque la semana actual solo tiene ${formatNumber(currentWeekMatches)} cierres y el minimo operativo es ${formatNumber(minimumMatches)}.`; + } + return `${windowLabel}: ${start} a ${end}.`; } function buildSnapshotMetaText(payload, missingMessage) {