58 KiB
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
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:
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_HOSTHLL_BACKEND_PORTHLL_BACKEND_ALLOWED_ORIGINSHLL_BACKEND_REFRESH_INTERVAL_SECONDSHLL_BACKEND_LIVE_DATA_SOURCEHLL_BACKEND_HISTORICAL_DATA_SOURCEHLL_BACKEND_RCON_TIMEOUT_SECONDSHLL_BACKEND_RCON_TARGETSHLL_RCON_HISTORICAL_CAPTURE_INTERVAL_SECONDSHLL_RCON_HISTORICAL_CAPTURE_MAX_RETRIESHLL_RCON_HISTORICAL_CAPTURE_RETRY_DELAY_SECONDSHLL_RCON_BACKFILL_CHUNK_HOURSHLL_RCON_BACKFILL_SLEEP_SECONDSHLL_RCON_BACKFILL_MAX_DAYS_BACKHLL_BACKEND_RCON_ADMIN_LOG_LOOKBACK_MINUTESHLL_BACKEND_SQLITE_WRITER_TIMEOUT_SECONDSHLL_BACKEND_SQLITE_BUSY_TIMEOUT_MSHLL_BACKEND_WRITER_LOCK_TIMEOUT_SECONDSHLL_BACKEND_WRITER_LOCK_POLL_INTERVAL_SECONDSHLL_HISTORICAL_CRCON_PAGE_SIZEHLL_HISTORICAL_CRCON_TIMEOUT_SECONDSHLL_HISTORICAL_CRCON_DETAIL_WORKERSHLL_HISTORICAL_CRCON_REQUEST_RETRIESHLL_HISTORICAL_CRCON_RETRY_DELAY_SECONDSHLL_HISTORICAL_REFRESH_INTERVAL_SECONDSHLL_HISTORICAL_REFRESH_OVERLAP_HOURSHLL_HISTORICAL_SNAPSHOT_REFRESH_INTERVAL_SECONDSHLL_HISTORICAL_FULL_SNAPSHOT_EVERY_RUNSHLL_HISTORICAL_REFRESH_MAX_RETRIESHLL_HISTORICAL_REFRESH_RETRY_DELAY_SECONDSHLL_BACKEND_SQLITE_WRITER_TIMEOUT_SECONDSHLL_BACKEND_SQLITE_BUSY_TIMEOUT_MSHLL_BACKEND_WRITER_LOCK_TIMEOUT_SECONDSHLL_BACKEND_WRITER_LOCK_POLL_INTERVAL_SECONDSHLL_HISTORICAL_WEEKLY_FALLBACK_MIN_MATCHESHLL_HISTORICAL_WEEKLY_FALLBACK_MAX_WEEKDAY
Variables especialmente relevantes para Docker y Compose:
HLL_BACKEND_HOSTHLL_BACKEND_PORTHLL_BACKEND_DATABASE_URLHLL_BACKEND_STORAGE_PATHHLL_BACKEND_ALLOWED_ORIGINSHLL_BACKEND_LIVE_DATA_SOURCEHLL_BACKEND_HISTORICAL_DATA_SOURCEHLL_BACKEND_RCON_TIMEOUT_SECONDSHLL_BACKEND_RCON_TARGETSHLL_HISTORICAL_CRCON_PAGE_SIZEHLL_HISTORICAL_CRCON_TIMEOUT_SECONDSHLL_HISTORICAL_CRCON_DETAIL_WORKERSHLL_HISTORICAL_CRCON_REQUEST_RETRIESHLL_HISTORICAL_CRCON_RETRY_DELAY_SECONDSHLL_HISTORICAL_REFRESH_OVERLAP_HOURSHLL_HISTORICAL_SNAPSHOT_REFRESH_INTERVAL_SECONDSHLL_HISTORICAL_FULL_SNAPSHOT_EVERY_RUNSHLL_HISTORICAL_REFRESH_MAX_RETRIESHLL_HISTORICAL_REFRESH_RETRY_DELAY_SECONDS
Para ejecucion containerizada, el repositorio incluye tambien:
backend/Dockerfilebackend/.dockerignorebackend/.env.example
El contenedor usa el mismo entrypoint real del proyecto:
python -m app.main
Dentro del contenedor arranca por defecto con:
HLL_BACKEND_HOST=0.0.0.0HLL_BACKEND_PORT=8000HLL_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:
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:
docker build -t hll-vietnam-backend ./backend
Ejecucion local con persistencia bind-mounted:
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:
nullhttp://127.0.0.1http://127.0.0.1:5500http://127.0.0.1:8080http://localhosthttp://localhost:5500http://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:
-
En una terminal, desde
backend/, arrancar el backend:python -m app.main -
En otra terminal, desde
frontend/, servir la landing:python -m http.server 8080 -
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 /healthGET /api/communityGET /api/trailerGET /api/discordGET /api/serversGET /api/servers/latestGET /api/servers/history?limit=20GET /api/servers/{id}/history?limit=20GET /api/historical/weekly-top-kills?limit=10&server=comunidad-hispana-01GET /api/historical/weekly-leaderboard?metric=kills&limit=10&server=comunidad-hispana-01GET /api/historical/leaderboard?timeframe=monthly&metric=kills&limit=10&server=comunidad-hispana-01GET /api/historical/monthly-mvp?limit=10&server=comunidad-hispana-01GET /api/historical/monthly-mvp-v2?limit=10&server=comunidad-hispana-01GET /api/historical/player-events?view=most-killed&limit=10&server=comunidad-hispana-01GET /api/historical/recent-matches?limit=20&server=comunidad-hispana-01GET /api/historical/server-summary?server=comunidad-hispana-01GET /api/historical/snapshots/server-summary?server=comunidad-hispana-01GET /api/historical/snapshots/weekly-leaderboard?metric=kills&limit=10&server=comunidad-hispana-01GET /api/historical/snapshots/leaderboard?timeframe=monthly&metric=kills&limit=10&server=comunidad-hispana-01GET /api/historical/snapshots/monthly-mvp?limit=10&server=comunidad-hispana-01GET /api/historical/snapshots/monthly-mvp-v2?limit=10&server=comunidad-hispana-01GET /api/historical/snapshots/player-events?view=most-killed&limit=10&server=comunidad-hispana-01GET /api/historical/snapshots/recent-matches?limit=6&server=comunidad-hispana-01GET /api/historical/player-profile?player=steam%3A76561198000000000
GET /health expone tambien:
live_data_sourcehistorical_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_atsnapshot_age_secondssnapshot_age_minutesmax_snapshot_age_secondsis_stalefreshnesssourcerefresh_attemptedrefresh_statusprimary_sourceselected_sourcefallback_usedfallback_reasonsource_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_SOURCEHLL_BACKEND_HISTORICAL_DATA_SOURCE
Valores soportados en esta fase:
- live:
rconcomo camino primario recomendadoa2scomo fallback legacy o override explicito
- historico:
rconcomo camino primario recomendado para captura y writer path primariopublic-scoreboardcomo fallback legacy controlado
Defaults actuales:
HLL_BACKEND_LIVE_DATA_SOURCE=rconHLL_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 porpayloads.pycuando/api/serversnecesita un refresh realget_historical_data_source()entrega el proveedor usado porhistorical_ingestion.pypara bootstrap y refresh incrementalproviders/public_scoreboard_provider.pyencapsula la semantica actual del scoreboard/CRCON publico bajo el contrato historicoproviders/rcon_provider.pyencapsula el proveedor live basado en comandos RCON HLL v2 medianteServerConnect,LoginyGetServerInformation
Proveedores operativos en esta fase:
- live
rcon - live
a2s - historico
rconsolo para read model minimo y captura prospectiva - historico
public-scoreboardcomo 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-scoreboardsolo 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.pyintenta primero el writer path prospectivo RCON- la persistencia competitiva
historical_*sigue necesitando fallback apublic-scoreboardmientras 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_SECONDSHLL_BACKEND_RCON_TARGETS
Variables especificas de captura historica prospectiva RCON:
HLL_RCON_HISTORICAL_CAPTURE_INTERVAL_SECONDSHLL_RCON_HISTORICAL_CAPTURE_MAX_RETRIESHLL_RCON_HISTORICAL_CAPTURE_RETRY_DELAY_SECONDS
HLL_BACKEND_RCON_TARGETS acepta un array JSON con:
nameslugopcional como alias legacyhostportpasswordexternal_server_idopcionalregionopcionalgame_portopcionalquery_portopcionalsource_nameopcional
Compatibilidad operativa del loader:
- si llega
slugpero noexternal_server_id, el backend reutilizaslugcomoexternal_server_id - si falta
namepero existeslug, el backend genera un nombre razonable a partir del slug en vez de dejarUnnamed 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_connectserver_connect_requestserver_connect_responsexor_key_decodelogin_requestlogin_responseget_server_information_requestget_server_information_responsepayload_decodeunexpected_responsetimeout
- 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:
$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=rconHLL_BACKEND_HISTORICAL_DATA_SOURCE=rcon- usar solo
comunidad-hispana-01ycomunidad-hispana-02en los targets RCON por defecto
- modo historico/RCON avanzado:
- iniciar workers solo de forma explicita
- no reintroducir
comunidad-hispana-03salvo validacion nueva
- override legacy live/A2S:
HLL_BACKEND_LIVE_DATA_SOURCE=a2sHLL_BACKEND_HISTORICAL_DATA_SOURCE=rcon
Verificacion minima del proveedor activo:
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-scoreboardsolo 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_workercaptura sesiones RCON y mantiene ventanas competitivas prospectivas.app.rcon_admin_log_ingestioningiere AdminLog para el periodo solicitado.app.rcon_admin_log_parsernormaliza eventos como inicio/cierre de partida, kills, cambios de equipo, chat y mensajes de perfil.app.rcon_admin_log_storagepersiste eventos AdminLog deduplicados y snapshots de perfil de jugador.app.rcon_admin_log_materializationmaterializa partidas cerradas y estadisticas por jugador desde eventos RCON.app.rcon_historical_read_modelexpone las lecturas historicas actuales y solo recurre apublic-scoreboardcomo fallback/enriquecimiento cuando RCON no cubre la operacion.
Comandos manuales equivalentes dentro de Docker Compose:
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:
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/:
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:
python -m app.rcon_historical_worker capture -
una validacion acotada sobre un target concreto:
python -m app.rcon_historical_worker capture --target comunidad-hispana-01 -
un worker local en bucle:
python -m app.rcon_historical_worker loop --interval 120 --max-runs 1
La salida del worker incluye:
target_scopecaptured_attargetserrorsstorage_status
Cuando una captura falla, cada error incluye como minimo:
target_keyexternal_server_idnamehostporttimeout_secondserror_typeerror_stagemessage
error_type intenta clasificar al menos:
timeoutauth/loginconnection-refusedpayload-invalidother-error
La persistencia queda separada del historico historical_* actual y usa:
rcon_historical_targetsrcon_historical_capture_runsrcon_historical_samplesrcon_historical_checkpoints
Lectura historica minima cuando HLL_BACKEND_HISTORICAL_DATA_SOURCE=rcon:
- endpoints soportados hoy directamente por RCON persistido:
GET /api/historical/server-summaryGET /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-scoreboardpara mantener el contrato completo:GET /api/historical/weekly-top-killsGET /api/historical/weekly-leaderboardGET /api/historical/leaderboardGET /api/historical/monthly-mvpGET /api/historical/monthly-mvp-v2GET /api/historical/player-eventsGET /api/historical/player-profileGET /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__.pydeclara el paqueteappy reexporta las utilidades publicas minimas del bootstrap.collector.pydefine el flujo minimo de captura para desarrollo usando una fuente controlada.a2s_client.pyencapsula una consulta minima A2S_INFO por UDP para probar servidores reales sin acoplar todavia el backend a una fuente mas compleja.rcon_client.pyencapsula una conexion minima HLL RCON v2 por TCP conServerConnect, XOR key base64,authTokenyGetServerInformationpara consultas live de produccion.config.pycentraliza host, puerto y allowlist minima de origenes locales.data_sources.pydefine los contratos y la seleccion por entorno para live e historico.historical_ingestion.pyintenta primero el writer path RCON y, si hace falta poblarhistorical_*, cae de forma explicita a la capa JSON publica de CRCON.historical_models.pyfija las entidades historicas minimas del dominio.historical_snapshots.pyfija los tipos y selectores validos de snapshots historicos precalculados.historical_snapshot_storage.pypersiste snapshots historicos precalculados listos para lectura rapida.historical_runner.pyejecuta refresh incremental periodico con reintentos basicos.historical_storage.pyprepara la persistenciahistorical_*y las consultas agregadas iniciales.main.pycontiene el entrypoint HTTP y la creacion del servidor.normalizers.pytransforma registros crudos o respuestas A2S a un modelo comun del colector.routes.pyresuelve las rutas GET soportadas.payloads.pycentraliza respuestas placeholder y mock.server_targets.pyregistra targets A2S de prueba de forma desacoplada del flujo principal del colector.snapshots.pyconstruye snapshots consistentes con timestamp comun de captura.storage.pyprepara una persistencia local minima en SQLite paragame_sources,serversyserver_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_sourcesserversserver_snapshotshistorical_servershistorical_mapshistorical_matcheshistorical_playershistorical_player_match_statshistorical_ingestion_runs
Por defecto el archivo se crea en:
backend/data/hll_vietnam_dev.sqlite3
En Docker, ese mismo rol de persistencia debe montarse fuera del contenedor en:
/app/data/hll_vietnam_dev.sqlite3
Politica comun SQLite para writers:
timeoutexplicito compartidoPRAGMA foreign_keys = ONPRAGMA journal_mode = WALPRAGMA busy_timeoutrow_factory = sqlite3.Row
Esta politica se aplica de forma uniforme a las capas writer-capable que comparten el mismo SQLite, incluyendo:
historical_storage.pyplayer_event_storage.pyrcon_historical_storage.pystorage.py
Politica read-only para historico:
- las rutas de lectura de
historical_storage.pyno ejecutan yainitialize_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=roconrow_factory = sqlite3.RowyPRAGMA busy_timeout - la inicializacion, migraciones, seed y normalizaciones siguen reservadas al writer path explicito
Variable opcional:
HLL_BACKEND_STORAGE_PATHHLL_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-summaryweekly-leaderboardcon metricaskills,deaths,supportymatches_over_100_killsmonthly-leaderboardcon las mismas metricas semanticasmonthly-mvprecent-matches
Por defecto se escriben bajo:
backend/data/snapshots/<server_key>/
En Docker, estos snapshots deben persistirse bajo:
/app/data/snapshots/<server_key>/
Ejemplos:
backend/data/snapshots/comunidad-hispana-01/server-summary.jsonbackend/data/snapshots/comunidad-hispana-01/weekly-kills.jsonbackend/data/snapshots/comunidad-hispana-02/recent-matches.jsonbackend/data/snapshots/all-servers/weekly-support.jsonbackend/data/snapshots/all-servers/monthly-mvp.json
Cada archivo conserva metadatos operativos minimos:
server_keysnapshot_typemetricwindowpayloadgenerated_atsource_range_startsource_range_endis_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 desarrolloquery_server_info()permite consultar metadata basica real por A2S_INFOfetch_a2s_probe()adapta una consulta A2S real al modelo interno del colectorfetch_configured_a2s_probes()consulta la lista configurada de targets A2Snormalize_server_record()reduce los registros a una forma comunnormalize_a2s_server_info()reduce una respuesta A2S al mismo contrato internobuild_server_snapshot()ybuild_snapshot_batch()generan snapshots concaptured_atcollect_server_snapshots()orquesta captura, normalizacion, ensamblado y persistencia opcionalpersist_snapshot_batch()escribe el lote en SQLite y mantiene identidad de servidor separada del historico
Ejecucion manual desde backend/:
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:
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: 2success_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 ambossource_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_originpara distinguirreal-a2sfrente acontrolled-fallbacksource_refpara conservar una referencia de procedencia util en historico
Si se quiere seguir usando solo datos controlados:
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:
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
120segundos 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_SECONDSpara cambiar el intervalo por defecto--interval 120para fijar el intervalo en segundos en una ejecucion concreta--source a2s --no-fallbackpara forzar solo capturas reales--max-runs 3para limitar el numero de ciclos y evitar un bucle indefinido
Ejemplos:
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:
-
Desde
backend/, arrancar la API:python -m app.main -
En otra terminal, dejar el scheduler corriendo:
python -m app.scheduler -
Servir
frontend/con un servidor local sencillo y abrir la landing. El frontend volvera a pedir/api/serverscada120segundos, 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/:
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:7778game_port:7777source_name:community-hispana-a2sexternal_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:7778game_port:7777external_server_id:comunidad-hispana-01
- host/IP:
Comunidad Hispana #02- host/IP:
152.114.195.150 query_port:7878game_port:7877external_server_id:comunidad-hispana-02
- host/IP:
Si se quiere cambiar la lista sin editar codigo, puede definirse
HLL_BACKEND_A2S_TARGETS como un array JSON:
$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:
namehostquery_portgame_portopcionalsource_nameexternal_server_idopcionalregionopcional
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/latestdevuelve el ultimo snapshot conocido por servidor/api/servers/historydevuelve snapshots recientes agregados/api/servers/{id}/historydevuelve 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:
limitentre1y100servercon slug historico comocomunidad-hispana-01playeren/api/historical/player-profileaceptandostable_player_key,steam_idosource_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:
killsdeathssupportmatches_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_statusmissing_reasonrequest_path_policygeneration_policygenerated_atsource_range_startsource_range_endis_stalefreshnessfoundwindow_startwindow_endwindow_kindwindow_labeluses_fallbackselection_reasoncurrent_week_closed_matchesprevious_week_closed_matchessufficient_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.jsonplayer-events-death-by.jsonplayer-events-duels.jsonplayer-events-weapon-kills.jsonplayer-events-teamkills.json
Los endpoints /api/historical/player-events y
/api/historical/snapshots/player-events aceptan:
view=most-killedview=death-byview=duelsview=weapon-killsview=teamkills
La respuesta expone metadata operativa alineada con el resto de snapshots:
generated_atmonth_keysource_range_startsource_range_endfoundis_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,deathsyteamkillspersistidos - recompone
KPMyKDAdesde totales mensuales - aplica elegibilidad minima de
6partidas cerradas y6horas - 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_edgeyduel_controlderivados 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_atmonth_keyfoundsource_range_startsource_range_endevent_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.eshttps://scoreboard.comunidadhll.es:5443https://scoreboard.comunidadhll.es:3443
Comandos manuales desde backend/:
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:
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-01para limitar a un servidor--server comunidad-hispana-02para validar solo el segundo servidor activo--overlap-hours 48para releer una ventana reciente mayor sin relanzar bootstrap--max-pages 2para validacion local acotada--page-size 25para ajustar paginacion--start-page 4para forzar una pagina concreta en bootstraps largos--detail-workers 16para 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_detailde 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_sourceselected_sourcefallback_usedfallback_reasonsource_attemptsprimary_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_pagelast_completed_pagediscovered_total_matchesdiscovered_total_pagesarchive_exhaustedlast_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:
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:
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_sourcetermina enpublic-scoreboard, eso significa que la reconstruccion del archivo competitivohistorical_*siguio necesitando fallback clasico - si RCON falla por red, auth o timeout, el motivo queda visible en
fallback_reasony ensource_attempts
Los reintentos de cada request JSON pueden ajustarse sin tocar codigo con:
HLL_HISTORICAL_CRCON_REQUEST_RETRIESHLL_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
900segundos - prewarmea en cada ciclo:
server-summaryparacomunidad-hispana-01,comunidad-hispana-02yall-serversweekly-leaderboardde la metrica por defectokillspara esos mismos alcancesmonthly-leaderboardde la metrica por defectokillspara esos mismos alcancesrecent-matchespara esos mismos alcances
- recompone la matriz completa de snapshots cada
4ciclos para mantener el resto de metricas al dia sin penalizar todos los refresh - reintenta hasta
2veces tras un fallo - espera
30segundos entre reintentos - reutiliza el registro de
historical_ingestion_runspara dejar trazabilidad de ultimo refresh, resultado y errores basicos - persiste por servidor:
server-summaryweekly-leaderboardparakills,deaths,supportymatches_over_100_killsmonthly-leaderboardparakills,deaths,supportymatches_over_100_killsrecent-matches
Flags utiles del runner:
--server comunidad-hispana-01para limitar a un servidor--interval 900para fijar la frecuencia recomendada de snapshots--hourlypara fijar directamente un ciclo horario de3600segundos--retries 1para reducir reintentos--retry-delay 10para bajar la espera entre fallos--max-runs 1para una validacion puntual sin bucle indefinido
Para dejar automatizado el refresh historico horario en local, el comando avanzado sigue disponible:
python -m app.historical_runner --hourly
Sin --server, ese runner refresca:
comunidad-hispana-01comunidad-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:
docker compose exec backend python -m app.historical_runner --max-runs 1
Operativa local minima:
- Desde
backend/, arrancar la API conpython -m app.main. - En otra terminal, dejar corriendo
python -m app.historical_runner --hourly. - Verificar el proceso revisando la salida del runner: al arrancar imprime un
bloque JSON con
event: "historical-refresh-loop-started",server_scopeysnapshot_scope. - Confirmar que los snapshots siguen actualizandose revisando
generated_aten archivos bajobackend/data/snapshots/, por ejemplo:backend/data/snapshots/comunidad-hispana-01/server-summary.jsonbackend/data/snapshots/comunidad-hispana-02/recent-matches.jsonbackend/data/snapshots/comunidad-hispana-02/weekly-kills.jsonbackend/data/snapshots/all-servers/monthly-kills.json
Operativa avanzada con Docker Compose:
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_ingestionapp.historical_runnerapp.player_event_workerapp.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_SECONDSHLL_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
- el holder venia de un cwd tipo
- la coordinacion principal es este single-writer lock; WAL y
busy_timeoutquedan como endurecimiento complementario, no como solucion unica
Runbook minimo:
-
pasada manual del historico base mientras el runner automatico existe:
docker compose exec backend python -m app.historical_ingestion refresh --overlap-hours 48Si 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:
docker compose exec backend python -m app.player_event_worker refresh --overlap-hours 48 -
pasada manual de captura prospectiva RCON:
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 1o comandos manuales puntuales cuando se quiera una pasada controlada
Comprobaciones utiles con Compose:
docker compose ps historical-runnerdocker compose logs -f historical-runnerdocker compose exec backend python -m app.historical_runner --max-runs 1
Compose para captura prospectiva RCON:
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_SECONDSHLL_HISTORICAL_FULL_SNAPSHOT_EVERY_RUNSHLL_HISTORICAL_REFRESH_MAX_RETRIESHLL_HISTORICAL_REFRESH_RETRY_DELAY_SECONDSHLL_HISTORICAL_WEEKLY_FALLBACK_MIN_MATCHESHLL_HISTORICAL_WEEKLY_FALLBACK_MAX_WEEKDAY
Al inicializar la persistencia local, el backend normaliza tambien la identidad historica ya guardada:
- prioriza
steaminfo.profile.steamidcuando existe - si
player_idya parece un SteamID real, lo promueve igualmente asteam:* - si no hay SteamID, usa
player_idcomo clavecrcon-player:* - deja
steaminfo.idcomo 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:5500http://localhost:5500http://127.0.0.1http://127.0.0.1:8080http://localhosthttp://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_killeddeath_byweaponsdeath_by_weaponsteamkills
occurred_atusa 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_matchesyhistorical_player_match_statsaunque comparte el mismo SQLite de desarrollo
Contrato minimo normalizado por evento:
event_idevent_typeoccurred_atserver_slugexternal_match_idsource_kindsource_refkiller_player_keyvictim_player_keyweapon_namekill_categoryis_teamkillevent_value
Tablas nuevas:
player_event_raw_ledgerplayer_event_ingestion_runsplayer_event_backfill_progress
Comandos manuales desde backend/:
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_SECONDSHLL_PLAYER_EVENT_REFRESH_OVERLAP_HOURSHLL_PLAYER_EVENT_REFRESH_MAX_RETRIESHLL_PLAYER_EVENT_REFRESH_RETRY_DELAY_SECONDS
Flags utiles del worker:
--server comunidad-hispana-01para validar un solo servidor--overlap-hours 48para releer una ventana reciente mayor--max-pages 1para una comprobacion acotada
Ejemplos operativos:
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-scoreboardsolo 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:
rconprimeroa2ssolo como fallback
- historico:
rconprimero tanto para lectura minima como para el writer path primario dehistorical_ingestionpublic-scoreboardsolo 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_sourceselected_sourcefallback_usedfallback_reasonsource_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-summaryyrecent-matchespasan a usar esa capa RCON-backed como camino principal real- en runtime, esas dos rutas solo se sirven como
rconcuando 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 concoverage.status != "empty"ywindow_countosample_countmayor que cerorecent-matches: al menos una ventana conmatch_id,closed_atysample_count > 0
- si el target persistido quedo con clave legacy
rcon:<host>:<port>pero el runtime actual ya conoce suexternal_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-scoreboardconfallback_used = trueyfallback_reasonvisible historical_ingestionintenta 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-scoreboardmientras 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-scoreboardsolo 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:
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_statspublic_scoreboard_historical_matches- fuentes de rankings semanales y mensuales
server_summary_cache,server_snapshots,player_event_ledgerscoreboard_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 rebuildpython -m app.elo_mmr_engine leaderboard --server all-servers --limit 10python -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_ratingselo_mmr_match_resultselo_mmr_monthly_rankingselo_mmr_monthly_checkpoints
Politica de exactitud:
exact: outcome, combat, utility, disciplina por teamkills, MMR persistenteapproximate: role bucket, objective index, strength of schedulenot_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 = rconselected_source = hybrid-rcon-competitive-plus-public-scoreboardfallback_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.