24 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_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_SNAPSHOT_REFRESH_INTERVAL_SECONDSHLL_HISTORICAL_REFRESH_MAX_RETRIESHLL_HISTORICAL_REFRESH_RETRY_DELAY_SECONDSHLL_HISTORICAL_WEEKLY_FALLBACK_MIN_MATCHESHLL_HISTORICAL_WEEKLY_FALLBACK_MAX_WEEKDAY
El frontend/index.html viene preparado para volver a consultar el bloque de
servidores cada 120000 ms (120s) sin recargar la pagina completa. La landing
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.1:5500http://127.0.0.1:8080http://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/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/recent-matches?limit=6&server=comunidad-hispana-01GET /api/historical/player-profile?player=steam%3A76561198000000000
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 una consulta A2S real inmediata contra los 2
servidores configurados antes de responder.
La respuesta incluye metadata de frescura pensada para frontend:
last_snapshot_atsnapshot_age_secondssnapshot_age_minutesmax_snapshot_age_secondsis_stalefreshnesssourcerefresh_attemptedrefresh_status
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.
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.
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.config.pycentraliza host, puerto y allowlist minima de origenes locales.historical_ingestion.pyconsulta la capa JSON publica de CRCON para bootstrap y refresh incremental.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
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_killsrecent-matches
Por defecto se escriben bajo:
backend/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-03/recent-matches.jsonbackend/data/snapshots/all-servers/weekly-support.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.
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/recent-matches/api/historical/server-summary/api/historical/snapshots/server-summary/api/historical/snapshots/weekly-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.
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:
generated_atsource_range_startsource_range_endis_stalefreshnessfoundwindow_startwindow_endwindow_kindwindow_labeluses_fallbackselection_reasoncurrent_week_closed_matchesprevious_week_closed_matchessufficient_sample
Si un servidor ya tiene historico bruto en historical_* pero aun no conserva
el archivo precalculado correspondiente en backend/data/snapshots/, la API
intenta regenerar automaticamente el lote de snapshots de ese servidor antes de
responder. Esto evita que un servidor quede bloqueado en found: false por una
ausencia puntual de persistencia precalculada.
/api/historical/snapshots/server-summary devuelve item con el resumen del
servidor. /api/historical/snapshots/weekly-leaderboard devuelve items ya
precalculados para una metrica semanal y acepta limit para recortar el
payload ya persistido sin recalcularlo. /api/historical/snapshots/recent-matches
devuelve items de cierres recientes ya preparados y tambien acepta limit
para servir solo una parte del snapshot persistido.
Ingesta historica CRCON
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
Flags utiles:
--server comunidad-hispana-01para limitar a un servidor--server comunidad-hispana-03para validar solo el tercer scoreboard historico--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.
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.
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 ese refresh incremental listo
para ejecucion local repetida sin depender de infraestructura externa y
regenera snapshots historicos precalculados tras cada refresh correcto. Por
defecto:
- refresca y recompone snapshots cada
900segundos - 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_killsrecent-matches
Flags utiles del runner:
--server comunidad-hispana-01para limitar a un servidor--interval 900para fijar la frecuencia recomendada de snapshots--retries 1para reducir reintentos--retry-delay 10para bajar la espera entre fallos--max-runs 1para una validacion puntual sin bucle indefinido
Variables utiles del runner:
HLL_HISTORICAL_SNAPSHOT_REFRESH_INTERVAL_SECONDSHLL_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.1:8080http://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.
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.