1686 lines
58 KiB
Markdown
1686 lines
58 KiB
Markdown
# Backend
|
|
|
|
Esta carpeta contiene el bootstrap minimo del futuro backend principal en Python para HLL Vietnam.
|
|
|
|
## Objetivo en esta fase
|
|
|
|
- dejar un punto de entrada claro para la aplicacion
|
|
- validar que el backend puede arrancar localmente
|
|
- exponer rutas placeholder coherentes con el contrato frontend-backend
|
|
|
|
## Stack actual del bootstrap
|
|
|
|
- Python 3
|
|
- libreria estandar de Python (`http.server`, sin frameworks ni dependencias externas)
|
|
|
|
## Estructura minima
|
|
|
|
```text
|
|
backend/
|
|
|-- README.md
|
|
|-- requirements.txt
|
|
`-- app/
|
|
|-- a2s_client.py
|
|
|-- __init__.py
|
|
|-- collector.py
|
|
|-- main.py
|
|
|-- historical_ingestion.py
|
|
|-- historical_models.py
|
|
|-- historical_runner.py
|
|
|-- historical_storage.py
|
|
|-- normalizers.py
|
|
|-- payloads.py
|
|
|-- routes.py
|
|
|-- server_targets.py
|
|
`-- snapshots.py
|
|
```
|
|
|
|
La persistencia local de desarrollo se crea bajo `backend/data/` cuando el
|
|
colector la necesita por primera vez.
|
|
|
|
`app` es el paquete Python del backend. El archivo correcto del paquete es
|
|
`backend/app/__init__.py`; no debe existir una variante `init.py`.
|
|
|
|
## Punto de entrada
|
|
|
|
El entrypoint real del backend es el modulo `app.main`, ubicado en
|
|
`backend/app/main.py`.
|
|
|
|
Desde la carpeta `backend/`, se puede arrancar localmente con:
|
|
|
|
```powershell
|
|
python -m app.main
|
|
```
|
|
|
|
Ese comando usa imports relativos de paquete (`from .routes import ...`), por lo
|
|
que la forma soportada de arranque es por modulo y no ejecutando el archivo como
|
|
script suelto.
|
|
|
|
Por defecto escuchara en `127.0.0.1:8000`.
|
|
|
|
Variables opcionales:
|
|
|
|
- `HLL_BACKEND_HOST`
|
|
- `HLL_BACKEND_PORT`
|
|
- `HLL_BACKEND_ALLOWED_ORIGINS`
|
|
- `HLL_BACKEND_REFRESH_INTERVAL_SECONDS`
|
|
- `HLL_BACKEND_LIVE_DATA_SOURCE`
|
|
- `HLL_BACKEND_HISTORICAL_DATA_SOURCE`
|
|
- `HLL_BACKEND_RCON_TIMEOUT_SECONDS`
|
|
- `HLL_BACKEND_RCON_TARGETS`
|
|
- `HLL_RCON_HISTORICAL_CAPTURE_INTERVAL_SECONDS`
|
|
- `HLL_RCON_HISTORICAL_CAPTURE_MAX_RETRIES`
|
|
- `HLL_RCON_HISTORICAL_CAPTURE_RETRY_DELAY_SECONDS`
|
|
- `HLL_RCON_BACKFILL_CHUNK_HOURS`
|
|
- `HLL_RCON_BACKFILL_SLEEP_SECONDS`
|
|
- `HLL_RCON_BACKFILL_MAX_DAYS_BACK`
|
|
- `HLL_BACKEND_RCON_ADMIN_LOG_LOOKBACK_MINUTES`
|
|
- `HLL_BACKEND_SQLITE_WRITER_TIMEOUT_SECONDS`
|
|
- `HLL_BACKEND_SQLITE_BUSY_TIMEOUT_MS`
|
|
- `HLL_BACKEND_WRITER_LOCK_TIMEOUT_SECONDS`
|
|
- `HLL_BACKEND_WRITER_LOCK_POLL_INTERVAL_SECONDS`
|
|
- `HLL_HISTORICAL_CRCON_PAGE_SIZE`
|
|
- `HLL_HISTORICAL_CRCON_TIMEOUT_SECONDS`
|
|
- `HLL_HISTORICAL_CRCON_DETAIL_WORKERS`
|
|
- `HLL_HISTORICAL_CRCON_REQUEST_RETRIES`
|
|
- `HLL_HISTORICAL_CRCON_RETRY_DELAY_SECONDS`
|
|
- `HLL_HISTORICAL_REFRESH_INTERVAL_SECONDS`
|
|
- `HLL_HISTORICAL_REFRESH_OVERLAP_HOURS`
|
|
- `HLL_HISTORICAL_SNAPSHOT_REFRESH_INTERVAL_SECONDS`
|
|
- `HLL_HISTORICAL_FULL_SNAPSHOT_EVERY_RUNS`
|
|
- `HLL_HISTORICAL_REFRESH_MAX_RETRIES`
|
|
- `HLL_HISTORICAL_REFRESH_RETRY_DELAY_SECONDS`
|
|
- `HLL_BACKEND_SQLITE_WRITER_TIMEOUT_SECONDS`
|
|
- `HLL_BACKEND_SQLITE_BUSY_TIMEOUT_MS`
|
|
- `HLL_BACKEND_WRITER_LOCK_TIMEOUT_SECONDS`
|
|
- `HLL_BACKEND_WRITER_LOCK_POLL_INTERVAL_SECONDS`
|
|
- `HLL_HISTORICAL_WEEKLY_FALLBACK_MIN_MATCHES`
|
|
- `HLL_HISTORICAL_WEEKLY_FALLBACK_MAX_WEEKDAY`
|
|
|
|
Variables especialmente relevantes para Docker y Compose:
|
|
|
|
- `HLL_BACKEND_HOST`
|
|
- `HLL_BACKEND_PORT`
|
|
- `HLL_BACKEND_DATABASE_URL`
|
|
- `HLL_BACKEND_STORAGE_PATH`
|
|
- `HLL_BACKEND_ALLOWED_ORIGINS`
|
|
- `HLL_BACKEND_LIVE_DATA_SOURCE`
|
|
- `HLL_BACKEND_HISTORICAL_DATA_SOURCE`
|
|
- `HLL_BACKEND_RCON_TIMEOUT_SECONDS`
|
|
- `HLL_BACKEND_RCON_TARGETS`
|
|
- `HLL_HISTORICAL_CRCON_PAGE_SIZE`
|
|
- `HLL_HISTORICAL_CRCON_TIMEOUT_SECONDS`
|
|
- `HLL_HISTORICAL_CRCON_DETAIL_WORKERS`
|
|
- `HLL_HISTORICAL_CRCON_REQUEST_RETRIES`
|
|
- `HLL_HISTORICAL_CRCON_RETRY_DELAY_SECONDS`
|
|
- `HLL_HISTORICAL_REFRESH_OVERLAP_HOURS`
|
|
- `HLL_HISTORICAL_SNAPSHOT_REFRESH_INTERVAL_SECONDS`
|
|
- `HLL_HISTORICAL_FULL_SNAPSHOT_EVERY_RUNS`
|
|
- `HLL_HISTORICAL_REFRESH_MAX_RETRIES`
|
|
- `HLL_HISTORICAL_REFRESH_RETRY_DELAY_SECONDS`
|
|
|
|
Para ejecucion containerizada, el repositorio incluye tambien:
|
|
|
|
- `backend/Dockerfile`
|
|
- `backend/.dockerignore`
|
|
- `backend/.env.example`
|
|
|
|
El contenedor usa el mismo entrypoint real del proyecto:
|
|
|
|
```powershell
|
|
python -m app.main
|
|
```
|
|
|
|
Dentro del contenedor arranca por defecto con:
|
|
|
|
- `HLL_BACKEND_HOST=0.0.0.0`
|
|
- `HLL_BACKEND_PORT=8000`
|
|
- `HLL_BACKEND_STORAGE_PATH=/app/data/hll_vietnam_dev.sqlite3`
|
|
|
|
Compose configura ademas `HLL_BACKEND_DATABASE_URL` para que PostgreSQL sea el
|
|
almacenamiento autoritativo de la fase 1 RCON: muestras/ventanas de captura,
|
|
AdminLog, snapshots de perfil y partidas/estadisticas materializadas. Sin esa
|
|
variable, la ejecucion local mantiene fallback SQLite para esos dominios.
|
|
|
|
Diagnostico rapido del backend activo:
|
|
|
|
```powershell
|
|
python -m app.storage_diagnostics
|
|
```
|
|
|
|
La salida lista el backend RCON activo, counts de las tablas migradas, la ultima
|
|
partida materializada por servidor y que superficies siguen temporalmente en
|
|
SQLite en esta fase.
|
|
|
|
Build local:
|
|
|
|
```powershell
|
|
docker build -t hll-vietnam-backend ./backend
|
|
```
|
|
|
|
Ejecucion local con persistencia bind-mounted:
|
|
|
|
```powershell
|
|
docker run --rm `
|
|
-p 8000:8000 `
|
|
--env-file backend/.env.example `
|
|
-v ${PWD}\backend\data:/app/data `
|
|
hll-vietnam-backend
|
|
```
|
|
|
|
Si se prefiere no usar `--env-file`, el contenedor puede arrancar solo con sus
|
|
defaults para host, puerto y path de SQLite. El bind mount de `/app/data` sigue
|
|
siendo la forma recomendada de no perder persistencia al recrear el contenedor.
|
|
|
|
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
|
|
lee ese valor desde `data-server-refresh-ms`, por lo que puede ajustarse en el
|
|
HTML si una demo local necesita un intervalo distinto.
|
|
|
|
Valor por defecto de `HLL_BACKEND_ALLOWED_ORIGINS`:
|
|
|
|
- `null`
|
|
- `http://127.0.0.1`
|
|
- `http://127.0.0.1:5500`
|
|
- `http://127.0.0.1:8080`
|
|
- `http://localhost`
|
|
- `http://localhost:5500`
|
|
- `http://localhost:8080`
|
|
|
|
Esto cubre el caso de abrir `frontend/index.html` directamente desde `file://`
|
|
y los puertos locales mas habituales cuando el frontend se sirve con un
|
|
servidor sencillo.
|
|
|
|
Prueba local recomendada para validar frontend y backend juntos:
|
|
|
|
1. En una terminal, desde `backend/`, arrancar el backend:
|
|
|
|
```powershell
|
|
python -m app.main
|
|
```
|
|
|
|
2. En otra terminal, desde `frontend/`, servir la landing:
|
|
|
|
```powershell
|
|
python -m http.server 8080
|
|
```
|
|
|
|
3. Abrir `http://localhost:8080`.
|
|
|
|
Si se necesita otra combinacion de origenes locales, puede sobrescribirse
|
|
`HLL_BACKEND_ALLOWED_ORIGINS` con una lista separada por comas. El backend
|
|
normaliza espacios y barras finales para mantener la comparacion con el header
|
|
`Origin` del navegador.
|
|
|
|
## Endpoints placeholder disponibles
|
|
|
|
- `GET /health`
|
|
- `GET /api/community`
|
|
- `GET /api/trailer`
|
|
- `GET /api/discord`
|
|
- `GET /api/servers`
|
|
- `GET /api/servers/latest`
|
|
- `GET /api/servers/history?limit=20`
|
|
- `GET /api/servers/{id}/history?limit=20`
|
|
- `GET /api/historical/weekly-top-kills?limit=10&server=comunidad-hispana-01`
|
|
- `GET /api/historical/weekly-leaderboard?metric=kills&limit=10&server=comunidad-hispana-01`
|
|
- `GET /api/historical/leaderboard?timeframe=monthly&metric=kills&limit=10&server=comunidad-hispana-01`
|
|
- `GET /api/historical/monthly-mvp?limit=10&server=comunidad-hispana-01`
|
|
- `GET /api/historical/monthly-mvp-v2?limit=10&server=comunidad-hispana-01`
|
|
- `GET /api/historical/player-events?view=most-killed&limit=10&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/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/leaderboard?timeframe=monthly&metric=kills&limit=10&server=comunidad-hispana-01`
|
|
- `GET /api/historical/snapshots/monthly-mvp?limit=10&server=comunidad-hispana-01`
|
|
- `GET /api/historical/snapshots/monthly-mvp-v2?limit=10&server=comunidad-hispana-01`
|
|
- `GET /api/historical/snapshots/player-events?view=most-killed&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 /health` expone tambien:
|
|
|
|
- `live_data_source`
|
|
- `historical_data_source`
|
|
|
|
`GET /api/servers` trata el ultimo snapshot persistido como cache local y lo
|
|
reutiliza solo si sigue dentro del objetivo de `120` segundos. Si ese snapshot
|
|
esta vencido, el endpoint intenta primero una consulta RCON real inmediata
|
|
contra los targets configurados. Solo si RCON falla o no devuelve snapshots
|
|
utilizables, cae a A2S de forma controlada antes de responder.
|
|
|
|
La respuesta incluye metadata de frescura pensada para frontend:
|
|
|
|
- `last_snapshot_at`
|
|
- `snapshot_age_seconds`
|
|
- `snapshot_age_minutes`
|
|
- `max_snapshot_age_seconds`
|
|
- `is_stale`
|
|
- `freshness`
|
|
- `source`
|
|
- `refresh_attempted`
|
|
- `refresh_status`
|
|
- `primary_source`
|
|
- `selected_source`
|
|
- `fallback_used`
|
|
- `fallback_reason`
|
|
- `source_attempts`
|
|
|
|
Si la consulta real falla, `/api/servers` devuelve el ultimo snapshot valido
|
|
disponible marcado como stale. Si no existe ningun snapshot valido, responde
|
|
`items: []` en lugar de reintroducir servidores de respaldo ajenos a la
|
|
comunidad. Cada respuesta deja tambien trazabilidad de arbitraje de fuente para
|
|
que sea visible si se sirvio RCON directo o si hubo fallback a A2S.
|
|
|
|
Los endpoints historicos leen la persistencia local SQLite creada por el
|
|
colector. Si todavia no hay snapshots guardados, responden `status: "ok"` con
|
|
`items: []` para mantener un contrato simple en desarrollo.
|
|
|
|
## Seleccion de fuente de datos
|
|
|
|
El backend separa ahora la fuente de datos del contrato HTTP del producto.
|
|
Esto permite cambiar proveedores por entorno sin tocar `routes.py`, payloads de
|
|
UI ni el formato consumido por frontend.
|
|
|
|
Variables nuevas:
|
|
|
|
- `HLL_BACKEND_LIVE_DATA_SOURCE`
|
|
- `HLL_BACKEND_HISTORICAL_DATA_SOURCE`
|
|
|
|
Valores soportados en esta fase:
|
|
|
|
- live:
|
|
- `rcon` como camino primario recomendado
|
|
- `a2s` como fallback legacy o override explicito
|
|
- historico:
|
|
- `rcon` como camino primario recomendado para captura y writer path primario
|
|
- `public-scoreboard` como fallback legacy controlado
|
|
|
|
Defaults actuales:
|
|
|
|
- `HLL_BACKEND_LIVE_DATA_SOURCE=rcon`
|
|
- `HLL_BACKEND_HISTORICAL_DATA_SOURCE=rcon`
|
|
|
|
La seleccion efectiva se resuelve en `app/data_sources.py` y en adapters
|
|
dedicados dentro de `app/providers/`:
|
|
|
|
- `get_live_data_source()` entrega el proveedor usado por `payloads.py`
|
|
cuando `/api/servers` necesita un refresh real
|
|
- `get_historical_data_source()` entrega el proveedor usado por
|
|
`historical_ingestion.py` para bootstrap y refresh incremental
|
|
- `providers/public_scoreboard_provider.py` encapsula la semantica actual del
|
|
scoreboard/CRCON publico bajo el contrato historico
|
|
- `providers/rcon_provider.py` encapsula el proveedor live basado en comandos
|
|
RCON HLL v2 mediante `ServerConnect`, `Login` y `GetServerInformation`
|
|
|
|
Proveedores operativos en esta fase:
|
|
|
|
- live `rcon`
|
|
- live `a2s`
|
|
- historico `rcon` solo para read model minimo y captura prospectiva
|
|
- historico `public-scoreboard` como fallback para cobertura competitiva sin paridad RCON
|
|
|
|
Politica funcional actual:
|
|
|
|
- live:
|
|
- RCON primero
|
|
- A2S solo si RCON falla o no devuelve snapshots utilizables
|
|
- historico/recopilacion:
|
|
- RCON primero
|
|
- `public-scoreboard` solo si RCON no soporta aun esa operacion concreta o
|
|
falla la captura primaria
|
|
|
|
Estado real de "historico por RCON" en esta repo:
|
|
|
|
- no existe backfill retroactivo por RCON con el cliente actual
|
|
- la viabilidad documentada hoy es solo para captura prospectiva separada
|
|
- `historical_ingestion.py` intenta primero el writer path prospectivo RCON
|
|
- la persistencia competitiva `historical_*` sigue necesitando fallback a
|
|
`public-scoreboard` mientras RCON no exponga pagina historica/detalle de
|
|
match cerrada con paridad suficiente
|
|
- el diseno tecnico de esa linea prospectiva queda en
|
|
`docs/rcon-historical-ingestion-design.md`
|
|
|
|
Variables especificas de RCON live:
|
|
|
|
- `HLL_BACKEND_RCON_TIMEOUT_SECONDS`
|
|
- `HLL_BACKEND_RCON_TARGETS`
|
|
|
|
Variables especificas de captura historica prospectiva RCON:
|
|
|
|
- `HLL_RCON_HISTORICAL_CAPTURE_INTERVAL_SECONDS`
|
|
- `HLL_RCON_HISTORICAL_CAPTURE_MAX_RETRIES`
|
|
- `HLL_RCON_HISTORICAL_CAPTURE_RETRY_DELAY_SECONDS`
|
|
|
|
`HLL_BACKEND_RCON_TARGETS` acepta un array JSON con:
|
|
|
|
- `name`
|
|
- `slug` opcional como alias legacy
|
|
- `host`
|
|
- `port`
|
|
- `password`
|
|
- `external_server_id` opcional
|
|
- `region` opcional
|
|
- `game_port` opcional
|
|
- `query_port` opcional
|
|
- `source_name` opcional
|
|
|
|
Compatibilidad operativa del loader:
|
|
|
|
- si llega `slug` pero no `external_server_id`, el backend reutiliza `slug`
|
|
como `external_server_id`
|
|
- si falta `name` pero existe `slug`, el backend genera un nombre razonable a
|
|
partir del slug en vez de dejar `Unnamed RCON target`
|
|
- los errores de validacion indican el campo que falta y las claves
|
|
efectivamente recibidas
|
|
|
|
Timeout recomendado por defecto:
|
|
|
|
- `HLL_BACKEND_RCON_TIMEOUT_SECONDS=20`
|
|
|
|
Diagnostico operativo del cliente RCON:
|
|
|
|
- el cliente informa ahora el stage exacto del fallo cuando puede distinguirlo
|
|
- stages observables:
|
|
- `tcp_connect`
|
|
- `server_connect_request`
|
|
- `server_connect_response`
|
|
- `xor_key_decode`
|
|
- `login_request`
|
|
- `login_response`
|
|
- `get_server_information_request`
|
|
- `get_server_information_response`
|
|
- `payload_decode`
|
|
- `unexpected_response`
|
|
- `timeout`
|
|
- esto mejora el diagnostico del protocolo, pero no resuelve por si solo la
|
|
conectividad real si el servidor acepta TCP y luego no responde al handshake
|
|
RCON o al comando `GetServerInformation`
|
|
|
|
Ejemplo:
|
|
|
|
```powershell
|
|
$env:HLL_BACKEND_RCON_TARGETS='[
|
|
{
|
|
"name": "Comunidad Hispana #01",
|
|
"slug": "comunidad-hispana-01",
|
|
"host": "152.114.195.174",
|
|
"port": 7779,
|
|
"password": "replace-me",
|
|
"external_server_id": "comunidad-hispana-01",
|
|
"region": "ES",
|
|
"game_port": null,
|
|
"query_port": null,
|
|
"source_name": "community-hispana-rcon"
|
|
}
|
|
]'
|
|
```
|
|
|
|
Runbook operativo minimo:
|
|
|
|
- modo recomendado por defecto:
|
|
- `HLL_BACKEND_LIVE_DATA_SOURCE=rcon`
|
|
- `HLL_BACKEND_HISTORICAL_DATA_SOURCE=rcon`
|
|
- usar solo `comunidad-hispana-01` y `comunidad-hispana-02` en los targets
|
|
RCON por defecto
|
|
- modo historico/RCON avanzado:
|
|
- iniciar workers solo de forma explicita
|
|
- no reintroducir `comunidad-hispana-03` salvo validacion nueva
|
|
- override legacy live/A2S:
|
|
- `HLL_BACKEND_LIVE_DATA_SOURCE=a2s`
|
|
- `HLL_BACKEND_HISTORICAL_DATA_SOURCE=rcon`
|
|
|
|
Verificacion minima del proveedor activo:
|
|
|
|
```powershell
|
|
Invoke-WebRequest http://127.0.0.1:8000/health | Select-Object -Expand Content
|
|
```
|
|
|
|
La respuesta incluye `live_data_source` y `historical_data_source`, util para
|
|
confirmar si la instancia esta usando `a2s` o `rcon` para live.
|
|
|
|
Captura historica prospectiva por RCON:
|
|
|
|
- se ejecuta fuera del request path HTTP
|
|
- persiste muestras live hacia delante en tablas `rcon_historical_*`
|
|
- usa RCON como camino historico primario y mantiene `public-scoreboard` solo
|
|
como fallback para operaciones competitivas que aun no tienen paridad RCON
|
|
- no promete backfill retroactivo de matches ya perdidos
|
|
|
|
Arquitectura RCON-first de datos historicos:
|
|
|
|
- `app.rcon_historical_worker` captura sesiones RCON y mantiene ventanas
|
|
competitivas prospectivas.
|
|
- `app.rcon_admin_log_ingestion` ingiere AdminLog para el periodo solicitado.
|
|
- `app.rcon_admin_log_parser` normaliza eventos como inicio/cierre de partida,
|
|
kills, cambios de equipo, chat y mensajes de perfil.
|
|
- `app.rcon_admin_log_storage` persiste eventos AdminLog deduplicados y
|
|
snapshots de perfil de jugador.
|
|
- `app.rcon_admin_log_materialization` materializa partidas cerradas y
|
|
estadisticas por jugador desde eventos RCON.
|
|
- `app.rcon_historical_read_model` expone las lecturas historicas actuales y
|
|
solo recurre a `public-scoreboard` como fallback/enriquecimiento cuando RCON
|
|
no cubre la operacion.
|
|
|
|
Comandos manuales equivalentes dentro de Docker Compose:
|
|
|
|
```powershell
|
|
docker compose exec backend python -m app.rcon_admin_log_ingestion --minutes 1440
|
|
docker compose exec backend python -m app.rcon_historical_worker capture
|
|
```
|
|
|
|
Backfill historico RCON/AdminLog:
|
|
|
|
- runbook: `docs/historical-rcon-backfill.md`
|
|
- ejemplo seco:
|
|
|
|
```powershell
|
|
docker compose run --rm rcon-historical-worker python -m app.rcon_historical_backfill --ensure-recent-matches 100 --servers comunidad-hispana-01,comunidad-hispana-02 --dry-run
|
|
```
|
|
|
|
Comandos manuales desde `backend/`:
|
|
|
|
```powershell
|
|
python -m app.rcon_historical_worker capture
|
|
python -m app.rcon_historical_worker capture --target comunidad-hispana-01
|
|
python -m app.rcon_historical_worker loop --interval 120
|
|
```
|
|
|
|
Runbook minimo:
|
|
|
|
- una pasada manual sobre todos los targets RCON configurados:
|
|
|
|
```powershell
|
|
python -m app.rcon_historical_worker capture
|
|
```
|
|
|
|
- una validacion acotada sobre un target concreto:
|
|
|
|
```powershell
|
|
python -m app.rcon_historical_worker capture --target comunidad-hispana-01
|
|
```
|
|
|
|
- un worker local en bucle:
|
|
|
|
```powershell
|
|
python -m app.rcon_historical_worker loop --interval 120 --max-runs 1
|
|
```
|
|
|
|
La salida del worker incluye:
|
|
|
|
- `target_scope`
|
|
- `captured_at`
|
|
- `targets`
|
|
- `errors`
|
|
- `storage_status`
|
|
|
|
Cuando una captura falla, cada error incluye como minimo:
|
|
|
|
- `target_key`
|
|
- `external_server_id`
|
|
- `name`
|
|
- `host`
|
|
- `port`
|
|
- `timeout_seconds`
|
|
- `error_type`
|
|
- `error_stage`
|
|
- `message`
|
|
|
|
`error_type` intenta clasificar al menos:
|
|
|
|
- `timeout`
|
|
- `auth/login`
|
|
- `connection-refused`
|
|
- `payload-invalid`
|
|
- `other-error`
|
|
|
|
La persistencia queda separada del historico `historical_*` actual y usa:
|
|
|
|
- `rcon_historical_targets`
|
|
- `rcon_historical_capture_runs`
|
|
- `rcon_historical_samples`
|
|
- `rcon_historical_checkpoints`
|
|
|
|
Lectura historica minima cuando `HLL_BACKEND_HISTORICAL_DATA_SOURCE=rcon`:
|
|
|
|
- endpoints soportados hoy directamente por RCON persistido:
|
|
- `GET /api/historical/server-summary`
|
|
- `GET /api/historical/recent-matches`
|
|
- lo que devuelven:
|
|
- cobertura por target RCON configurado
|
|
- frescura del ultimo capture exitoso
|
|
- actividad reciente persistida
|
|
- endpoints que hoy caen automaticamente a `public-scoreboard` para mantener el
|
|
contrato completo:
|
|
- `GET /api/historical/weekly-top-kills`
|
|
- `GET /api/historical/weekly-leaderboard`
|
|
- `GET /api/historical/leaderboard`
|
|
- `GET /api/historical/monthly-mvp`
|
|
- `GET /api/historical/monthly-mvp-v2`
|
|
- `GET /api/historical/player-events`
|
|
- `GET /api/historical/player-profile`
|
|
- `GET /api/historical/snapshots/*`
|
|
|
|
Cuando esos endpoints se consultan con `historical_data_source=rcon`, el backend
|
|
intenta primero RCON para la operacion soportada y, si no hay cobertura o la
|
|
capacidad aun no existe, cae automaticamente a `public-scoreboard`. La
|
|
respuesta deja trazabilidad con `primary_source`, `selected_source`,
|
|
`fallback_used`, `fallback_reason` y `source_attempts`.
|
|
|
|
## Criterio de estructura
|
|
|
|
- `__init__.py` declara el paquete `app` y reexporta las utilidades publicas
|
|
minimas del bootstrap.
|
|
- `collector.py` define el flujo minimo de captura para desarrollo usando una
|
|
fuente controlada.
|
|
- `a2s_client.py` encapsula una consulta minima A2S_INFO por UDP para probar
|
|
servidores reales sin acoplar todavia el backend a una fuente mas compleja.
|
|
- `rcon_client.py` encapsula una conexion minima HLL RCON v2 por TCP con
|
|
`ServerConnect`, XOR key base64, `authToken` y `GetServerInformation` para
|
|
consultas live de produccion.
|
|
- `config.py` centraliza host, puerto y allowlist minima de origenes locales.
|
|
- `data_sources.py` define los contratos y la seleccion por entorno para live e historico.
|
|
- `historical_ingestion.py` intenta primero el writer path RCON y, si hace falta
|
|
poblar `historical_*`, cae de forma explicita a la capa JSON publica de CRCON.
|
|
- `historical_models.py` fija las entidades historicas minimas del dominio.
|
|
- `historical_snapshots.py` fija los tipos y selectores validos de snapshots historicos precalculados.
|
|
- `historical_snapshot_storage.py` persiste snapshots historicos precalculados listos para lectura rapida.
|
|
- `historical_runner.py` ejecuta refresh incremental periodico con reintentos basicos.
|
|
- `historical_storage.py` prepara la persistencia `historical_*` y las consultas agregadas iniciales.
|
|
- `main.py` contiene el entrypoint HTTP y la creacion del servidor.
|
|
- `normalizers.py` transforma registros crudos o respuestas A2S a un modelo
|
|
comun del colector.
|
|
- `routes.py` resuelve las rutas GET soportadas.
|
|
- `payloads.py` centraliza respuestas placeholder y mock.
|
|
- `server_targets.py` registra targets A2S de prueba de forma desacoplada del
|
|
flujo principal del colector.
|
|
- `snapshots.py` construye snapshots consistentes con timestamp comun de
|
|
captura.
|
|
- `storage.py` prepara una persistencia local minima en SQLite para
|
|
`game_sources`, `servers` y `server_snapshots`.
|
|
|
|
## Persistencia local minima
|
|
|
|
El backend ya puede guardar snapshots en un SQLite local de desarrollo usando
|
|
solo libreria estandar de Python. Esta base minima sigue el modelo logico de:
|
|
|
|
- `game_sources`
|
|
- `servers`
|
|
- `server_snapshots`
|
|
- `historical_servers`
|
|
- `historical_maps`
|
|
- `historical_matches`
|
|
- `historical_players`
|
|
- `historical_player_match_stats`
|
|
- `historical_ingestion_runs`
|
|
|
|
Por defecto el archivo se crea en:
|
|
|
|
```text
|
|
backend/data/hll_vietnam_dev.sqlite3
|
|
```
|
|
|
|
En Docker, ese mismo rol de persistencia debe montarse fuera del contenedor en:
|
|
|
|
```text
|
|
/app/data/hll_vietnam_dev.sqlite3
|
|
```
|
|
|
|
Politica comun SQLite para writers:
|
|
|
|
- `timeout` explicito compartido
|
|
- `PRAGMA foreign_keys = ON`
|
|
- `PRAGMA journal_mode = WAL`
|
|
- `PRAGMA busy_timeout`
|
|
- `row_factory = sqlite3.Row`
|
|
|
|
Esta politica se aplica de forma uniforme a las capas writer-capable que
|
|
comparten el mismo SQLite, incluyendo:
|
|
|
|
- `historical_storage.py`
|
|
- `player_event_storage.py`
|
|
- `rcon_historical_storage.py`
|
|
- `storage.py`
|
|
|
|
Politica read-only para historico:
|
|
|
|
- las rutas de lectura de `historical_storage.py` no ejecutan ya
|
|
`initialize_historical_storage()`
|
|
- si el SQLite historico todavia no existe, esas lecturas devuelven resultados
|
|
vacios o defaults estables sin crear archivo ni correr seed/migraciones
|
|
- cuando el archivo ya existe, esas lecturas abren `mode=ro` con
|
|
`row_factory = sqlite3.Row` y `PRAGMA busy_timeout`
|
|
- la inicializacion, migraciones, seed y normalizaciones siguen reservadas al
|
|
writer path explicito
|
|
|
|
Variable opcional:
|
|
|
|
- `HLL_BACKEND_STORAGE_PATH`
|
|
- `HLL_BACKEND_A2S_TARGETS`
|
|
|
|
La base logica sigue documentada en
|
|
`docs/stats-database-schema-foundation.md` para snapshots live y en
|
|
`docs/historical-domain-model.md` para el historico CRCON. Esta implementacion
|
|
no introduce ORM, migraciones ni una decision de almacenamiento productivo.
|
|
|
|
## Snapshots historicos precalculados
|
|
|
|
La capa historica persiste ahora los snapshots precalculados orientados a UI
|
|
como archivos JSON independientes en disco, separados del SQLite del historico
|
|
bruto. Esta capa esta preparada para guardar:
|
|
|
|
- `server-summary`
|
|
- `weekly-leaderboard` con metricas `kills`, `deaths`, `support` y `matches_over_100_kills`
|
|
- `monthly-leaderboard` con las mismas metricas semanticas
|
|
- `monthly-mvp`
|
|
- `recent-matches`
|
|
|
|
Por defecto se escriben bajo:
|
|
|
|
```text
|
|
backend/data/snapshots/<server_key>/
|
|
```
|
|
|
|
En Docker, estos snapshots deben persistirse bajo:
|
|
|
|
```text
|
|
/app/data/snapshots/<server_key>/
|
|
```
|
|
|
|
Ejemplos:
|
|
|
|
- `backend/data/snapshots/comunidad-hispana-01/server-summary.json`
|
|
- `backend/data/snapshots/comunidad-hispana-01/weekly-kills.json`
|
|
- `backend/data/snapshots/comunidad-hispana-02/recent-matches.json`
|
|
- `backend/data/snapshots/all-servers/weekly-support.json`
|
|
- `backend/data/snapshots/all-servers/monthly-mvp.json`
|
|
|
|
Cada archivo conserva metadatos operativos minimos:
|
|
|
|
- `server_key`
|
|
- `snapshot_type`
|
|
- `metric`
|
|
- `window`
|
|
- `payload`
|
|
- `generated_at`
|
|
- `source_range_start`
|
|
- `source_range_end`
|
|
- `is_stale`
|
|
|
|
La persistencia usa una identidad de archivo estable por combinacion de
|
|
servidor, tipo y metrica para que cada refresh reemplace el artefacto anterior
|
|
sin mezclarlo con el historico bruto.
|
|
|
|
Resumen de persistencia recomendada para contenedor:
|
|
|
|
- montar `/app/data`
|
|
- conservar el SQLite historico en `/app/data/hll_vietnam_dev.sqlite3`
|
|
- conservar los snapshots JSON en `/app/data/snapshots/`
|
|
|
|
Con `docker compose`, esa persistencia ya queda montada desde:
|
|
|
|
- `./backend/data -> /app/data`
|
|
|
|
## Bootstrap del colector
|
|
|
|
El backend incluye un bootstrap minimo para el futuro flujo de snapshots:
|
|
|
|
- `fetch_controlled_server_source()` obtiene datos controlados de desarrollo
|
|
- `query_server_info()` permite consultar metadata basica real por A2S_INFO
|
|
- `fetch_a2s_probe()` adapta una consulta A2S real al modelo interno del colector
|
|
- `fetch_configured_a2s_probes()` consulta la lista configurada de targets A2S
|
|
- `normalize_server_record()` reduce los registros a una forma comun
|
|
- `normalize_a2s_server_info()` reduce una respuesta A2S al mismo contrato interno
|
|
- `build_server_snapshot()` y `build_snapshot_batch()` generan snapshots con
|
|
`captured_at`
|
|
- `collect_server_snapshots()` orquesta captura, normalizacion, ensamblado y
|
|
persistencia opcional
|
|
- `persist_snapshot_batch()` escribe el lote en SQLite y mantiene identidad de
|
|
servidor separada del historico
|
|
|
|
Ejecucion manual desde `backend/`:
|
|
|
|
```powershell
|
|
python -m app.collector --source auto
|
|
```
|
|
|
|
Ese comando intenta consultar primero los targets A2S configurados. Si ninguno
|
|
responde y no se ha desactivado el fallback, usa la fuente controlada de
|
|
desarrollo para no romper el flujo local. El resultado imprime el modo usado,
|
|
los errores de consulta y el lote de snapshots persistido en SQLite.
|
|
|
|
Si se quiere forzar solo A2S real:
|
|
|
|
```powershell
|
|
python -m app.collector --source a2s --no-fallback
|
|
```
|
|
|
|
Ese flujo es la validacion local minima extremo a extremo para los targets
|
|
reales configurados de Comunidad Hispana. El timeout por defecto del cliente
|
|
A2S es `6.0s` para tolerar mejor latencia puntual entre multiples consultas
|
|
reales consecutivas. Cuando responden ambos targets por defecto, el comando
|
|
debe devolver:
|
|
|
|
- `collection_mode: "a2s"`
|
|
- `target_count: 2`
|
|
- `success_count: 2`
|
|
- un snapshot con `external_server_id: "comunidad-hispana-01"`
|
|
- un snapshot con `external_server_id: "comunidad-hispana-02"`
|
|
- `source_name: "community-hispana-a2s"`
|
|
- `snapshot_origin: "real-a2s"` en ambos
|
|
- `source_ref: "a2s://152.114.195.174:7778"`
|
|
- `source_ref: "a2s://152.114.195.150:7878"`
|
|
- persistencia en `backend/data/hll_vietnam_dev.sqlite3`
|
|
|
|
Si la consulta se ejecuta desde un entorno con red restringida, sin salida UDP
|
|
o con latencia puntual alta, el cliente puede devolver timeout aunque el target
|
|
este sano. En ese caso el resultado conserva errores controlados por target y
|
|
puede acabar con `success_count` parcial o `0` segun cuantas consultas fallen.
|
|
|
|
Los snapshots persistidos y los endpoints historicos exponen ademas:
|
|
|
|
- `snapshot_origin` para distinguir `real-a2s` frente a `controlled-fallback`
|
|
- `source_ref` para conservar una referencia de procedencia util en historico
|
|
|
|
Si se quiere seguir usando solo datos controlados:
|
|
|
|
```powershell
|
|
python -m app.collector --source controlled
|
|
```
|
|
|
|
## Refresco local periodico de snapshots
|
|
|
|
Para evitar lanzar el colector manualmente en cada captura, el backend incluye
|
|
un bucle local de refresco periodico pensado solo para desarrollo:
|
|
|
|
```powershell
|
|
python -m app.scheduler
|
|
```
|
|
|
|
Ese comando ejecuta capturas persistidas de forma repetida usando el mismo
|
|
flujo del colector y la base SQLite local. Por defecto:
|
|
|
|
- usa `--source auto`
|
|
- espera `120` segundos entre ejecuciones
|
|
- permite fallback controlado si A2S no responde
|
|
- sigue en ejecucion hasta que se detiene manualmente
|
|
|
|
Se puede detener de forma segura con `Ctrl+C`.
|
|
|
|
Variables y flags utiles:
|
|
|
|
- `HLL_BACKEND_REFRESH_INTERVAL_SECONDS` para cambiar el intervalo por defecto
|
|
- `--interval 120` para fijar el intervalo en segundos en una ejecucion concreta
|
|
- `--source a2s --no-fallback` para forzar solo capturas reales
|
|
- `--max-runs 3` para limitar el numero de ciclos y evitar un bucle indefinido
|
|
|
|
Ejemplos:
|
|
|
|
```powershell
|
|
python -m app.scheduler --interval 120
|
|
python -m app.scheduler --source a2s --no-fallback --max-runs 2
|
|
```
|
|
|
|
Flujo local recomendado para ver datos vivos en la landing:
|
|
|
|
1. Desde `backend/`, arrancar la API:
|
|
|
|
```powershell
|
|
python -m app.main
|
|
```
|
|
|
|
2. En otra terminal, dejar el scheduler corriendo:
|
|
|
|
```powershell
|
|
python -m app.scheduler
|
|
```
|
|
|
|
3. Servir `frontend/` con un servidor local sencillo y abrir la landing. El
|
|
frontend volvera a pedir `/api/servers` cada `120` segundos, por lo que los
|
|
cambios de mapa o poblacion apareceran sin recarga manual cuando existan
|
|
snapshots nuevos.
|
|
|
|
Este mecanismo deja el refresco desacoplado del servidor HTTP y es facil de
|
|
reemplazar mas adelante por un scheduler mas serio sin rehacer el colector.
|
|
|
|
Prueba manual minima de A2S desde `backend/`:
|
|
|
|
```powershell
|
|
python -m app.a2s_client 203.0.113.10 27015
|
|
```
|
|
|
|
Ese comando lanza una consulta `A2S_INFO` por UDP y devuelve JSON con nombre de
|
|
servidor, mapa, jugadores y capacidad maxima cuando el query port responde.
|
|
Tambien puede reutilizarse desde Python con `query_server_info()` o
|
|
`fetch_a2s_probe()`. Si el servidor no responde o el puerto es incorrecto, el
|
|
cliente eleva errores controlados de timeout o protocolo para que la siguiente
|
|
task pueda integrarlo en el pipeline de snapshots sin romper el backend.
|
|
|
|
## Registro local de targets A2S
|
|
|
|
La lista de targets A2S vive en `app/server_targets.py`. Por defecto el backend
|
|
registra solo el primer target real verificado del proyecto:
|
|
|
|
- `Comunidad Hispana #01`
|
|
- host/IP: `152.114.195.174`
|
|
- `query_port`: `7778`
|
|
- `game_port`: `7777`
|
|
- `source_name`: `community-hispana-a2s`
|
|
- `external_server_id`: `comunidad-hispana-01`
|
|
|
|
`query_port` es el puerto usado para `A2S_INFO`; `game_port` se conserva por
|
|
separado para documentar el puerto de juego real sin mezclar ambos conceptos en
|
|
la configuracion.
|
|
|
|
El registro por defecto incluye dos targets reales verificados:
|
|
|
|
- `Comunidad Hispana #01`
|
|
- host/IP: `152.114.195.174`
|
|
- `query_port`: `7778`
|
|
- `game_port`: `7777`
|
|
- `external_server_id`: `comunidad-hispana-01`
|
|
- `Comunidad Hispana #02`
|
|
- host/IP: `152.114.195.150`
|
|
- `query_port`: `7878`
|
|
- `game_port`: `7877`
|
|
- `external_server_id`: `comunidad-hispana-02`
|
|
|
|
Si se quiere cambiar la lista sin editar codigo, puede definirse
|
|
`HLL_BACKEND_A2S_TARGETS` como un array JSON:
|
|
|
|
```powershell
|
|
$env:HLL_BACKEND_A2S_TARGETS='[
|
|
{
|
|
"name": "Comunidad Hispana #01",
|
|
"host": "152.114.195.174",
|
|
"query_port": 7778,
|
|
"game_port": 7777,
|
|
"source_name": "community-hispana-a2s",
|
|
"external_server_id": "comunidad-hispana-01",
|
|
"region": "ES"
|
|
},
|
|
{
|
|
"name": "Comunidad Hispana #02",
|
|
"host": "152.114.195.150",
|
|
"query_port": 7878,
|
|
"game_port": 7877,
|
|
"source_name": "community-hispana-a2s",
|
|
"external_server_id": "comunidad-hispana-02",
|
|
"region": "ES"
|
|
}
|
|
]'
|
|
```
|
|
|
|
Cada target soporta:
|
|
|
|
- `name`
|
|
- `host`
|
|
- `query_port`
|
|
- `game_port` opcional
|
|
- `source_name`
|
|
- `external_server_id` opcional
|
|
- `region` opcional
|
|
|
|
El colector puede resolver esos targets con `load_a2s_targets()` o
|
|
`fetch_configured_a2s_probes()` sin depender de constantes dispersas.
|
|
|
|
## Consulta historica minima
|
|
|
|
Una vez existen snapshots persistidos, el backend expone una primera capa de
|
|
consulta historica:
|
|
|
|
- `/api/servers/latest` devuelve el ultimo snapshot conocido por servidor
|
|
- `/api/servers/history` devuelve snapshots recientes agregados
|
|
- `/api/servers/{id}/history` devuelve el historial reciente de un servidor
|
|
|
|
`{id}` acepta el `server_id` numerico interno o el `external_server_id`
|
|
persistido por el colector. El parametro opcional `limit` acepta valores entre
|
|
`1` y `100`.
|
|
|
|
La capa historica propia expone:
|
|
|
|
- `/api/historical/weekly-top-kills`
|
|
- `/api/historical/weekly-leaderboard`
|
|
- `/api/historical/leaderboard`
|
|
- `/api/historical/recent-matches`
|
|
- `/api/historical/server-summary`
|
|
- `/api/historical/snapshots/server-summary`
|
|
- `/api/historical/snapshots/weekly-leaderboard`
|
|
- `/api/historical/snapshots/leaderboard`
|
|
- `/api/historical/snapshots/recent-matches`
|
|
- `/api/historical/player-profile`
|
|
|
|
Parametros opcionales:
|
|
|
|
- `limit` entre `1` y `100`
|
|
- `server` con slug historico como `comunidad-hispana-01`
|
|
- `player` en `/api/historical/player-profile` aceptando `stable_player_key`,
|
|
`steam_id` o `source_player_id`
|
|
|
|
Ademas de los slugs fisicos de cada scoreboard, la capa historica acepta la
|
|
clave logica `all-servers` para devolver agregados globales sobre los tres
|
|
servidores de Comunidad Hispana sin tratarla como un origen CRCON real aparte.
|
|
|
|
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 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`
|
|
- `support`
|
|
- `matches_over_100_kills`
|
|
|
|
El endpoint legacy `/api/historical/weekly-top-kills` se conserva como alias
|
|
compatible para la metrica `kills`.
|
|
|
|
`/api/historical/leaderboard` y `/api/historical/snapshots/leaderboard`
|
|
generalizan ese mismo contrato con `timeframe=weekly|monthly`. Para `monthly`,
|
|
la politica temporal usa el mes natural UTC en curso y hace fallback al mes
|
|
cerrado anterior solo cuando el mes actual todavia no tiene ningun cierre.
|
|
Ambas variantes exponen el rango real usado mediante `window_start`,
|
|
`window_end`, `window_kind`, `window_label` y `selection_reason`.
|
|
|
|
`recent-matches` devuelve cierres recientes por servidor con marcador, mapa y
|
|
conteo de jugadores. `server-summary` agrega volumen historico, jugadores
|
|
unicos, kills, mapas dominantes y rango temporal cubierto. `player-profile`
|
|
deja lista la base de consulta agregada por jugador para futuras vistas.
|
|
|
|
La familia `/api/historical/snapshots/*` lee directamente los archivos JSON
|
|
precalculados bajo `backend/data/snapshots/` y evita recalcular agregados
|
|
pesados en cada request. Estos endpoints devuelven payloads ligeros listos para
|
|
frontend con:
|
|
|
|
- `snapshot_status`
|
|
- `missing_reason`
|
|
- `request_path_policy`
|
|
- `generation_policy`
|
|
- `generated_at`
|
|
- `source_range_start`
|
|
- `source_range_end`
|
|
- `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`
|
|
|
|
Si un snapshot todavia no existe en `backend/data/snapshots/`, la API responde
|
|
rapido con `found: false`, `snapshot_status: "missing"` y
|
|
`missing_reason: "snapshot-not-generated"`. La generacion y refresco de esos
|
|
artefactos debe ocurrir fuera del request path mediante `historical_ingestion`
|
|
o `historical_runner`; la lectura HTTP se mantiene como fast path de solo
|
|
lectura.
|
|
|
|
`/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.
|
|
|
|
La misma capa de snapshots guarda tambien `monthly-leaderboard` por servidor y
|
|
por agregado `all-servers`, con archivos como `monthly-kills.json` y
|
|
`monthly-support.json`.
|
|
|
|
Tambien persiste `monthly-mvp.json` por servidor y para `all-servers`, listo
|
|
para lectura rapida desde `/api/historical/monthly-mvp` y
|
|
`/api/historical/snapshots/monthly-mvp` sin recalculo pesado en request.
|
|
|
|
La misma operativa persiste tambien snapshots V2 de eventos de jugador para el
|
|
ultimo mes con datos disponible por servidor y para `all-servers`, listos para
|
|
lectura rapida sin consultas pesadas on-demand:
|
|
|
|
- `player-events-most-killed.json`
|
|
- `player-events-death-by.json`
|
|
- `player-events-duels.json`
|
|
- `player-events-weapon-kills.json`
|
|
- `player-events-teamkills.json`
|
|
|
|
Los endpoints `/api/historical/player-events` y
|
|
`/api/historical/snapshots/player-events` aceptan:
|
|
|
|
- `view=most-killed`
|
|
- `view=death-by`
|
|
- `view=duels`
|
|
- `view=weapon-kills`
|
|
- `view=teamkills`
|
|
|
|
La respuesta expone metadata operativa alineada con el resto de snapshots:
|
|
|
|
- `generated_at`
|
|
- `month_key`
|
|
- `source_range_start`
|
|
- `source_range_end`
|
|
- `found`
|
|
- `is_stale`
|
|
|
|
El backend incluye ademas el calculo interno de `monthly MVP V1` en
|
|
`app/monthly_mvp.py`, separado de los leaderboards mensuales simples por
|
|
metrica. Ese calculo:
|
|
|
|
- usa solo `kills`, `support`, `time_seconds`, `deaths` y `teamkills`
|
|
persistidos
|
|
- recompone `KPM` y `KDA` desde totales mensuales
|
|
- aplica elegibilidad minima de `6` partidas cerradas y `6` horas
|
|
- soporta servidor individual y el agregado logico `all-servers`
|
|
|
|
En esta fase el ranking MVP queda listo para serializar en snapshots o payloads
|
|
sin reemplazar los leaderboards mensuales ya existentes por `kills`, `deaths`,
|
|
`support` y `matches_over_100_kills`.
|
|
|
|
La repo incluye tambien un calculo backend separado de `monthly MVP V2` en
|
|
`app/monthly_mvp_v2.py`, expuesto de momento por
|
|
`/api/historical/monthly-mvp-v2`.
|
|
|
|
Esta V2:
|
|
|
|
- convive sin reemplazar `monthly MVP V1`
|
|
- reusa la misma ventana mensual y la misma elegibilidad base
|
|
- anade `rivalry_edge` y `duel_control` derivados del ledger V2 de eventos
|
|
- aplica una penalizacion de teamkills mas estricta
|
|
- mantiene fuera del score el peso por arma o tipo de kill hasta validar mejor
|
|
esas senales
|
|
|
|
Esa capacidad V2 se persiste tambien en snapshots dedicados
|
|
`monthly-mvp-v2.json` por servidor y para `all-servers`, leidos por:
|
|
|
|
- `/api/historical/monthly-mvp-v2`
|
|
- `/api/historical/snapshots/monthly-mvp-v2`
|
|
|
|
La lectura HTTP de V2 sigue asi la misma politica de fast path de solo lectura
|
|
que el resto de snapshots historicos, con metadata util como:
|
|
|
|
- `generated_at`
|
|
- `month_key`
|
|
- `found`
|
|
- `source_range_start`
|
|
- `source_range_end`
|
|
- `event_coverage`
|
|
|
|
## Ingesta historica CRCON
|
|
|
|
La ingesta historica no usa A2S ni scraping del HTML de `/games`. Consume la
|
|
capa JSON publica detectada en los scoreboards CRCON de Comunidad Hispana y
|
|
persiste el resultado en las tablas `historical_*`.
|
|
|
|
Fuentes configuradas:
|
|
|
|
- `https://scoreboard.comunidadhll.es`
|
|
- `https://scoreboard.comunidadhll.es:5443`
|
|
- `https://scoreboard.comunidadhll.es:3443`
|
|
|
|
Comandos manuales desde `backend/`:
|
|
|
|
```powershell
|
|
python -m app.historical_ingestion bootstrap
|
|
python -m app.historical_ingestion refresh
|
|
python -m app.historical_runner --interval 1800
|
|
```
|
|
|
|
Los mismos flujos desde Docker Compose:
|
|
|
|
```powershell
|
|
docker compose exec backend python -m app.historical_ingestion bootstrap
|
|
docker compose exec backend python -m app.historical_ingestion refresh
|
|
docker compose exec backend python -m app.historical_runner --interval 1800
|
|
```
|
|
|
|
Flags utiles:
|
|
|
|
- `--server comunidad-hispana-01` para limitar a un servidor
|
|
- `--server comunidad-hispana-02` para validar solo el segundo servidor activo
|
|
- `--overlap-hours 48` para releer una ventana reciente mayor sin relanzar bootstrap
|
|
- `--max-pages 2` para validacion local acotada
|
|
- `--page-size 25` para ajustar paginacion
|
|
- `--start-page 4` para forzar una pagina concreta en bootstraps largos
|
|
- `--detail-workers 16` para paralelizar el detalle por partida
|
|
|
|
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. 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.
|
|
Si la recomposicion se lanza para un servidor fisico concreto, el backend
|
|
rehace tambien el agregado logico `all-servers` para mantener `Todos`
|
|
alineado con `#01` y `#02` aunque `#03` siga sin bootstrap.
|
|
|
|
En esta fase, el comando muestra progreso operativo util sin saturar stdout:
|
|
|
|
- intento primario RCON
|
|
- fuente finalmente seleccionada
|
|
- servidor actual
|
|
- pagina actual
|
|
- `match_ids_to_detail` de cada pagina
|
|
|
|
Si RCON no puede cubrir la operacion competitiva real, el fallback a
|
|
`public-scoreboard` queda visible tanto durante la ejecucion como en el JSON
|
|
final mediante:
|
|
|
|
- `primary_source`
|
|
- `selected_source`
|
|
- `fallback_used`
|
|
- `fallback_reason`
|
|
- `source_attempts`
|
|
- `primary_writer_result`
|
|
|
|
El comando devuelve ademas un resumen de cobertura persistida por servidor. Esto
|
|
ayuda a validar rapidamente cuantos matches reales quedaron importados, el rango
|
|
temporal cubierto y si la carga ya supera la ultima semana movil que usa la UI.
|
|
Ese resumen incluye tambien checkpoint y estado operativo de backfill por
|
|
servidor:
|
|
|
|
- `next_page`
|
|
- `last_completed_page`
|
|
- `discovered_total_matches`
|
|
- `discovered_total_pages`
|
|
- `archive_exhausted`
|
|
- `last_run`
|
|
|
|
Como la fuente CRCON publica expone un archivo muy profundo y puede devolver
|
|
errores `502` intermitentes bajo carga sostenida, el bootstrap completo debe
|
|
tratarse como una operacion reanudable. Flujo recomendado:
|
|
|
|
```powershell
|
|
python -m app.historical_ingestion bootstrap --detail-workers 16
|
|
python -m app.historical_ingestion bootstrap --detail-workers 16
|
|
```
|
|
|
|
La segunda invocacion reutiliza automaticamente el checkpoint persistido en
|
|
`historical_backfill_progress` y continua desde la siguiente pagina pendiente si
|
|
la sesion anterior se corta por tiempo disponible o por inestabilidad puntual
|
|
del origen. `--start-page` queda como override manual cuando se quiera
|
|
reprocesar o inspeccionar un tramo concreto.
|
|
|
|
Runbook operativo para overlap manual:
|
|
|
|
```powershell
|
|
python -m app.historical_ingestion refresh --overlap-hours 48
|
|
python -m app.historical_ingestion refresh --server comunidad-hispana-01 --overlap-hours 48 --max-pages 2
|
|
python -m app.historical_runner --max-runs 1
|
|
```
|
|
|
|
La primera pasada relee 48 horas sobre los tres servidores historicos ya
|
|
registrados. La segunda sirve para validar un solo servidor con alcance
|
|
acotado. La tercera recompone snapshots despues de una pasada manual cuando se
|
|
quiere confirmar que la capa precalculada vuelve a quedar alineada.
|
|
|
|
Interpretacion operativa recomendada:
|
|
|
|
- si aparece `historical-ingestion-rcon-primary-succeeded`, RCON se intento de
|
|
verdad primero y la captura prospectiva quedo registrada
|
|
- si despues `selected_source` termina en `public-scoreboard`, eso significa
|
|
que la reconstruccion del archivo competitivo `historical_*` siguio
|
|
necesitando fallback clasico
|
|
- si RCON falla por red, auth o timeout, el motivo queda visible en
|
|
`fallback_reason` y en `source_attempts`
|
|
|
|
Los reintentos de cada request JSON pueden ajustarse sin tocar codigo con:
|
|
|
|
- `HLL_HISTORICAL_CRCON_REQUEST_RETRIES`
|
|
- `HLL_HISTORICAL_CRCON_RETRY_DELAY_SECONDS`
|
|
|
|
El runner `python -m app.historical_runner` deja ahora una orquestacion
|
|
RCON-first lista para ejecucion local repetida sin depender de infraestructura
|
|
externa y mantiene calientes los snapshots historicos mas visibles cuando el
|
|
fallback clasico entra de forma controlada. Por defecto:
|
|
|
|
- intenta primero una captura prospectiva RCON en cada ciclo
|
|
- solo lanza el refresh historico clasico cuando RCON falla, cuando se pide un
|
|
scope manual que sigue requiriendo cobertura competitiva, o en la cadencia
|
|
periodica de fallback para mantener rankings y snapshots clasicos
|
|
- refresca cada `900` segundos
|
|
- prewarmea en cada ciclo:
|
|
- `server-summary` para `comunidad-hispana-01`, `comunidad-hispana-02` y `all-servers`
|
|
- `weekly-leaderboard` de la metrica por defecto `kills` para esos mismos alcances
|
|
- `monthly-leaderboard` de la metrica por defecto `kills` para esos mismos alcances
|
|
- `recent-matches` para esos mismos alcances
|
|
- recompone la matriz completa de snapshots cada `4` ciclos para mantener el resto de metricas al dia sin penalizar todos los refresh
|
|
- reintenta hasta `2` veces tras un fallo
|
|
- espera `30` segundos entre reintentos
|
|
- reutiliza el registro de `historical_ingestion_runs` para dejar trazabilidad
|
|
de ultimo refresh, resultado y errores basicos
|
|
- persiste por servidor:
|
|
- `server-summary`
|
|
- `weekly-leaderboard` para `kills`, `deaths`, `support` y `matches_over_100_kills`
|
|
- `monthly-leaderboard` para `kills`, `deaths`, `support` y `matches_over_100_kills`
|
|
- `recent-matches`
|
|
|
|
Flags utiles del runner:
|
|
|
|
- `--server comunidad-hispana-01` para limitar a un servidor
|
|
- `--interval 900` para fijar la frecuencia recomendada de snapshots
|
|
- `--hourly` para fijar directamente un ciclo horario de `3600` segundos
|
|
- `--retries 1` para reducir reintentos
|
|
- `--retry-delay 10` para bajar la espera entre fallos
|
|
- `--max-runs 1` para una validacion puntual sin bucle indefinido
|
|
|
|
Para dejar automatizado el refresh historico horario en local, el comando
|
|
avanzado sigue disponible:
|
|
|
|
```powershell
|
|
python -m app.historical_runner --hourly
|
|
```
|
|
|
|
Sin `--server`, ese runner refresca:
|
|
|
|
- `comunidad-hispana-01`
|
|
- `comunidad-hispana-02`
|
|
|
|
Despues de cada fallback clasico correcto, recompone snapshots para los
|
|
servidores afectados y vuelve a alinear el agregado `all-servers`. Si el ciclo
|
|
RCON primario fue suficiente y no hizo falta el fallback clasico, el runner
|
|
deja constancia explicita de ese motivo en su salida JSON.
|
|
|
|
Para regenerar snapshots de forma puntual dentro del contenedor sin dejar un
|
|
bucle permanente, la validacion operativa minima es:
|
|
|
|
```powershell
|
|
docker compose exec backend python -m app.historical_runner --max-runs 1
|
|
```
|
|
|
|
Operativa local minima:
|
|
|
|
1. Desde `backend/`, arrancar la API con `python -m app.main`.
|
|
2. En otra terminal, dejar corriendo `python -m app.historical_runner --hourly`.
|
|
3. Verificar el proceso revisando la salida del runner: al arrancar imprime un
|
|
bloque JSON con `event: "historical-refresh-loop-started"`, `server_scope`
|
|
y `snapshot_scope`.
|
|
4. Confirmar que los snapshots siguen actualizandose revisando `generated_at`
|
|
en archivos bajo `backend/data/snapshots/`, por ejemplo:
|
|
- `backend/data/snapshots/comunidad-hispana-01/server-summary.json`
|
|
- `backend/data/snapshots/comunidad-hispana-02/recent-matches.json`
|
|
- `backend/data/snapshots/comunidad-hispana-02/weekly-kills.json`
|
|
- `backend/data/snapshots/all-servers/monthly-kills.json`
|
|
|
|
Operativa avanzada con Docker Compose:
|
|
|
|
```powershell
|
|
docker compose --profile advanced up -d backend historical-runner frontend
|
|
```
|
|
|
|
El servicio `historical-runner` usa el mismo volumen persistente `./backend/data`
|
|
y ejecuta `python -m app.historical_runner --hourly` como bucle operativo
|
|
dedicado, sin mezclar el scheduler con el proceso HTTP principal. No forma
|
|
parte del despliegue normal, que queda limitado a `backend` + `frontend`.
|
|
|
|
En frontend, la landing ya no arranca con cards fake estaticas para servidores:
|
|
|
|
- el contenedor queda en estado de loading
|
|
- solo se renderizan cards con datos reales al hidratar
|
|
- si la API falla, se muestra una degradacion limpia en lugar de datos falsos
|
|
|
|
## Coordinacion single-writer para automatizaciones y CLI
|
|
|
|
Todos los procesos writer-oriented que comparten el mismo SQLite usan ahora un
|
|
lock comun derivado de `HLL_BACKEND_STORAGE_PATH` y persistido junto al volumen
|
|
de datos compartido. Ese lock coordina:
|
|
|
|
- `app.historical_ingestion`
|
|
- `app.historical_runner`
|
|
- `app.player_event_worker`
|
|
- `app.rcon_historical_worker`
|
|
|
|
Rutas HTTP read-only como `/api/historical/snapshots/*`, `/api/servers` en modo
|
|
cache local y el read model minimo RCON no adquieren este lock.
|
|
|
|
Variables operativas:
|
|
|
|
- `HLL_BACKEND_WRITER_LOCK_TIMEOUT_SECONDS`
|
|
- `HLL_BACKEND_WRITER_LOCK_POLL_INTERVAL_SECONDS`
|
|
|
|
Comportamiento:
|
|
|
|
- si un writer ya esta ejecutandose, el siguiente espera de forma controlada
|
|
hasta agotar el timeout configurado
|
|
- si no puede adquirir el lock, falla con un error claro indicando:
|
|
- lock path
|
|
- holder
|
|
- `started_at`
|
|
- host
|
|
- pid
|
|
- si el lock parece venir de un contenedor Docker ya parado, el backend puede
|
|
recuperarlo automaticamente cuando:
|
|
- el holder venia de un cwd tipo `/app`
|
|
- el lock ya supero una gracia minima de seguridad
|
|
- la coordinacion principal es este single-writer lock; WAL y `busy_timeout`
|
|
quedan como endurecimiento complementario, no como solucion unica
|
|
|
|
Runbook minimo:
|
|
|
|
- pasada manual del historico base mientras el runner automatico existe:
|
|
|
|
```powershell
|
|
docker compose exec backend python -m app.historical_ingestion refresh --overlap-hours 48
|
|
```
|
|
|
|
Si el lock esta ocupado, el comando esperara hasta el timeout configurado y,
|
|
si no se libera, terminara con un mensaje claro de lock ocupado.
|
|
|
|
- pasada manual de player-events:
|
|
|
|
```powershell
|
|
docker compose exec backend python -m app.player_event_worker refresh --overlap-hours 48
|
|
```
|
|
|
|
- pasada manual de captura prospectiva RCON:
|
|
|
|
```powershell
|
|
docker compose exec backend python -m app.rcon_historical_worker capture
|
|
```
|
|
|
|
- convivencia recomendada con automatizaciones:
|
|
- no hace falta parar contenedores por defecto
|
|
- dejar que el lock coordine la exclusión mutua
|
|
- usar `--max-runs 1` o comandos manuales puntuales cuando se quiera una
|
|
pasada controlada
|
|
|
|
Comprobaciones utiles con Compose:
|
|
|
|
- `docker compose ps historical-runner`
|
|
- `docker compose logs -f historical-runner`
|
|
- `docker compose exec backend python -m app.historical_runner --max-runs 1`
|
|
|
|
Compose para captura prospectiva RCON:
|
|
|
|
```powershell
|
|
docker compose --profile advanced up -d rcon-historical-worker
|
|
docker compose logs -f rcon-historical-worker
|
|
docker compose exec backend python -m app.rcon_historical_worker capture
|
|
```
|
|
|
|
Variables utiles del runner:
|
|
|
|
- `HLL_HISTORICAL_SNAPSHOT_REFRESH_INTERVAL_SECONDS`
|
|
- `HLL_HISTORICAL_FULL_SNAPSHOT_EVERY_RUNS`
|
|
- `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:
|
|
|
|
- prioriza `steaminfo.profile.steamid` cuando existe
|
|
- si `player_id` ya parece un SteamID real, lo promueve igualmente a `steam:*`
|
|
- si no hay SteamID, usa `player_id` como clave `crcon-player:*`
|
|
- deja `steaminfo.id` como ultimo fallback cuando faltan las claves anteriores
|
|
|
|
La misma inicializacion fusiona filas duplicadas si una partida abierta quedo
|
|
guardada con un id sintetico y mas tarde CRCON la expone con un id numerico
|
|
definitivo. Esto evita que el ranking semanal cuente dos veces la misma sesion.
|
|
|
|
## CORS local minimo
|
|
|
|
El backend responde con `Access-Control-Allow-Origin` solo si la peticion llega
|
|
desde uno de los origenes permitidos en desarrollo local. No se habilita un
|
|
comodin global ni configuracion de produccion en esta fase.
|
|
|
|
La allowlist por defecto cubre `file://` mediante el origen `null` y los flujos
|
|
locales mas comunes del proyecto:
|
|
|
|
- `http://127.0.0.1:5500`
|
|
- `http://localhost:5500`
|
|
- `http://127.0.0.1`
|
|
- `http://127.0.0.1:8080`
|
|
- `http://localhost`
|
|
- `http://localhost:8080`
|
|
|
|
Las respuestas `GET` y `OPTIONS` incluyen `Access-Control-Allow-Origin` cuando
|
|
el origen esta permitido, suficiente para probar la landing contra la API local
|
|
sin tocar endpoints ni payloads.
|
|
|
|
Esta separacion mantiene el backend simple y deja una base clara para futuras tasks sin introducir integraciones reales todavia.
|
|
|
|
## Fuente y ledger de eventos de jugador V2
|
|
|
|
La repo incluye ahora una primera base V2 separada del historico `historical_*`
|
|
para preparar metricas avanzadas de duelos, armas y teamkills sin tocar todavia
|
|
la UI ni el scoring final.
|
|
|
|
Fuente minima elegida en esta fase:
|
|
|
|
- detalle de partida `GET /api/get_map_scoreboard?map_id={id}` del scoreboard CRCON
|
|
|
|
Importante:
|
|
|
|
- esta fuente no es un feed raw por kill
|
|
- el adaptador actual normaliza solo senales parciales ya visibles en el
|
|
resumen de partida:
|
|
- `most_killed`
|
|
- `death_by`
|
|
- `weapons`
|
|
- `death_by_weapons`
|
|
- `teamkills`
|
|
- `occurred_at` usa el timestamp de cierre o inicio de la partida, no el
|
|
instante exacto del kill
|
|
- el ledger raw es append-only y deduplica por `event_id`
|
|
- la persistencia queda separada de `historical_matches` y
|
|
`historical_player_match_stats` aunque comparte el mismo SQLite de desarrollo
|
|
|
|
Contrato minimo normalizado por evento:
|
|
|
|
- `event_id`
|
|
- `event_type`
|
|
- `occurred_at`
|
|
- `server_slug`
|
|
- `external_match_id`
|
|
- `source_kind`
|
|
- `source_ref`
|
|
- `killer_player_key`
|
|
- `victim_player_key`
|
|
- `weapon_name`
|
|
- `kill_category`
|
|
- `is_teamkill`
|
|
- `event_value`
|
|
|
|
Tablas nuevas:
|
|
|
|
- `player_event_raw_ledger`
|
|
- `player_event_ingestion_runs`
|
|
- `player_event_backfill_progress`
|
|
|
|
Comandos manuales desde `backend/`:
|
|
|
|
```powershell
|
|
python -m app.player_event_worker refresh
|
|
python -m app.player_event_worker refresh --server comunidad-hispana-01 --max-pages 1
|
|
python -m app.player_event_worker loop --interval 1800
|
|
```
|
|
|
|
Variables opcionales del worker:
|
|
|
|
- `HLL_PLAYER_EVENT_REFRESH_INTERVAL_SECONDS`
|
|
- `HLL_PLAYER_EVENT_REFRESH_OVERLAP_HOURS`
|
|
- `HLL_PLAYER_EVENT_REFRESH_MAX_RETRIES`
|
|
- `HLL_PLAYER_EVENT_REFRESH_RETRY_DELAY_SECONDS`
|
|
|
|
Flags utiles del worker:
|
|
|
|
- `--server comunidad-hispana-01` para validar un solo servidor
|
|
- `--overlap-hours 48` para releer una ventana reciente mayor
|
|
- `--max-pages 1` para una comprobacion acotada
|
|
|
|
Ejemplos operativos:
|
|
|
|
```powershell
|
|
python -m app.player_event_worker refresh --overlap-hours 48
|
|
python -m app.player_event_worker refresh --server comunidad-hispana-01 --overlap-hours 48 --max-pages 1
|
|
```
|
|
|
|
Politica operativa minima:
|
|
|
|
- el worker corre fuera del request path HTTP
|
|
- reusa la capa historica `public-scoreboard` solo como fuente de detalle
|
|
- persiste checkpoints por servidor y pagina
|
|
- la reejecucion es segura porque el ledger usa insercion idempotente por
|
|
`event_id`
|
|
|
|
Agregados V2 ya disponibles desde codigo:
|
|
|
|
- `list_most_killed()`
|
|
- `list_death_by()`
|
|
- `list_net_duel_summaries()`
|
|
- `list_weapon_kills()`
|
|
- `list_teamkill_summaries()`
|
|
|
|
Limitaciones actuales de esta fase:
|
|
|
|
- no existe todavia un ledger raw por kill individual
|
|
- los agregados de duelos y armas son parciales, porque dependen del mejor
|
|
resumen disponible por jugador en CRCON y no de todos los encounters del match
|
|
- la V2 no expone aun endpoints HTTP ni snapshots propios
|
|
|
|
## Historical Runtime Policy
|
|
|
|
El backend queda orientado a `RCON-first` tambien para historico:
|
|
|
|
- live:
|
|
- `rcon` primero
|
|
- `a2s` solo como fallback
|
|
- historico:
|
|
- `rcon` primero tanto para lectura minima como para el writer path primario
|
|
de `historical_ingestion`
|
|
- `public-scoreboard` solo como fallback cuando RCON no cubre una operacion
|
|
competitiva concreta, no tiene cobertura suficiente o falla la captura
|
|
primaria
|
|
|
|
Metadata observable en payloads historicos:
|
|
|
|
- `primary_source`
|
|
- `selected_source`
|
|
- `fallback_used`
|
|
- `fallback_reason`
|
|
- `source_attempts`
|
|
|
|
Estado real a fecha de esta fase:
|
|
|
|
- el read model historico RCON soporta ya una capa competitiva primaria basada
|
|
en ventanas derivadas desde persistencia `rcon_historical_*`
|
|
- `server-summary` y `recent-matches` pasan a usar esa capa RCON-backed como
|
|
camino principal real
|
|
- en runtime, esas dos rutas solo se sirven como `rcon` cuando la capability
|
|
sigue soportada y existe cobertura RCON util para el scope pedido
|
|
- cobertura util en esta frontera significa:
|
|
- `server-summary`: al menos una fila con `coverage.status != "empty"` y
|
|
`window_count` o `sample_count` mayor que cero
|
|
- `recent-matches`: al menos una ventana con `match_id`, `closed_at` y
|
|
`sample_count > 0`
|
|
- si el target persistido quedo con clave legacy `rcon:<host>:<port>` pero el
|
|
runtime actual ya conoce su `external_server_id`, la capa read model intenta
|
|
resolver ambos aliases antes de caer a fallback
|
|
- si no hay coverage suficiente o la lectura RCON falla, el backend mantiene
|
|
fallback explicito a `public-scoreboard` con `fallback_used = true` y
|
|
`fallback_reason` visible
|
|
- `historical_ingestion` intenta primero una captura writer-oriented por RCON y
|
|
deja esa tentativa visible en su salida
|
|
- leaderboards semanales/mensuales, MVP V1/V2 y player-events siguen teniendo
|
|
fallback a `public-scoreboard` mientras RCON no disponga de señal competitiva
|
|
por jugador con paridad suficiente
|
|
- Elo/MMR permanece pausado y desacoplado del arranque del backend; cuando se
|
|
reactive mediante una task explicita, debera respetar el contexto
|
|
RCON-backed primario y usar `public-scoreboard` solo como suplemento/fallback
|
|
para estadisticas por jugador sin paridad RCON
|
|
|
|
## PostgreSQL Phase 2 Displayed Data Migration
|
|
|
|
Cuando `HLL_BACKEND_DATABASE_URL` esta configurado, los endpoints visibles de
|
|
historico y el cache mostrado por `/api/servers` leen PostgreSQL. SQLite y los
|
|
JSON legacy quedan como fuente de migracion o fixture explicito con `db_path`.
|
|
|
|
Migracion idempotente:
|
|
|
|
```powershell
|
|
cd backend
|
|
python -m app.sqlite_to_postgres_migration
|
|
python -m app.storage_diagnostics
|
|
```
|
|
|
|
La salida JSON de `sqlite_to_postgres_migration` lista rutas fuente, dominios y
|
|
tablas migradas, filas leidas, insertadas, actualizadas, omitidas y errores.
|
|
La migracion conserva `external_match_id`, IDs legacy y `match_key` RCON para
|
|
que URLs de detalle existentes sigan resolviendo. Tambien copia candidatos y
|
|
URLs seguras de scoreboard; no vuelve a activar filas visibles de
|
|
`comunidad-hispana-03`.
|
|
|
|
Paridad minima a revisar en `storage_diagnostics`:
|
|
|
|
- `admin_log_events`, `materialized_matches`, `player_stats`
|
|
- `public_scoreboard_historical_matches`
|
|
- fuentes de rankings semanales y mensuales
|
|
- `server_summary_cache`, `server_snapshots`, `player_event_ledger`
|
|
- `scoreboard_candidates`
|
|
- ultimas partidas materializadas y ultimos eventos AdminLog `match_end`
|
|
|
|
Fuera de phase 2 quedan checkpoints/runs de ingesta publica que no se muestran
|
|
en frontend y Elo/MMR pausado. Si un endpoint de mantenimiento recibe un
|
|
`db_path` explicito, sigue trabajando contra SQLite para migracion, tests o
|
|
compatibilidad operativa controlada.
|
|
|
|
## Elo/MMR Monthly Ranking
|
|
|
|
Se añade una primera base operativa inspirada en el documento
|
|
`sistema_elo_mensual_hll.pdf`, pero adaptada a la telemetria real disponible.
|
|
|
|
Superficies nuevas:
|
|
|
|
- `python -m app.elo_mmr_engine rebuild`
|
|
- `python -m app.elo_mmr_engine leaderboard --server all-servers --limit 10`
|
|
- `python -m app.elo_mmr_engine player --server all-servers --player <stable_player_key>`
|
|
- `/api/historical/elo-mmr/leaderboard`
|
|
- `/api/historical/elo-mmr/player`
|
|
|
|
Persistencia nueva en SQLite:
|
|
|
|
- `elo_mmr_player_ratings`
|
|
- `elo_mmr_match_results`
|
|
- `elo_mmr_monthly_rankings`
|
|
- `elo_mmr_monthly_checkpoints`
|
|
|
|
Politica de exactitud:
|
|
|
|
- `exact`: outcome, combat, utility, disciplina por teamkills, MMR persistente
|
|
- `approximate`: role bucket, objective index, strength of schedule
|
|
- `not_available`: leadership y tacticas finas no persistidas
|
|
|
|
Cuando `historical_data_source=rcon`, el motor Elo/MMR deja visible una
|
|
frontera hibrida y honesta:
|
|
|
|
- `primary_source = rcon`
|
|
- `selected_source = hybrid-rcon-competitive-plus-public-scoreboard`
|
|
- `fallback_used = true`
|
|
|
|
Eso significa que la capa RCON-backed ya aporta el contexto competitivo de
|
|
cobertura y calidad de match, pero las estadisticas competitivas por jugador
|
|
siguen necesitando el suplemento clasico hasta que RCON tenga esa granularidad.
|
|
|
|
La especificacion detallada y el mapa de capabilities quedan en:
|
|
|
|
- `docs/elo-mmr-monthly-ranking-design.md`
|
|
|
|
## Alcance
|
|
|
|
Esta fase no implementa:
|
|
|
|
- logica real de Discord
|
|
- integraciones con servidores de juego
|
|
- base de datos
|
|
- autenticacion
|
|
- dependencias nuevas
|
|
|
|
La idea es dejar un esqueleto funcional, pequeno y coherente con `docs/frontend-backend-contract.md`.
|