sistema A2S

This commit is contained in:
devRaGonSa
2026-03-20 14:38:57 +01:00
parent a82a90a1b4
commit 87c1f4e8c3
75 changed files with 7625 additions and 137 deletions

View File

@@ -9,7 +9,7 @@ En esta primera fase, el proyecto se centra en una landing sencilla, limpia y pr
- Landing inicial de comunidad. - Landing inicial de comunidad.
- Estructura de repositorio preparada para crecer. - Estructura de repositorio preparada para crecer.
- Carpeta de backend reservada para una futura implementación en Python. - Carpeta de backend reservada para una futura implementación en Python.
- Carpeta `ai/` preparada para integrar más adelante una capa de orquestación y tareas. - Carpeta `ai/` ya integrada como capa operativa para orquestación por tasks y trabajo con Codex.
## Estructura del repositorio ## Estructura del repositorio
@@ -36,17 +36,26 @@ En esta primera fase, el proyecto se centra en una landing sencilla, limpia y pr
| |-- requirements.txt | |-- requirements.txt
| `-- app/ | `-- app/
| `-- __init__.py | `-- __init__.py
`-- ai/ |-- ai/
|-- README.md | |-- README.md
|-- orchestrator/ | |-- architecture-index.md
| `-- README.md | |-- repo-context.md
`-- tasks/ | |-- system-metrics.md
|-- pending/ | |-- task-template.md
| `-- .gitkeep | |-- prompts/
|-- in-progress/ | | `-- plan-feature.md
| `-- .gitkeep | |-- orchestrator/
`-- done/ | | `-- README.md
`-- .gitkeep | `-- tasks/
| |-- pending/
| | `-- .gitkeep
| |-- in-progress/
| | `-- .gitkeep
| `-- done/
| `-- .gitkeep
`-- scripts/
|-- codex-runner.ps1
`-- run-integration-tests.ps1
``` ```
## Backend futuro ## Backend futuro
@@ -62,4 +71,4 @@ No hace falta servidor para esta primera versión.
## Evolución prevista ## Evolución prevista
En una fase posterior se integrará el repositorio plantilla `ai-dev-platform-template`. En esta ejecución no se ha copiado esa plantilla completa; únicamente se ha dejado el proyecto preparado para recibirla de forma ordenada. La capa inspirada en `ai-dev-platform-template` ya está integrada y adaptada al contexto real de HLL Vietnam. Las siguientes iteraciones deben centrarse en usarla para planificar y ejecutar tasks reales del producto sin ampliar alcance fuera de ese flujo.

View File

@@ -12,6 +12,7 @@ Community website repository with a static landing in the current phase and a pl
- `README.md` - `README.md`
- `AGENTS.md` - `AGENTS.md`
- `docs/current-hll-servers-source-plan.md`
- `docs/` - `docs/`
### Frontend ### Frontend
@@ -41,7 +42,7 @@ Community website repository with a static landing in the current phase and a pl
## Current Technical Baseline ## Current Technical Baseline
- Frontend runtime is plain browser-loaded HTML, CSS and JavaScript. - Frontend runtime is plain browser-loaded HTML, CSS and JavaScript.
- There is no active backend runtime yet. - Backend runtime is a minimal Python bootstrap with `GET /health` and room for placeholder API routes.
- Python is the expected backend language for future development. - Python is the expected backend language for future development.
- GitHub Actions and local PowerShell scripts may support the AI task workflow. - GitHub Actions and local PowerShell scripts may support the AI task workflow.
@@ -56,3 +57,15 @@ Community website repository with a static landing in the current phase and a pl
- Frontend changes should remain compatible with local browser opening where applicable. - Frontend changes should remain compatible with local browser opening where applicable.
- AI platform changes should keep task paths and documentation aligned. - AI platform changes should keep task paths and documentation aligned.
- Script changes should fail safely when optional tools or tests are not configured. - Script changes should fail safely when optional tools or tests are not configured.
## Current Integration Direction
- Discord and game server data remain in planning phase until sources, limits and security are validated.
- Initial dynamic data should come from controlled backend placeholders, not direct frontend calls to external services.
- The technical plan for these integrations is documented in `docs/discord-and-server-data-plan.md`.
- Current Hell Let Loose servers may be exposed as a clearly marked provisional reference block before HLL Vietnam-specific data exists.
- The phased source strategy for that provisional block is documented in `docs/current-hll-servers-source-plan.md`.
- The ingestion strategy for converting that provisional block into normalized server snapshots is documented in `docs/current-hll-data-ingestion-plan.md`.
- The logical storage foundation for persisting server snapshots is documented in `docs/stats-database-schema-foundation.md`.
- Frontend data consumption should remain progressive, endpoint by endpoint, with static fallbacks preserved during migration.
- The frontend integration strategy is documented in `docs/frontend-data-consumption-plan.md`.

View File

@@ -13,3 +13,5 @@ This file tracks lightweight execution history for the HLL Vietnam AI developmen
## Task History ## Task History
Date | Task | Duration | Result | Notes Date | Task | Duration | Result | Notes
2026-03-19 | TASK-001-platform-readiness-check | n/a | success | README y roadmap alineados con la integración actual de AI Platform; scripts clave presentes; no hay tests de integración configurados para este alcance.
2026-03-19T13:59:33 | worker-cycle | 641.24 sec | success | codex-runner

View File

@@ -0,0 +1,75 @@
# TASK-001-platform-readiness-check
## Goal
Validar que la integración de AI Development Platform en el repositorio HLL Vietnam está completa, coherente y lista para ejecutar tasks de forma segura con Codex.
## Context
El repositorio ya tiene una estructura base de producto y se ha integrado la capa operativa inspirada en ai-dev-platform-template. Antes de generar tasks funcionales del producto, hay que verificar que el flujo por tasks, documentación, scripts y estructura están realmente alineados y no contienen inconsistencias.
## Steps
1. Revisar la estructura de carpetas del repositorio.
2. Verificar que existen y son coherentes:
- AGENTS.md
- ai/task-template.md
- ai/repo-context.md
- ai/architecture-index.md
- ai/system-metrics.md
- ai/prompts/plan-feature.md
- ai/orchestrator/*
- ai/tasks/pending
- ai/tasks/in-progress
- ai/tasks/done
- scripts/codex-runner.ps1
- scripts/run-integration-tests.ps1
3. Confirmar que AGENTS.md refleja el flujo correcto:
- el orquestador analiza código y redacta tasks
- Codex no actúa fuera de tasks salvo mantenimiento o inspección explícita
- backend previsto en Python
- frontend actual en HTML/CSS/JS
4. Revisar que la documentación de contexto no contradice el proyecto HLL Vietnam.
5. Revisar que la task actual y la plantilla de task usan una estructura coherente con el template.
6. Si detectas incoherencias pequeñas, corrígelas.
7. Dejar un breve resumen en ai/system-metrics.md o en la documentación correspondiente si el flujo del repo ya lo contempla.
## Files to Read First
- AGENTS.md
- README.md
- docs/project-overview.md
- docs/decisions.md
- ai/task-template.md
- ai/repo-context.md
- ai/architecture-index.md
- ai/orchestrator/README.md
- scripts/codex-runner.ps1
- scripts/run-integration-tests.ps1
## Expected Files to Modify
- Solo los estrictamente necesarios si se detectan inconsistencias menores.
- No modificar archivos de producto salvo documentación mínima si es imprescindible.
## Constraints
- No implementar features del producto.
- No tocar la landing salvo que haya una inconsistencia documental o estructural.
- No añadir dependencias nuevas.
- No hacer cambios destructivos.
- Mantener el cambio pequeño y verificable.
## Validation
- La estructura AI Platform existe y es navegable.
- AGENTS.md refleja correctamente el flujo de trabajo del proyecto.
- No hay referencias obsoletas a .NET como arquitectura del producto.
- Los scripts clave existen.
- La plantilla de task es usable para futuras tasks.
- El repo queda listo para que el siguiente paso sea crear una task funcional real.
## Change Budget
- Preferir menos de 8 archivos modificados.
- Preferir menos de 250 líneas cambiadas.
## Outcome
- Verificada la existencia y coherencia de `AGENTS.md`, `ai/task-template.md`, `ai/repo-context.md`, `ai/architecture-index.md`, `ai/system-metrics.md`, `ai/prompts/plan-feature.md`, `ai/orchestrator/*`, `ai/tasks/*`, `scripts/codex-runner.ps1` y `scripts/run-integration-tests.ps1`.
- Confirmado que `AGENTS.md` refleja correctamente el flujo por tasks, el alcance de Codex y la dirección técnica actual: frontend en HTML/CSS/JS y backend futuro en Python.
- Corregidas dos incoherencias documentales menores:
- `README.md` ya no presenta la plataforma AI como integración futura.
- `docs/roadmap.md` ahora trata la orquestación como capacidad existente que debe evolucionar, no incorporarse desde cero.
- Validación manual completada. El script `scripts/run-integration-tests.ps1` existe, pero actualmente solo documenta que no hay tests de integración configurados para este alcance.

View File

@@ -0,0 +1,61 @@
# TASK-002-logo-asset-integration
## Goal
Integrar correctamente el logo real del proyecto HLL Vietnam en la landing actual, asegurando que el frontend use un asset local estable y coherente con la identidad visual del proyecto.
## Context
La landing inicial ya existe y actualmente está preparada para mostrar un logo local en `frontend/assets/img/logo.png`. Antes de hacer mejoras visuales adicionales, hay que consolidar la integración del logo real para que el proyecto tenga una base visual correcta, mantenible y consistente.
## Steps
1. Revisar la estructura actual del frontend.
2. Confirmar que existe la ruta esperada para el logo:
- `frontend/assets/img/logo.png`
3. Si el logo real aún no está en esa ruta, dejar la integración preparada de forma segura sin romper la página.
4. Revisar `frontend/index.html` para asegurar que:
- usa la ruta local correcta del logo
- el `alt` del logo es descriptivo y coherente
- no hay rutas temporales, absolutas o inconsistentes
5. Revisar `frontend/assets/css/styles.css` para asegurar que:
- el bloque visual del logo tiene un tamaño adecuado
- mantiene buena presentación en escritorio y móvil
- no deforma la imagen
6. Corregir cualquier inconsistencia menor relacionada con la carga o presentación del logo.
7. Mantener la landing simple: logo, tráiler y botón de Discord.
## Files to Read First
- frontend/index.html
- frontend/assets/css/styles.css
- docs/project-overview.md
- ai/repo-context.md
- AGENTS.md
## Expected Files to Modify
- frontend/index.html
- frontend/assets/css/styles.css
- opcionalmente `frontend/assets/img/logo.png` si el flujo de la task contempla dejar el asset correctamente ubicado
## Constraints
- No rediseñar toda la landing.
- No añadir nuevas secciones.
- No introducir frameworks ni dependencias nuevas.
- No tocar backend.
- No hacer cambios destructivos.
- Mantener la estética militar sobria ya definida.
## Validation
- La landing referencia el logo local mediante una ruta estable.
- El logo se muestra correctamente sin deformaciones.
- El HTML sigue funcionando al abrirse directamente en navegador.
- La presentación del logo es correcta tanto en móvil como en escritorio.
- No se rompe el tráiler ni el botón de Discord.
## Change Budget
- Preferir menos de 4 archivos modificados.
- Preferir menos de 120 líneas cambiadas.
## Outcome
- Confirmada la existencia del asset local `frontend/assets/img/logo.png`; no fue necesario reemplazarlo ni moverlo.
- `frontend/index.html` mantiene una ruta local estable (`./assets/img/logo.png`) y ahora usa un `alt` más descriptivo junto con dimensiones explícitas para reducir saltos de layout.
- `frontend/assets/css/styles.css` ya no fuerza un contenedor cuadrado para el logo: la imagen conserva su proporción con `max-width` y `max-height`, con ajustes específicos para escritorio y móvil.
- Validación completada mediante revisión del marcado, verificación de la ruta local del logo y revisión de `git diff --name-only`.
- No hay tests de integración configurados para este alcance; la comprobación aplicable fue manual sobre HTML/CSS y compatibilidad con apertura directa en navegador.

View File

@@ -0,0 +1,67 @@
# TASK-003-landing-minimal-polish
## Goal
Mejorar visualmente la landing actual de HLL Vietnam con un pulido minimo y controlado, reforzando la identidad militar y la claridad visual sin cambiar el alcance funcional de la pagina.
## Context
La landing ya tiene los tres elementos principales del alcance actual: logo local, boton de Discord y trailer embebido. Tras integrar correctamente el asset del logo, el siguiente paso es mejorar la presentacion general para que la pagina tenga una apariencia mas solida, mas limpia y mas coherente con la tematica del proyecto, sin anadir nuevas secciones ni complejidad innecesaria.
## Steps
1. Revisar la estructura actual de `frontend/index.html`.
2. Revisar los estilos actuales en `frontend/assets/css/styles.css`.
3. Mejorar la jerarquia visual del bloque principal:
- logo
- titulo principal
- subtitulo, si existe
- boton de Discord
- bloque del trailer
4. Ajustar espaciados, tamanos, anchuras y alineaciones para mejorar legibilidad.
5. Reforzar la identidad visual militar sobria:
- paleta oscura
- acentos controlados
- mejor contraste
- sensacion tactica/cinematica sin recargar
6. Mejorar el contenedor del video para que se vea mas integrado en la composicion general.
7. Verificar que la landing siga funcionando correctamente en escritorio y movil.
8. Mantener el alcance estricto actual, sin anadir nuevas funcionalidades ni secciones.
## Files to Read First
- frontend/index.html
- frontend/assets/css/styles.css
- docs/project-overview.md
- ai/repo-context.md
- AGENTS.md
## Expected Files to Modify
- frontend/index.html
- frontend/assets/css/styles.css
## Constraints
- No anadir nuevas secciones de contenido.
- No tocar backend.
- No introducir frameworks ni librerias nuevas.
- No modificar la ruta del logo.
- No cambiar el enlace de Discord.
- No cambiar el trailer.
- No hacer cambios destructivos.
- Mantener el HTML funcional abriendose directamente en navegador.
## Validation
- La landing se ve mas solida y ordenada visualmente.
- Se mantiene el enfoque minimo: logo, identidad, Discord y trailer.
- El logo sigue cargando desde `./assets/img/logo.png`.
- El boton de Discord sigue visible y destacado.
- El trailer sigue funcionando correctamente.
- La presentacion sigue siendo responsive.
- No se anaden elementos fuera del alcance actual.
## Change Budget
- Preferir menos de 3 archivos modificados.
- Preferir menos de 180 lineas cambiadas.
## Outcome
- `frontend/index.html` mantiene el contenido original de la landing y solo anade hooks de estilo para reforzar la jerarquia visual del bloque principal.
- `frontend/assets/css/styles.css` mejora composicion, contraste, espaciados y tratamiento del bloque de video con una direccion mas sobria y tactica, sin introducir nuevas secciones ni funcionalidades.
- Se mantuvieron sin cambios la ruta del logo (`./assets/img/logo.png`), el enlace de Discord y el `iframe` del trailer.
- Validacion completada mediante revision del HTML/CSS resultante y `git diff --name-only`, confirmando que el alcance quedo limitado a los archivos esperados.
- No hay tests de integracion configurados para este alcance; la validacion aplicable fue manual sobre compatibilidad de HTML/CSS para apertura directa en navegador y comportamiento responsive esperado.

View File

@@ -0,0 +1,69 @@
# TASK-004-python-backend-bootstrap
## Goal
Preparar una base minima y ordenada de backend en Python para el proyecto HLL Vietnam, dejando la estructura lista para crecer sin implementar todavia logica de negocio real ni integraciones externas.
## Context
El proyecto ya dispone de una landing minima funcional y de la capa operativa AI Platform integrada. El siguiente paso tecnico logico es dejar preparado el backend principal del proyecto, que estara basado en Python. En esta fase no se busca desarrollar funcionalidades reales, sino establecer una base limpia, mantenible y coherente con la futura evolucion del sistema.
## Steps
1. Revisar la estructura actual de la carpeta `backend/`.
2. Preparar una base minima de aplicacion Python dentro de `backend/app/`.
3. Crear o ajustar un punto de entrada claro para la aplicacion, por ejemplo un archivo principal como:
- `backend/app/main.py`
4. Definir una estructura minima coherente para crecimiento posterior.
5. Anadir un endpoint o mecanismo minimo de verificacion de estado, por ejemplo:
- `/health`
o equivalente segun la solucion elegida.
6. Actualizar `backend/README.md` para explicar:
- proposito de la carpeta backend
- stack elegido en esta fase
- como arrancar localmente la base minima si ya aplica
7. Ajustar `backend/requirements.txt` con las dependencias minimas reales, si fueran necesarias.
8. Mantener el alcance estricto: solo bootstrap tecnico del backend.
## Files to Read First
- README.md
- AGENTS.md
- docs/project-overview.md
- docs/roadmap.md
- docs/decisions.md
- ai/repo-context.md
- ai/architecture-index.md
- backend/README.md
- backend/requirements.txt
- backend/app/__init__.py
## Expected Files to Modify
- backend/README.md
- backend/requirements.txt
- backend/app/__init__.py
- backend/app/main.py
## Constraints
- No implementar logica de Discord.
- No implementar logica de servidores de juego.
- No anadir base de datos todavia.
- No introducir complejidad innecesaria.
- No tocar frontend salvo documentacion cruzada minima si fuera imprescindible.
- No hacer cambios destructivos.
- Mantener la solucion simple, clara y preparada para crecer.
## Validation
- Existe una estructura backend mas solida que la inicial.
- Existe un punto de entrada claro para la aplicacion Python.
- Existe una verificacion minima de estado o equivalente.
- La documentacion del backend refleja correctamente el nuevo estado.
- No se han anadido funcionalidades fuera de alcance.
- El backend queda listo para que una siguiente task defina contratos API o primeras rutas reales.
## Change Budget
- Preferir menos de 6 archivos modificados o creados.
- Preferir menos de 220 lineas cambiadas.
## Outcome
- Bootstrap backend implementado con Python estandar, sin frameworks ni dependencias externas.
- Punto de entrada creado en `backend/app/main.py`.
- Health check minimo disponible en `GET /health`.
- `backend/README.md` actualizado con estructura, alcance y forma de arranque local.
- `backend/requirements.txt` mantenido sin dependencias externas para evitar compromisos prematuros de arquitectura.

View File

@@ -0,0 +1,86 @@
# TASK-005-frontend-backend-contract
## Goal
Definir el contrato inicial entre frontend y backend para el proyecto HLL Vietnam, estableciendo endpoints, formatos de respuesta y convenciones mínimas sin implementar todavía integraciones reales con Discord, servidores de juego o base de datos.
## Context
El proyecto ya dispone de una landing mínima funcional y de un backend Python bootstrap con verificación básica de estado. Antes de añadir lógica real, es importante fijar un contrato claro entre frontend y backend para evitar improvisación posterior y permitir que futuras tasks implementen endpoints y consumo de datos de forma consistente.
## Steps
1. Revisar la estructura actual del frontend y del backend.
2. Revisar el estado actual del backend bootstrap y el endpoint de salud existente.
3. Definir el conjunto mínimo de endpoints previstos a corto plazo, aunque algunos queden documentados como futuros. Incluir al menos:
- `GET /health`
- `GET /api/community`
- `GET /api/trailer`
- `GET /api/discord`
- `GET /api/servers`
4. Para cada endpoint, documentar:
- propósito
- método HTTP
- ruta
- formato de respuesta
- ejemplo JSON
- estado actual: implementado, previsto o placeholder
5. Definir convenciones básicas de respuesta:
- nombres de campos
- estructura JSON
- uso de `status`
- tratamiento mínimo de errores
6. Documentar cómo debería consumir el frontend estos endpoints más adelante, sin implementarlo todavía.
7. Añadir o actualizar documentación técnica para que el contrato quede claro dentro del repositorio.
8. Si detectas incoherencias pequeñas entre documentación y bootstrap actual, corrígelas sin salirte del alcance.
## Files to Read First
- README.md
- AGENTS.md
- docs/project-overview.md
- docs/roadmap.md
- docs/decisions.md
- ai/repo-context.md
- ai/architecture-index.md
- backend/README.md
- backend/app/__init__.py
- backend/app/main.py
- frontend/index.html
- frontend/assets/js/main.js
## Expected Files to Modify
- docs/project-overview.md
- docs/decisions.md
- backend/README.md
- ai/architecture-index.md
- opcionalmente un nuevo documento técnico si encaja mejor, por ejemplo:
- `docs/frontend-backend-contract.md`
## Constraints
- No implementar todavía endpoints reales adicionales.
- No integrar Discord real.
- No integrar servidores de juego reales.
- No introducir base de datos.
- No cambiar el comportamiento visible del frontend.
- No añadir frameworks ni dependencias nuevas.
- No hacer cambios destructivos.
- Mantener la solución clara, pequeña y útil para futuras tasks.
## Validation
- Existe documentación clara del contrato inicial frontend-backend.
- `GET /health` queda reflejado correctamente como endpoint actual.
- Los endpoints futuros quedan documentados con ejemplos coherentes.
- El contrato usa convenciones consistentes de respuesta JSON.
- El repositorio queda preparado para que siguientes tasks implementen endpoints reales de forma ordenada.
## Change Budget
- Preferir menos de 5 archivos modificados o creados.
- Preferir menos de 220 líneas cambiadas.
## Outcome
- Se añadió `docs/frontend-backend-contract.md` como referencia central del contrato inicial frontend-backend.
- Se actualizaron `docs/project-overview.md`, `docs/decisions.md` y `backend/README.md` para reflejar el contrato y el estado actual de `GET /health`.
- No se implementaron endpoints nuevos ni se cambió el comportamiento visible del frontend.
## Validation Result
- Verificado con `python -c "from app.main import build_health_payload; print(build_health_payload())"` en `backend/`.
- Resultado comprobado: `{'status': 'ok', 'service': 'hll-vietnam-backend', 'phase': 'bootstrap'}`.
- No aplican integration tests para este alcance documental.

View File

@@ -0,0 +1,84 @@
# TASK-006-discord-and-server-data-plan
## Goal
Definir la estrategia técnica inicial para obtener, modelar y exponer los futuros datos de Discord y de los servidores de juego en el proyecto HLL Vietnam, sin implementar todavía integraciones reales.
## Context
El proyecto ya dispone de una landing mínima, un backend Python bootstrap y un contrato inicial frontend-backend documentado. Antes de implementar endpoints reales o lógica de consumo en frontend, es necesario fijar un plan claro sobre qué datos se quieren mostrar, de qué fuentes podrían obtenerse, qué limitaciones existen y cuál será el orden recomendado de implementación.
## Steps
1. Revisar la documentación técnica actual del proyecto.
2. Identificar qué información tendría sentido mostrar en la web sobre Discord. Incluir al menos posibles bloques como:
- enlace de invitación
- nombre de la comunidad
- estado o presencia aproximada si fuera viable
- información pública útil para comunidad
3. Identificar qué información tendría sentido mostrar sobre los servidores de juego. Incluir al menos posibles bloques como:
- nombre del servidor
- estado online/offline
- mapa actual o rotación si fuera viable
- jugadores conectados
- capacidad máxima
- ping o metadatos similares si la fuente lo permite
4. Documentar las posibles fuentes de datos para Discord, distinguiendo claramente entre:
- widget público
- API o integraciones externas
- bot propio
- datos configurados manualmente
5. Documentar las posibles fuentes de datos para servidores de juego, distinguiendo claramente entre:
- consultas al servidor
- API externa
- datos mock/placeholder
- actualización manual
6. Documentar riesgos, límites y restricciones:
- credenciales
- rate limits
- disponibilidad
- seguridad
- CORS
- latencia
- dependencia de servicios externos
7. Proponer una estrategia por fases:
- fase inicial con placeholders o datos controlados
- fase intermedia con integración técnica limitada
- fase posterior con integración más real si procede
8. Dejar claro qué NO se implementará todavía.
9. Actualizar la documentación técnica del repositorio para que futuras tasks de backend y frontend tengan una base clara.
## Files to Read First
- README.md
- AGENTS.md
- docs/project-overview.md
- docs/roadmap.md
- docs/decisions.md
- docs/frontend-backend-contract.md
- ai/repo-context.md
- ai/architecture-index.md
- backend/README.md
## Expected Files to Modify
- docs/roadmap.md
- docs/decisions.md
- ai/architecture-index.md
- opcionalmente un nuevo documento técnico si encaja mejor, por ejemplo:
- docs/discord-and-server-data-plan.md
## Constraints
- No implementar integraciones reales.
- No modificar comportamiento del frontend.
- No añadir endpoints funcionales nuevos.
- No introducir dependencias nuevas.
- No tocar base de datos.
- No hacer cambios destructivos.
- Mantener el resultado claro, útil y centrado en planificación técnica.
## Validation
- Existe una estrategia documentada para los datos futuros de Discord.
- Existe una estrategia documentada para los datos futuros de servidores.
- Se distinguen claramente fuentes posibles, riesgos y fases.
- Queda explícito qué se hará primero y qué se pospone.
- La documentación sirve como base directa para siguientes tasks técnicas.
## Change Budget
- Preferir menos de 5 archivos modificados o creados.
- Preferir menos de 240 líneas cambiadas.

View File

@@ -0,0 +1,66 @@
# TASK-007-backend-api-skeleton
## Goal
Preparar un esqueleto inicial de API en el backend Python para HLL Vietnam, dejando rutas y estructura listas para crecimiento posterior mediante respuestas placeholder o mock, sin integrar todavía Discord ni servidores reales.
## Context
El backend ya cuenta con un bootstrap mínimo y un endpoint de salud. El contrato frontend-backend ya define un conjunto inicial de endpoints previstos. El siguiente paso técnico es convertir esa definición en una estructura API básica y mantenible, con rutas organizadas y respuestas coherentes, aunque todavía sean estáticas o placeholder.
## Steps
1. Revisar el backend actual y la documentación del contrato frontend-backend.
2. Confirmar el punto de entrada actual del backend y su estructura.
3. Preparar una organización mínima para rutas API futuras dentro de `backend/app/`.
4. Implementar o estructurar las rutas placeholder mínimas para:
- `GET /health`
- `GET /api/community`
- `GET /api/trailer`
- `GET /api/discord`
- `GET /api/servers`
5. Hacer que las respuestas sean coherentes con el contrato ya documentado, aunque usen datos placeholder.
6. Mantener una estructura clara para crecimiento posterior, por ejemplo separando:
- punto de entrada
- rutas
- utilidades o payloads placeholder si hiciera falta
7. Actualizar la documentación del backend para reflejar la nueva estructura y cómo ejecutar la API localmente.
8. Mantener el alcance estricto: esqueleto funcional, no integración real.
## Files to Read First
- AGENTS.md
- docs/frontend-backend-contract.md
- docs/project-overview.md
- docs/decisions.md
- ai/repo-context.md
- ai/architecture-index.md
- backend/README.md
- backend/requirements.txt
- backend/app/__init__.py
- backend/app/main.py
## Expected Files to Modify
- backend/app/main.py
- backend/app/__init__.py
- backend/README.md
- opcionalmente archivos nuevos dentro de `backend/app/` si mejoran claridad, por ejemplo:
- backend/app/routes.py
- backend/app/payloads.py
## Constraints
- No integrar Discord real.
- No integrar servidores reales.
- No añadir base de datos.
- No introducir complejidad innecesaria.
- No tocar frontend.
- No añadir frameworks nuevos si no son estrictamente necesarios para mantener coherencia con el bootstrap actual.
- No hacer cambios destructivos.
- Mantener la API simple, clara y consistente con la documentación ya existente.
## Validation
- El backend expone claramente los endpoints placeholder definidos.
- `GET /health` sigue funcionando correctamente.
- Los endpoints futuros definidos en el contrato existen al menos como placeholders coherentes.
- La estructura backend queda mejor preparada para crecimiento.
- La documentación del backend refleja el nuevo estado real.
## Change Budget
- Preferir menos de 7 archivos modificados o creados.
- Preferir menos de 260 líneas cambiadas.

View File

@@ -0,0 +1,68 @@
# TASK-008-frontend-data-consumption-plan
## Goal
Definir cómo consumirá el frontend de HLL Vietnam los futuros endpoints del backend, dejando documentada una estrategia clara de integración de datos sin implementar todavía lógica real de consumo.
## Context
Ya existe una landing mínima y se ha definido un contrato inicial entre frontend y backend. Además, se va a preparar un esqueleto API en backend. Antes de introducir JavaScript de consumo real o secciones dinámicas, conviene dejar documentado cómo deberá organizarse la integración en frontend para mantener simplicidad, claridad y consistencia con la evolución del proyecto.
## Steps
1. Revisar el frontend actual y el contrato frontend-backend.
2. Identificar qué bloques del frontend actual o futuro podrán depender de datos dinámicos del backend.
3. Definir una estrategia mínima de consumo de datos adecuada al proyecto en esta fase, por ejemplo:
- `fetch`
- JavaScript simple
- módulos ligeros si fueran necesarios más adelante
4. Documentar cómo deberían gestionarse:
- estados de carga
- errores
- ausencia de datos
- placeholders
- fallback visual
5. Documentar qué endpoints podrían ser consumidos primero y con qué prioridad:
- `/health`
- `/api/community`
- `/api/trailer`
- `/api/discord`
- `/api/servers`
6. Proponer una estrategia progresiva para pasar de landing estática a bloques dinámicos sin romper simplicidad.
7. Añadir o actualizar documentación técnica del frontend o del proyecto con esta estrategia.
8. Mantener el alcance documental: no implementar todavía fetch real ni renderizado dinámico.
## Files to Read First
- AGENTS.md
- README.md
- docs/project-overview.md
- docs/frontend-backend-contract.md
- docs/decisions.md
- ai/repo-context.md
- ai/architecture-index.md
- frontend/index.html
- frontend/assets/js/main.js
- frontend/assets/css/styles.css
## Expected Files to Modify
- docs/project-overview.md
- docs/decisions.md
- ai/architecture-index.md
- opcionalmente un nuevo documento técnico si encaja mejor, por ejemplo:
- docs/frontend-data-consumption-plan.md
## Constraints
- No implementar consumo real de API.
- No cambiar comportamiento visible del frontend.
- No añadir librerías nuevas.
- No rediseñar la landing.
- No tocar backend salvo referencias documentales mínimas si son imprescindibles.
- No hacer cambios destructivos.
- Mantener la solución centrada en planificación de integración.
## Validation
- Existe una estrategia documentada para el consumo de datos en frontend.
- Quedan definidos criterios para loading, error, empty state y fallback.
- Se identifican prioridades de consumo de endpoints.
- El frontend queda preparado conceptualmente para evolucionar sin improvisación.
## Change Budget
- Preferir menos de 5 archivos modificados o creados.
- Preferir menos de 220 líneas cambiadas.

View File

@@ -0,0 +1,55 @@
# TASK-009-backend-package-hygiene-and-entrypoint-alignment
## Goal
Corregir y alinear la estructura mínima del paquete backend en Python para HLL Vietnam, asegurando que el paquete, los imports y el entrypoint real sean consistentes, legibles y mantenibles.
## Context
El backend bootstrap y el esqueleto API placeholder ya existen, pero en resúmenes recientes aparece una posible inconsistencia con el archivo de paquete Python (`init.py` frente a `__init__.py`). Antes de continuar con más ajustes o integrar consumo desde frontend, es importante dejar la base del paquete limpia y sin ambigüedades.
## Steps
1. Revisar la estructura actual de `backend/app/`.
2. Confirmar si el archivo correcto del paquete existe como:
- `backend/app/__init__.py`
3. Si existe cualquier inconsistencia de naming o packaging, corregirla.
4. Revisar y alinear los imports internos del backend para que sean coherentes con la estructura final.
5. Confirmar cuál es el entrypoint real del backend y dejarlo claro en la documentación.
6. Revisar el comando de arranque documentado y adaptarlo si fuera necesario.
7. Validar que los endpoints placeholder actuales siguen funcionando tras el ajuste.
8. Mantener el cambio pequeño y centrado en higiene estructural, no en nuevas funcionalidades.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- ai/architecture-index.md
- backend/README.md
- backend/requirements.txt
- backend/app/__init__.py
- backend/app/main.py
- backend/app/routes.py
- backend/app/payloads.py
## Expected Files to Modify
- backend/app/__init__.py
- backend/app/main.py
- backend/app/routes.py
- backend/README.md
- opcionalmente otros archivos internos del backend si son estrictamente necesarios para alinear imports y entrypoint
## Constraints
- No añadir integraciones reales.
- No introducir dependencias nuevas innecesarias.
- No tocar frontend.
- No añadir base de datos.
- No hacer cambios destructivos fuera del alcance del paquete backend.
- Mantener el backend simple y consistente con la fase actual.
## Validation
- Existe un archivo de paquete Python correcto en `backend/app/__init__.py`.
- No quedan referencias ambiguas o incorrectas al package layout.
- El entrypoint del backend está claro.
- Los endpoints placeholder actuales siguen respondiendo.
- La documentación del backend refleja el estado real.
## Change Budget
- Preferir menos de 5 archivos modificados.
- Preferir menos de 160 líneas cambiadas.

View File

@@ -0,0 +1,55 @@
# TASK-010-backend-local-dev-config-and-cors-bootstrap
## Goal
Preparar la configuración mínima de desarrollo local del backend para HLL Vietnam, incluyendo parámetros básicos de ejecución y soporte controlado para consumo desde frontend local durante la fase de desarrollo.
## Context
El proyecto ya tiene frontend estático y backend placeholder. El siguiente paso para permitir un primer enlace real entre ambos es dejar clara la forma de ejecución local y, si aplica, habilitar el mínimo soporte técnico necesario para consultas desde frontend local, evitando problemas básicos de entorno o de origen cruzado en desarrollo.
## Steps
1. Revisar cómo se ejecuta actualmente el backend.
2. Definir y documentar claramente:
- host por defecto
- puerto por defecto
- comando de arranque local
3. Revisar si el frontend local necesitará consumir el backend desde distinto origen durante desarrollo.
4. Si hace falta, añadir una solución mínima y controlada para CORS o para origen local de desarrollo, sin sobredimensionar el sistema.
5. Mantener la configuración simple y enfocada al entorno local.
6. Actualizar la documentación relevante para que levantar el stack local sea fácil.
7. No introducir complejidad de producción en esta fase.
## Files to Read First
- README.md
- AGENTS.md
- ai/repo-context.md
- docs/project-overview.md
- docs/frontend-backend-contract.md
- backend/README.md
- backend/app/main.py
- backend/app/routes.py
## Expected Files to Modify
- backend/README.md
- backend/app/main.py
- opcionalmente un archivo nuevo de configuración mínima si mejora claridad, por ejemplo:
- backend/app/config.py
- opcionalmente documentación raíz si conviene reflejar arranque local combinado
## Constraints
- No integrar Discord real.
- No integrar servidores reales.
- No añadir base de datos.
- No añadir frameworks nuevos salvo que sean estrictamente necesarios y coherentes con la base ya existente.
- No tocar el comportamiento visible del frontend todavía.
- No hacer cambios destructivos.
- Mantener la configuración pequeña, clara y orientada a desarrollo local.
## Validation
- El backend tiene host/puerto local claramente definidos o documentados.
- Existe una forma clara de levantarlo en local.
- Si el frontend necesita consultar el backend desde otro origen, la solución mínima está resuelta o documentada.
- El proyecto queda listo para un primer consumo real desde frontend en una task posterior inmediata.
## Change Budget
- Preferir menos de 5 archivos modificados o creados.
- Preferir menos de 180 líneas cambiadas.

View File

@@ -0,0 +1,60 @@
# TASK-011-frontend-health-and-trailer-progressive-enhancement
## Goal
Realizar el primer enlace real entre frontend y backend en HLL Vietnam mediante una mejora progresiva y controlada, consultando el estado del backend y, de forma opcional, el endpoint placeholder del tráiler, sin romper la landing estática actual.
## Context
La landing actual ya funciona como página estática simple con logo, botón de Discord y tráiler embebido. El backend ya dispone de un bootstrap y de endpoints placeholder. El siguiente ajuste lógico es introducir una mejora progresiva mínima desde `frontend/assets/js/main.js`, manteniendo fallback estático cuando el backend no esté disponible.
## Steps
1. Revisar el frontend actual y el plan de consumo de datos.
2. Revisar el backend actual y el contrato frontend-backend.
3. Preparar en `main.js` una integración mínima con el backend, manteniendo la simplicidad del proyecto.
4. Consultar al menos:
- `GET /health`
5. Evaluar si también conviene consumir:
- `GET /api/trailer`
siempre manteniendo fallback estático en el HTML.
6. Añadir una señal visual mínima, discreta y no intrusiva para reflejar estado del backend si la integración lo justifica.
7. Asegurar que, si el backend no está disponible, la landing sigue funcionando exactamente como página estática.
8. No introducir todavía integración real de Discord ni servidores.
9. Mantener intacta la identidad visual general de la landing.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- docs/frontend-backend-contract.md
- docs/frontend-data-consumption-plan.md
- frontend/index.html
- frontend/assets/js/main.js
- frontend/assets/css/styles.css
- backend/app/main.py
- backend/app/routes.py
- backend/app/payloads.py
## Expected Files to Modify
- frontend/index.html
- frontend/assets/js/main.js
- frontend/assets/css/styles.css
- opcionalmente documentación mínima si fuera necesario reflejar el comportamiento progresivo
## Constraints
- No rediseñar la landing completa.
- No añadir nuevas secciones grandes.
- No integrar Discord real.
- No integrar servidores reales.
- No añadir librerías nuevas.
- No romper el funcionamiento estático actual.
- No hacer cambios destructivos.
- Mantener la mejora contenida y reversible.
## Validation
- La landing sigue funcionando aunque el backend no esté disponible.
- El frontend puede consultar `/health` correctamente cuando el backend está levantado.
- El comportamiento visual añadido es discreto y coherente.
- El tráiler y el botón de Discord siguen funcionando.
- La solución sienta la base para consumos reales futuros sin introducir complejidad excesiva.
## Change Budget
- Preferir menos de 4 archivos modificados.
- Preferir menos de 200 líneas cambiadas.

View File

@@ -0,0 +1,72 @@
# TASK-012-current-hll-servers-source-plan
## Goal
Definir la estrategia técnica y de producto para mostrar en la web de HLL Vietnam un bloque provisional de servidores actuales de Hell Let Loose, diferenciándolos claramente del futuro contexto Vietnam y sin implementar todavía integración real definitiva.
## Context
A día de hoy el foco temático del proyecto es HLL Vietnam, pero todavía no existen servidores reales de ese entorno para consumirlos como fuente del producto. Como solución provisional y útil para la comunidad, se quiere mostrar información de servidores actuales de Hell Let Loose, dejando claro que se trata de servidores del juego actual y no de HLL Vietnam. Antes de construir integración backend o UI final, hay que documentar fuente, campos, límites y estrategia de sustitución futura.
## Steps
1. Revisar la documentación actual del proyecto y la estrategia de datos ya definida.
2. Documentar el objetivo del bloque provisional de servidores actuales de HLL.
3. Definir claramente cómo debe presentarse en producto para evitar confusión con HLL Vietnam.
4. Identificar qué campos tiene sentido mostrar en esta fase. Incluir al menos:
- nombre del servidor
- estado online/offline
- jugadores actuales
- capacidad máxima
- mapa actual si está disponible
- región o etiqueta útil si existe
5. Documentar las posibles fuentes de esos datos, distinguiendo entre:
- fuente externa pública
- datos controlados o placeholder
- integración futura más robusta
6. Documentar riesgos y restricciones:
- disponibilidad de terceros
- CORS
- rate limits
- estabilidad del formato
- dependencia de scraping o fuentes no oficiales
7. Proponer una estrategia por fases:
- fase 1: payload controlado/mock con forma realista
- fase 2: adaptador backend para fuente externa o dataset controlado
- fase 3: sustitución por datos más cercanos a HLL Vietnam cuando existan
8. Dejar claro qué no se implementará todavía.
9. Actualizar documentación técnica del repositorio para que siguientes tasks tengan base clara.
## Files to Read First
- README.md
- AGENTS.md
- docs/project-overview.md
- docs/roadmap.md
- docs/decisions.md
- docs/frontend-backend-contract.md
- docs/discord-and-server-data-plan.md
- ai/repo-context.md
- ai/architecture-index.md
- backend/README.md
## Expected Files to Modify
- docs/roadmap.md
- docs/decisions.md
- ai/architecture-index.md
- opcionalmente un nuevo documento técnico si encaja mejor, por ejemplo:
- docs/current-hll-servers-source-plan.md
## Constraints
- No implementar integración real todavía.
- No modificar comportamiento visible del frontend.
- No añadir dependencias nuevas.
- No tocar base de datos.
- No hacer cambios destructivos.
- Mantener el resultado centrado en planificación técnica y de producto.
## Validation
- Existe una estrategia documentada para mostrar servidores actuales de Hell Let Loose como contenido provisional.
- Queda claramente diferenciada la identidad de HLL actual frente a HLL Vietnam.
- Se documentan campos, fuentes, riesgos y fases.
- La documentación sirve como base directa para una task backend y una task frontend posteriores.
## Change Budget
- Preferir menos de 5 archivos modificados o creados.
- Preferir menos de 220 líneas cambiadas.

View File

@@ -0,0 +1,57 @@
# TASK-013-backend-current-hll-servers-placeholder-adapter
## Goal
Preparar en el backend Python un adaptador placeholder limpio y consistente para exponer al frontend una lista provisional de servidores actuales de Hell Let Loose, sin integrar todavía una fuente externa real.
## Context
El backend ya dispone de un esqueleto API y de endpoints placeholder. Se ha decidido que la web podrá mostrar, de forma provisional, servidores actuales de Hell Let Loose mientras no existan datos reales de HLL Vietnam. Antes de conectar fuentes externas, conviene preparar un adaptador backend limpio que devuelva datos controlados con una estructura estable y preparada para sustitución futura.
## Steps
1. Revisar el backend actual, especialmente el endpoint relacionado con servidores y los payloads placeholder existentes.
2. Revisar la documentación del contrato frontend-backend y el plan de servidores actuales.
3. Definir una estructura clara y estable para el payload de servidores actuales de HLL.
4. Ajustar el backend para que `GET /api/servers` devuelva una respuesta coherente con ese modelo provisional.
5. Asegurar que el payload distinga claramente que se trata de servidores actuales de Hell Let Loose y no de HLL Vietnam, si esa distinción encaja en el modelo.
6. Mantener la implementación desacoplada para que más adelante pueda sustituirse la fuente sin romper al frontend.
7. Actualizar la documentación backend si el comportamiento real del endpoint cambia respecto a lo documentado.
8. Mantener el alcance estricto: adaptador placeholder estable, no integración real.
## Files to Read First
- AGENTS.md
- docs/frontend-backend-contract.md
- docs/discord-and-server-data-plan.md
- docs/current-hll-servers-source-plan.md
- ai/repo-context.md
- ai/architecture-index.md
- backend/README.md
- backend/app/__init__.py
- backend/app/main.py
- backend/app/routes.py
- backend/app/payloads.py
- backend/app/config.py
## Expected Files to Modify
- backend/app/routes.py
- backend/app/payloads.py
- backend/app/main.py
- backend/README.md
- opcionalmente documentación contractual o técnica si necesita alineación menor
## Constraints
- No integrar todavía una fuente externa real.
- No añadir scraping.
- No añadir base de datos.
- No tocar frontend en esta task.
- No introducir complejidad innecesaria.
- No hacer cambios destructivos.
- Mantener la API simple, estable y preparada para evolución posterior.
## Validation
- `GET /api/servers` devuelve un payload placeholder más consistente y útil.
- La respuesta está preparada para consumo frontend.
- La estructura deja claro que el contenido actual es provisional y basado en servidores actuales de HLL.
- La documentación backend refleja el estado real del endpoint si cambió.
## Change Budget
- Preferir menos de 5 archivos modificados.
- Preferir menos de 180 líneas cambiadas.

View File

@@ -0,0 +1,61 @@
# TASK-014-landing-current-hll-servers-panel
## Goal
Añadir a la landing de HLL Vietnam un panel visual de servidores actuales de Hell Let Loose, alimentado por el backend placeholder, dejando claro que se trata de contenido provisional mientras no existan datos reales de HLL Vietnam.
## Context
La landing ya dispone de una mejora progresiva frontend-backend y el backend puede exponer endpoints placeholder. El proyecto necesita empezar a mostrar información más útil y dinámica, pero sin fingir que ya existen servidores de HLL Vietnam. Esta task debe introducir un bloque visual controlado que muestre servidores actuales de HLL como referencia provisional para la comunidad.
## Steps
1. Revisar la landing actual, los estilos y la mejora progresiva ya implementada en frontend.
2. Revisar el payload real o previsto de `GET /api/servers`.
3. Añadir un bloque visual nuevo en la landing para mostrar servidores actuales de Hell Let Loose.
4. Hacer que el frontend consuma `GET /api/servers` de forma progresiva, manteniendo fallback seguro si el backend no está disponible.
5. Mostrar de forma clara y ordenada, como mínimo:
- nombre del servidor
- estado
- jugadores
- capacidad máxima
- mapa si está disponible
6. Añadir una etiqueta o texto breve que deje claro que son servidores actuales de Hell Let Loose usados como referencia provisional.
7. Mantener coherencia visual con la identidad militar sobria del proyecto.
8. Asegurar que la landing siga funcionando si el backend no responde o si no hay datos.
9. No tocar todavía integración real de Discord ni fuentes externas reales.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- docs/frontend-backend-contract.md
- docs/frontend-data-consumption-plan.md
- docs/current-hll-servers-source-plan.md
- frontend/index.html
- frontend/assets/js/main.js
- frontend/assets/css/styles.css
- backend/app/routes.py
- backend/app/payloads.py
## Expected Files to Modify
- frontend/index.html
- frontend/assets/js/main.js
- frontend/assets/css/styles.css
- opcionalmente documentación mínima si fuera necesario reflejar el nuevo bloque
## Constraints
- No rediseñar completamente la landing.
- No añadir librerías nuevas.
- No romper el fallback estático actual.
- No presentar esos servidores como si fueran de HLL Vietnam.
- No integrar fuentes externas reales directamente desde frontend.
- No hacer cambios destructivos.
- Mantener la mejora acotada, clara y coherente con la fase actual.
## Validation
- La landing muestra un panel de servidores actuales de HLL de forma clara.
- El panel obtiene datos desde `GET /api/servers` cuando el backend está disponible.
- Si el backend no está disponible, la landing sigue funcionando sin romperse.
- La UI deja claro que se trata de contenido provisional.
- La integración visual es coherente con el resto de la página.
## Change Budget
- Preferir menos de 4 archivos modificados.
- Preferir menos de 220 líneas cambiadas.

View File

@@ -0,0 +1,66 @@
# TASK-015-landing-visual-system-pass
## Goal
Realizar una pasada de sistema visual sobre la landing de HLL Vietnam para reforzar identidad, consistencia, contraste y jerarquia general, manteniendo el alcance actual del producto y sin cambiar la arquitectura funcional.
## Context
La landing ya cuenta con logo, trailer, CTA de Discord, estado de backend y panel provisional de servidores actuales de Hell Let Loose. La base funcional existe, pero ahora hace falta una pasada centrada unicamente en calidad visual para que la web tenga una presentacion mas solida, coherente y atractiva dentro del tono militar/tactico del proyecto.
## Steps
1. Revisar la composicion visual global de la landing actual.
2. Revisar la paleta actual, contraste, espaciados, tipografias, contenedores y ritmo vertical.
3. Refinar el sistema visual general sin redisenar por completo la pagina.
4. Mejorar:
- jerarquia tipografica
- espaciado entre bloques
- consistencia entre tarjetas y paneles
- contraste entre fondo, texto y elementos destacados
- sensacion visual militar sobria y cinematografica
5. Mantener una estetica oscura, tactica y limpia, evitando recargar la pagina.
6. Asegurar que todos los bloques principales compartan un lenguaje visual coherente.
7. Validar que el resultado siga funcionando bien en escritorio y movil.
8. No cambiar el alcance funcional del producto.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- docs/project-overview.md
- frontend/index.html
- frontend/assets/css/styles.css
- frontend/assets/js/main.js
## Expected Files to Modify
- frontend/index.html
- frontend/assets/css/styles.css
## Constraints
- No anadir nuevas funcionalidades.
- No cambiar endpoints ni logica backend.
- No introducir librerias nuevas.
- No cambiar el flujo funcional de la landing.
- No reescribir toda la estructura HTML salvo ajustes razonables para mejorar composicion.
- No hacer cambios destructivos.
- Mantener el diseno dentro del tono HLL Vietnam.
## Validation
- La landing tiene una apariencia mas coherente y profesional.
- La jerarquia visual es mas clara.
- El contraste y la legibilidad mejoran.
- La identidad militar/tactica queda reforzada.
- No se rompe ninguna funcionalidad existente.
- La landing sigue viendose correctamente en movil y escritorio.
## Change Budget
- Preferir menos de 3 archivos modificados.
- Preferir menos de 220 lineas cambiadas.
## Outcome
- Se reforzo el sistema visual general de la landing sin cambiar su alcance funcional.
- Se ajusto la jerarquia visual entre hero, panel de trailer y panel de servidores para que compartan mejor lenguaje visual.
- Se mejoraron contraste, ritmo vertical, bordes, sombras y consistencia de contenedores dentro del tono militar sobrio del proyecto.
## Validation Notes
- `git diff --name-only -- frontend/index.html frontend/assets/css/styles.css` devuelve solo los archivos esperados por la task.
- Se reviso el CSS para mantener compatibilidad responsive en escritorio y movil mediante los breakpoints existentes.
- No se cambiaron endpoints, logica backend ni comportamiento funcional de la landing.
- No hay pruebas automaticas configuradas para este ajuste visual; la validacion disponible en esta task es revision de alcance y consistencia del marcado y CSS.

View File

@@ -0,0 +1,73 @@
# TASK-016-hero-logo-and-trailer-cinematic-polish
## Goal
Mejorar visualmente el bloque principal de la landing de HLL Vietnam, con foco en hero, logo y trailer, para darle una presencia mas cinematografica, mas inmersiva y mas propia de una comunidad tactica.
## Context
El usuario quiere priorizar el ajuste visual. El hero actual ya funciona, pero debe ganar impacto sin perder simplicidad. El logo debe sentirse mas protagonista, el bloque del trailer debe integrarse mejor en la composicion y el conjunto debe transmitir mejor el tono del proyecto.
## Steps
1. Revisar la composicion actual del hero.
2. Revisar el tratamiento visual del logo:
- tamano
- posicion
- marco
- sombra
- integracion con el fondo
3. Revisar el tratamiento visual del trailer:
- contenedor
- marco
- separacion respecto al resto
- equilibrio con el CTA y el titulo
4. Mejorar la presencia del bloque principal sin anadir secciones nuevas.
5. Reforzar:
- impacto inicial
- sensacion de identidad
- claridad del CTA principal
- integracion visual entre logo, titulo y video
6. Mantener el diseno sobrio, sin caer en efectos exagerados.
7. Validar que la composicion siga siendo responsive y estable.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- frontend/index.html
- frontend/assets/css/styles.css
- frontend/assets/js/main.js
## Expected Files to Modify
- frontend/index.html
- frontend/assets/css/styles.css
## Constraints
- No cambiar la ruta del logo.
- No cambiar el enlace de Discord.
- No cambiar el enlace del trailer.
- No anadir nuevas funcionalidades.
- No romper el fallback actual.
- No usar librerias nuevas.
- No hacer cambios destructivos.
- Mantener la landing simple.
## Validation
- El hero tiene mas presencia visual.
- El logo se percibe mejor integrado y mas protagonista.
- El trailer se siente mejor enmarcado dentro del diseno.
- El CTA sigue siendo claro y visible.
- El resultado mantiene coherencia con el tono militar/tactico del proyecto.
- La composicion funciona en desktop y mobile.
## Change Budget
- Preferir menos de 2 archivos modificados.
- Preferir menos de 180 lineas cambiadas.
## Outcome
- Se reorganizo el bloque principal para agrupar mejor estado backend y CTA sin cambiar su funcionamiento.
- Se reforzo el tratamiento visual del logo con un marco mas trabajado, sombras mas controladas y una mejor integracion con el fondo del hero.
- Se ajusto el panel de trailer para que se sienta mas cinematografico y mejor conectado con la identidad visual del hero.
## Validation Notes
- `git diff --name-only -- frontend/index.html frontend/assets/css/styles.css` devuelve solo los archivos esperados por la task.
- Se confirmo que no se cambiaron la ruta del logo, el enlace de Discord ni la URL del trailer.
- La task se mantuvo en cambios de composicion y estilo; no se altero el fallback ni la logica existente.
- No hay pruebas automaticas especificas para esta composicion; la validacion disponible es revision estructural de HTML/CSS y del alcance del diff.

View File

@@ -0,0 +1,69 @@
# TASK-017-server-panel-visual-polish-and-responsive-pass
## Goal
Pulir visualmente el panel de servidores actuales de Hell Let Loose y mejorar su comportamiento responsive para que se integre mejor con el resto de la landing y se vea claro, ordenado y fiable.
## Context
El panel de servidores ya existe y consume datos del backend placeholder con fallback estatico. Ahora hace falta una pasada especifica de UI para que las tarjetas o elementos del panel tengan una presentacion mas clara y mas profesional, sin tocar todavia estrategia de copy ni fuentes reales de datos.
## Steps
1. Revisar el marcado actual del panel de servidores en la landing.
2. Revisar como se renderizan los datos desde frontend.
3. Mejorar visualmente:
- tarjetas o filas de servidor
- jerarquia de nombre, estado, jugadores y mapa
- badges o indicadores de estado
- separacion entre items
- integracion con el resto de la landing
4. Mejorar el comportamiento responsive del panel:
- anchuras
- apilado
- legibilidad en movil
- consistencia de espaciados
5. Mantener claro que se trata de un bloque provisional sin tocar todavia el copy estrategico en profundidad.
6. Respetar el fallback actual si el backend no responde.
7. No cambiar el alcance funcional del panel.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- frontend/index.html
- frontend/assets/css/styles.css
- frontend/assets/js/main.js
- backend/app/payloads.py
## Expected Files to Modify
- frontend/index.html
- frontend/assets/css/styles.css
- frontend/assets/js/main.js
## Constraints
- No cambiar el contrato del backend.
- No tocar payloads backend.
- No anadir librerias nuevas.
- No romper el fallback estatico.
- No presentar estos servidores como si fueran de HLL Vietnam.
- No hacer cambios destructivos.
- Mantener el ajuste centrado en UI y responsive.
## Validation
- El panel de servidores se ve mas limpio y claro.
- La lectura de estado, jugadores y mapa mejora.
- El panel responde bien en movil y escritorio.
- El fallback sigue funcionando.
- La integracion visual con la landing es mejor que antes.
## Change Budget
- Preferir menos de 3 archivos modificados.
- Preferir menos de 200 lineas cambiadas.
## Outcome
- Se mejoro la jerarquia visual de cada servidor con identidad, estado mas legible y mejor separacion entre bloques de datos.
- Se anadio un indicador visual de ocupacion basado en los datos ya disponibles sin cambiar el contrato del backend.
- Se alineo el markup estatico de `index.html` con el render dinamico de `main.js` para mantener el mismo acabado cuando el backend esta disponible o no.
## Validation Notes
- `git diff --name-only -- frontend/index.html frontend/assets/css/styles.css frontend/assets/js/main.js` devuelve solo los archivos esperados por la task.
- `node --check frontend\\assets\\js\\main.js` se ejecuto correctamente.
- No se modifico `backend/app/payloads.py` ni el contrato del backend.
- El fallback estatico se conserva porque el HTML base y el render dinamico usan la misma estructura visual del panel.

View File

@@ -0,0 +1,74 @@
# TASK-018-current-hll-data-ingestion-plan
## Goal
Definir la estrategia técnica de ingesta de datos para Hell Let Loose actual, usándolo como banco de pruebas del proyecto HLL Vietnam, con foco en servidores, snapshots e históricos básicos sin implementar todavía la ingesta real completa.
## Context
La landing ya muestra servidores actuales de HLL como contenido provisional y el backend ya dispone de una base API placeholder. El siguiente paso lógico es fijar cómo se obtendrán y normalizarán datos reales o semirrealistas de HLL actual para validar la arquitectura que más adelante podrá reutilizarse con HLL Vietnam.
## Steps
1. Revisar la documentación actual del proyecto relacionada con servidores, backend y evolución de datos.
2. Definir qué tipos de datos se quieren ingerir inicialmente desde HLL actual. Incluir al menos:
- listado de servidores
- estado online/offline
- jugadores actuales
- capacidad máxima
- mapa actual si está disponible
- timestamp de captura
3. Definir el concepto de snapshot de servidor y cómo se usará para construir histórico.
4. Documentar las posibles fuentes técnicas de ingesta para HLL actual, distinguiendo entre:
- datos controlados/mock
- fuente externa pública
- consulta directa de servidor o capa intermedia
5. Documentar riesgos y límites:
- disponibilidad de terceros
- cambios de formato
- rate limits
- latencia
- CORS
- fiabilidad de datos
- dependencia de scraping o APIs no oficiales
6. Proponer la arquitectura de ingesta por fases:
- fase 1: payload controlado y estructura estable
- fase 2: colector de snapshots con fuente real o casi real
- fase 3: explotación histórica y estadísticas básicas
7. Dejar claro qué no se implementará todavía.
8. Actualizar la documentación técnica del repositorio para servir de base a las siguientes tasks de base de datos y colector.
## Files to Read First
- README.md
- AGENTS.md
- docs/project-overview.md
- docs/roadmap.md
- docs/decisions.md
- docs/discord-and-server-data-plan.md
- docs/current-hll-servers-source-plan.md
- docs/frontend-backend-contract.md
- ai/repo-context.md
- ai/architecture-index.md
- backend/README.md
## Expected Files to Modify
- docs/roadmap.md
- docs/decisions.md
- ai/architecture-index.md
- opcionalmente un nuevo documento técnico si encaja mejor, por ejemplo:
- docs/current-hll-data-ingestion-plan.md
## Constraints
- No implementar todavía ingesta real.
- No modificar comportamiento visible del frontend.
- No añadir base de datos funcional en esta task.
- No añadir dependencias nuevas.
- No hacer cambios destructivos.
- Mantener el resultado centrado en planificación técnica reutilizable para futuro HLL Vietnam.
## Validation
- Existe una estrategia documentada de ingesta para HLL actual como banco de pruebas.
- Quedan definidos snapshots, fuentes posibles, riesgos y fases.
- La documentación deja claro cómo se conectará este trabajo con almacenamiento e históricos.
- La base sirve para una siguiente task de esquema de base de datos.
## Change Budget
- Preferir menos de 5 archivos modificados o creados.
- Preferir menos de 240 líneas cambiadas.

View File

@@ -0,0 +1,65 @@
# TASK-019-stats-database-schema-foundation
## Goal
Diseñar y dejar preparada la base del esquema de almacenamiento para snapshots y estadísticas iniciales de servidores de HLL actual, con una modelización genérica que pueda reutilizarse en el futuro para HLL Vietnam.
## Context
El proyecto necesita pasar de payloads placeholder a una arquitectura capaz de almacenar históricos. Antes de implementar colectores reales, es necesario definir un esquema de datos claro, neutro y extensible para snapshots de servidores y primeras métricas agregadas.
## Steps
1. Revisar la documentación técnica actual sobre backend, contratos y plan de ingesta.
2. Definir las entidades mínimas necesarias para la primera fase de persistencia. Incluir al menos:
- game o source context si aplica
- servers
- server_snapshots
- posibles tablas de agregación inicial o vistas documentadas para estadísticas
3. Asegurar que el naming sea genérico y reutilizable, evitando acoplar el modelo a Vietnam.
4. Definir para cada entidad:
- propósito
- campos principales
- claves
- relaciones
- timestamps
5. Documentar qué datos deben persistirse por snapshot y cuáles pueden derivarse después.
6. Si el backend ya usa o prevé una tecnología concreta para persistencia, reflejarlo con claridad. Si aún no, documentar una base neutra y coherente.
7. Añadir o actualizar documentación técnica del repositorio para dejar claro el modelo inicial de almacenamiento.
8. Mantener el alcance en diseño y preparación, no en implementación completa de persistencia productiva.
## Files to Read First
- AGENTS.md
- docs/project-overview.md
- docs/roadmap.md
- docs/decisions.md
- docs/frontend-backend-contract.md
- docs/current-hll-data-ingestion-plan.md
- ai/repo-context.md
- ai/architecture-index.md
- backend/README.md
- backend/app/config.py
- backend/app/main.py
## Expected Files to Modify
- docs/decisions.md
- ai/architecture-index.md
- backend/README.md
- opcionalmente un nuevo documento técnico si encaja mejor, por ejemplo:
- docs/stats-database-schema-foundation.md
- opcionalmente archivos base de estructura si el repositorio ya está preparado para ello, pero sin implementar una base de datos completa
## Constraints
- No implementar todavía la base de datos completa.
- No añadir migraciones productivas si la decisión técnica aún no está consolidada.
- No tocar frontend.
- No añadir integraciones reales de servidor.
- No hacer cambios destructivos.
- Mantener el modelo simple, genérico y preparado para crecer.
## Validation
- Existe un esquema de almacenamiento inicial claro para snapshots y estadísticas básicas.
- El naming es reutilizable para HLL actual y futuro HLL Vietnam.
- La documentación deja claro qué se persistirá primero y por qué.
- El resultado sirve como base directa para una siguiente task de colector de snapshots.
## Change Budget
- Preferir menos de 5 archivos modificados o creados.
- Preferir menos de 220 líneas cambiadas.

View File

@@ -0,0 +1,61 @@
# TASK-020-server-snapshot-collector-bootstrap
## Goal
Preparar un bootstrap técnico mínimo para un colector de snapshots de servidores en el backend Python, dejando la estructura lista para capturar y normalizar datos de HLL actual sin implementar todavía una ingesta completa de producción.
## Context
Tras definir la estrategia de ingesta y el esquema base de almacenamiento, el siguiente paso es dejar preparada la estructura mínima de un colector en backend. Esta task no debe resolver toda la persistencia ni depender de una fuente definitiva, pero sí debe sentar la base para un flujo futuro de captura periódica.
## Steps
1. Revisar la documentación de ingesta y esquema de almacenamiento.
2. Revisar la estructura actual del backend Python.
3. Crear una estructura mínima y clara para un colector o servicio de snapshots dentro de `backend/app/`.
4. Definir interfaces o funciones base para:
- obtener datos crudos de una fuente
- normalizar esos datos
- producir un snapshot consistente
5. Si encaja con la fase actual, permitir una ejecución manual o de desarrollo del colector usando datos controlados.
6. Mantener la implementación desacoplada para que la fuente real pueda sustituirse más adelante sin romper el resto del backend.
7. Actualizar la documentación backend para explicar el papel del colector y cómo encaja con futuras estadísticas.
8. Mantener el alcance estricto: bootstrap técnico del colector, no pipeline completo de producción.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- ai/architecture-index.md
- docs/current-hll-data-ingestion-plan.md
- docs/stats-database-schema-foundation.md
- backend/README.md
- backend/app/__init__.py
- backend/app/config.py
- backend/app/main.py
- backend/app/routes.py
- backend/app/payloads.py
## Expected Files to Modify
- backend/README.md
- backend/app/__init__.py
- opcionalmente archivos nuevos dentro de `backend/app/` si mejoran claridad, por ejemplo:
- backend/app/collector.py
- backend/app/normalizers.py
- backend/app/snapshots.py
- opcionalmente documentación técnica relacionada si requiere alineación menor
## Constraints
- No implementar todavía una ingesta real completa.
- No depender de scraping productivo.
- No introducir una base de datos completa en esta task.
- No tocar frontend.
- No añadir complejidad innecesaria.
- No hacer cambios destructivos.
- Mantener la estructura clara y pequeña.
## Validation
- El backend contiene una base mínima para un colector de snapshots.
- La estructura separa captura, normalización y snapshot de forma razonable para la fase actual.
- La documentación backend refleja el nuevo estado.
- El resultado prepara el terreno para una siguiente task de persistencia real o ejecución periódica.
## Change Budget
- Preferir menos de 6 archivos modificados o creados.
- Preferir menos de 240 líneas cambiadas.

View File

@@ -0,0 +1,73 @@
# TASK-021-snapshot-persistence-bootstrap
## Goal
Preparar una persistencia local minima para snapshots de servidores en el backend Python, de forma que los datos recogidos durante las pruebas con HLL actual puedan guardarse y reutilizarse para consultas historicas posteriores.
## Context
El proyecto ya dispone de plan de ingesta, modelo logico de almacenamiento y bootstrap de colector. El siguiente paso es dejar de depender exclusivamente de estructuras temporales o payloads controlados y empezar a guardar snapshots de forma persistente para validar el circuito real de estadisticas.
## Steps
1. Revisar la documentacion actual de ingesta, esquema logico y colector.
2. Definir una estrategia de persistencia local minima adecuada para esta fase de pruebas.
3. Implementar una capa pequena de persistencia alineada con las entidades ya definidas logicamente:
- game_sources
- servers
- server_snapshots
4. Mantener la implementacion simple, reutilizable y desacoplada de una base de datos de produccion futura.
5. Hacer que el colector pueda guardar snapshots reales o controlados en esa persistencia local.
6. Documentar como inicializar y usar esta persistencia en desarrollo.
7. No introducir todavia consultas historicas complejas ni visualizacion en frontend.
8. Mantener el alcance centrado en almacenamiento basico funcional.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- ai/architecture-index.md
- docs/current-hll-data-ingestion-plan.md
- docs/stats-database-schema-foundation.md
- backend/README.md
- backend/app/__init__.py
- backend/app/config.py
- backend/app/collector.py
- backend/app/normalizers.py
- backend/app/snapshots.py
## Expected Files to Modify
- backend/README.md
- backend/app/__init__.py
- backend/app/config.py
- backend/app/collector.py
- backend/app/snapshots.py
- opcionalmente archivos nuevos dentro de backend/app/ si mejoran la separacion de persistencia, por ejemplo:
- backend/app/storage.py
- backend/app/repository.py
## Constraints
- No implementar todavia integraciones reales complejas de terceros.
- No tocar frontend.
- No anadir una infraestructura de produccion sobredimensionada.
- No hacer cambios destructivos.
- Mantener la persistencia simple y util para desarrollo local.
## Validation
- El backend puede guardar snapshots de servidores en una persistencia local real.
- La persistencia sigue el modelo logico documentado.
- La documentacion explica como usar esta base minima en local.
- El resultado prepara el terreno para consultas historicas inmediatas.
## Change Budget
- Preferir menos de 7 archivos modificados o creados.
- Preferir menos de 260 lineas cambiadas.
## Outcome
- Se anadio `backend/app/storage.py` con una persistencia SQLite minima basada en libreria estandar.
- El colector puede persistir snapshots controlados en `game_sources`, `servers` y `server_snapshots`.
- La configuracion local expone `HLL_BACKEND_STORAGE_PATH` para cambiar la ruta del archivo SQLite sin fijar una decision de produccion.
- `backend/README.md` documenta la inicializacion y uso de la persistencia local.
## Validation Result
- Ejecutado: `python -m app.collector`
- Resultado: lote de 3 snapshots persistido correctamente en SQLite local de desarrollo.
## Decision Notes
- Se eligio SQLite porque cubre persistencia local real con dependencias cero y mantiene abierta la migracion futura a otra tecnologia de almacenamiento.

View File

@@ -0,0 +1,77 @@
# TASK-022-historical-server-query-api
## Goal
Exponer desde el backend una primera API de consulta historica para snapshots de servidores, permitiendo recuperar el ultimo estado conocido y una evolucion basica a partir de la persistencia local ya preparada.
## Context
Una vez que los snapshots puedan guardarse, el siguiente paso es poder consultarlos de forma estructurada. Esta task debe convertir la persistencia inicial en una API util para comprobar que el flujo de estadisticas ya funciona extremo a extremo.
## Steps
1. Revisar la capa de persistencia de snapshots y el backend actual.
2. Definir endpoints minimos de consulta historica, por ejemplo:
- `GET /api/servers/latest`
- `GET /api/servers/history`
- `GET /api/servers/{id}/history`
3. Alinear el formato de respuesta con las convenciones del backend existente.
4. Hacer que las respuestas devuelvan como minimo:
- timestamps
- ultimo estado conocido
- jugadores
- capacidad
- mapa si esta disponible
- contexto o fuente si aplica
5. Mantener la implementacion simple y enfocada a validacion tecnica.
6. Actualizar la documentacion del backend y del contrato si fuera necesario.
7. No introducir todavia analitica avanzada ni agregaciones pesadas.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- ai/architecture-index.md
- docs/frontend-backend-contract.md
- docs/current-hll-data-ingestion-plan.md
- docs/stats-database-schema-foundation.md
- backend/README.md
- backend/app/main.py
- backend/app/routes.py
- backend/app/payloads.py
- backend/app/collector.py
- backend/app/snapshots.py
- archivos de persistencia creados en la task anterior
## Expected Files to Modify
- backend/app/main.py
- backend/app/routes.py
- backend/app/payloads.py
- backend/README.md
- opcionalmente documentacion tecnica contractual si requiere alineacion menor
## Constraints
- No tocar frontend en esta task.
- No anadir visualizacion nueva.
- No introducir complejidad innecesaria.
- No hacer cambios destructivos.
- Mantener la API clara, estable y centrada en historico basico.
## Validation
- Existen endpoints historicos minimos funcionales.
- El backend puede devolver ultimo snapshot y una historia basica de snapshots.
- La documentacion refleja correctamente el nuevo estado.
- El resultado prepara el terreno para una primera visualizacion en frontend.
## Change Budget
- Preferir menos de 6 archivos modificados o creados.
- Preferir menos de 240 lineas cambiadas.
## Outcome
- Se anadieron lecturas minimas desde SQLite para ultimo snapshot por servidor, historial agregado e historial por servidor.
- `backend/app/routes.py` resuelve `GET /api/servers/latest`, `GET /api/servers/history` y `GET /api/servers/{id}/history`.
- `backend/app/payloads.py` mantiene el formato JSON del backend y devuelve errores controlados para parametros invalidos.
- `backend/README.md` y `docs/frontend-backend-contract.md` documentan los nuevos endpoints.
## Validation Result
- Ejecutado: `python -c "from app.collector import collect_server_snapshots; from app.routes import resolve_get_payload; collect_server_snapshots(persist=True); ..."`
- Resultado: los tres endpoints historicos devolvieron datos persistidos y `limit=999` respondio con `status: "error"`.
## Decision Notes
- Se mantuvo la API sobre la persistencia SQLite local ya existente, evitando una capa analitica separada hasta que el frontend necesite algo mas que consultas historicas basicas.

View File

@@ -0,0 +1,68 @@
# TASK-023-server-stats-preview-panel
## Goal
Anadir a la web una primera visualizacion de estadisticas de servidores basada en datos persistidos, mostrando una vista previa util y controlada del historico sin convertir todavia la landing en una aplicacion compleja.
## Context
La landing ya dispone de un panel provisional de servidores y el backend va a disponer de persistencia local y consultas historicas minimas. Esta task debe conectar ambos mundos con una primera visualizacion ligera que confirme que el pipeline completo funciona.
## Steps
1. Revisar el frontend actual, el panel de servidores existente y el plan de consumo de datos.
2. Revisar los nuevos endpoints historicos del backend.
3. Disenar una mejora progresiva en frontend que pueda mostrar una vista previa de estadisticas sin romper la landing actual.
4. Mostrar como minimo algunos indicadores utiles, por ejemplo:
- ultimo snapshot por servidor
- hora de ultima actualizacion
- evolucion basica de poblacion o actividad reciente
5. Mantener fallback seguro si el backend o los endpoints historicos no estan disponibles.
6. Integrar visualmente el bloque con el diseno actual sin redisenar toda la pagina.
7. No anadir todavia dashboards complejos, graficas pesadas ni navegacion nueva.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- docs/frontend-data-consumption-plan.md
- docs/frontend-backend-contract.md
- frontend/index.html
- frontend/assets/js/main.js
- frontend/assets/css/styles.css
- backend/app/routes.py
- backend/app/payloads.py
- documentacion de endpoints historicos creada en la task anterior
## Expected Files to Modify
- frontend/index.html
- frontend/assets/js/main.js
- frontend/assets/css/styles.css
- opcionalmente documentacion minima si fuera necesario reflejar el nuevo bloque o comportamiento
## Constraints
- No redisenar completamente la landing.
- No anadir librerias nuevas.
- No romper el fallback actual.
- No introducir analitica visual compleja.
- No hacer cambios destructivos.
- Mantener la mejora contenida, clara y alineada con la fase actual.
## Validation
- La web puede mostrar una primera vista previa de estadisticas de servidores usando datos persistidos.
- El frontend sigue funcionando si el backend no esta disponible.
- La mejora visual se integra con la landing actual.
- El resultado demuestra que el flujo captura -> persistencia -> consulta -> visualizacion ya funciona.
## Change Budget
- Preferir menos de 4 archivos modificados.
- Preferir menos de 220 lineas cambiadas.
## Outcome
- El panel de servidores ahora puede mostrar una vista previa historica con resumen, ultima actualizacion y tendencia reciente por servidor.
- `frontend/assets/js/main.js` mantiene el bloque estatico como fallback y solo sustituye el contenido cuando los endpoints historicos responden correctamente.
- `frontend/index.html` anade un contenedor ligero para el resumen historico.
- `frontend/assets/css/styles.css` integra la nueva vista previa con el lenguaje visual tactico existente.
## Validation Result
- Ejecutado: `node --check frontend/assets/js/main.js`
- Resultado: sintaxis valida en el script principal del frontend.
## Decision Notes
- Se mantuvo la mejora dentro del panel de servidores ya existente para evitar una nueva seccion o un dashboard separado en esta fase.

View File

@@ -0,0 +1,77 @@
# TASK-024-a2s-client-bootstrap
## Goal
Preparar un cliente A2S mínimo en el backend Python para consultar servidores de Hell Let Loose actual mediante query pública, obteniendo metadata básica reutilizable por el colector de snapshots.
## Context
El proyecto ya dispone de colector bootstrap, persistencia local y consultas históricas mínimas. El siguiente paso es introducir una fuente real de datos en vivo. Hell Let Loose usa Steam A2S para metadata de servidor a través del query port, por lo que esta task debe preparar la base técnica para consultar servidores reales sin depender todavía de integraciones administrativas o scoreboards avanzados.
## Steps
1. Revisar el backend actual, especialmente el colector y las funciones de normalización.
2. Revisar la documentación técnica de ingesta y el modelo de snapshots.
3. Preparar un cliente A2S mínimo capaz de consultar al menos la información equivalente a:
- nombre del servidor
- mapa actual
- jugadores actuales
- capacidad máxima
4. Mantener la implementación desacoplada para que futuras consultas de players o rules puedan añadirse después sin romper la base.
5. Normalizar la salida del cliente A2S hacia el modelo interno ya usado por el proyecto.
6. Asegurar que la implementación maneje fallos básicos de red o timeouts de forma controlada.
7. Actualizar la documentación backend para reflejar que existe una fuente A2S real de prueba.
8. Mantener el alcance centrado en bootstrap del cliente, no en pipeline completo.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- ai/architecture-index.md
- docs/current-hll-data-ingestion-plan.md
- docs/stats-database-schema-foundation.md
- backend/README.md
- backend/app/__init__.py
- backend/app/config.py
- backend/app/collector.py
- backend/app/normalizers.py
- backend/app/snapshots.py
- backend/app/storage.py
## Expected Files to Modify
- backend/README.md
- backend/app/__init__.py
- backend/app/normalizers.py
- backend/app/collector.py
- opcionalmente archivos nuevos dentro de backend/app/ si mejoran claridad, por ejemplo:
- backend/app/a2s_client.py
- backend/app/a2s_protocol.py
## Constraints
- No implementar todavía scraping de terceros.
- No tocar frontend.
- No añadir base de datos nueva.
- No hacer cambios destructivos.
- Mantener la implementación pequeña, clara y orientada a desarrollo local.
## Validation
- Existe un cliente A2S mínimo reutilizable desde el backend.
- El cliente puede obtener metadata básica de servidor.
- La salida se normaliza al modelo interno del proyecto.
- La documentación backend refleja el nuevo estado.
## Change Budget
- Preferir menos de 6 archivos modificados o creados.
- Preferir menos de 260 líneas cambiadas.
## Outcome
- Se anadio un cliente A2S minimo en `backend/app/a2s_client.py` usando solo libreria estandar para consultar `A2S_INFO`.
- `backend/app/normalizers.py` ahora puede convertir una respuesta A2S al modelo interno de snapshots ya usado por el proyecto.
- `backend/app/collector.py` expone `fetch_a2s_probe()` como adaptador pequeno reutilizable para la siguiente task de integracion del pipeline.
- `backend/app/__init__.py` mantiene acceso ligero a `query_server_info()` y `fetch_a2s_probe()` sin acoplar la carga del paquete al CLI del cliente.
- `backend/README.md` documenta el nuevo estado y una prueba manual local del cliente.
## Validation Result
- Ejecutado: `python -m compileall backend/app`
- Resultado: compilacion correcta de los modulos del backend.
- Ejecutado: `python -m app.a2s_client --help`
- Resultado: CLI disponible sin warnings y con los argumentos esperados.
## Decision Notes
- Se mantuvo la implementacion en libreria estandar con UDP directo para evitar introducir dependencias antes de validar targets y configuracion en las siguientes tasks.

View File

@@ -0,0 +1,67 @@
# TASK-025-a2s-source-configuration-and-target-registry
## Goal
Definir y preparar una configuración limpia para registrar servidores objetivo de prueba consultables por A2S, de forma que el backend pueda trabajar con una lista controlada de targets sin acoplarse a valores hardcodeados.
## Context
Una vez que exista un cliente A2S mínimo, el proyecto necesita una manera clara de declarar qué servidores de HLL actual se usarán como fuentes de prueba. Esta task debe dejar preparada una configuración o registro simple de targets A2S, reutilizable por el colector y fácil de adaptar en desarrollo.
## Steps
1. Revisar la configuración actual del backend.
2. Definir una forma clara de registrar targets A2S de prueba, incluyendo al menos:
- nombre amigable
- host o IP
- query port
- contexto o etiqueta de fuente
3. Mantener el diseño desacoplado del resto del backend y fácil de ampliar.
4. Permitir que el colector lea esa configuración sin depender de valores hardcodeados dentro de la lógica principal.
5. Documentar cómo añadir, quitar o modificar servidores de prueba en entorno local.
6. Mantener el alcance en configuración y registro de targets, no en analítica avanzada.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- docs/current-hll-data-ingestion-plan.md
- backend/README.md
- backend/app/config.py
- backend/app/collector.py
- backend/app/storage.py
- backend/app/a2s_client.py si ya existe por la task anterior
## Expected Files to Modify
- backend/README.md
- backend/app/config.py
- backend/app/collector.py
- opcionalmente archivos nuevos dentro de backend/app/ si mejoran claridad, por ejemplo:
- backend/app/server_targets.py
## Constraints
- No tocar frontend.
- No introducir infraestructura compleja.
- No añadir dependencias innecesarias.
- No hacer cambios destructivos.
- Mantener la configuración simple y orientada a pruebas.
## Validation
- El backend dispone de una lista configurable de targets A2S.
- El colector puede leer esos targets sin depender de constantes dispersas.
- La documentación deja claro cómo configurar servidores de prueba.
## Change Budget
- Preferir menos de 5 archivos modificados o creados.
- Preferir menos de 180 líneas cambiadas.
## Outcome
- Se anadio `backend/app/server_targets.py` con un registro pequeno de targets A2S y una carga desacoplada en forma de dataclass.
- `backend/app/config.py` ahora soporta `HLL_BACKEND_A2S_TARGETS` para sobrescribir la lista con un array JSON en desarrollo local.
- `backend/app/collector.py` puede resolver los targets configurados mediante `fetch_configured_a2s_probes()` sin depender de valores hardcodeados en su logica principal.
- `backend/README.md` documenta la ubicacion del registro y como anadir, quitar o modificar targets de prueba.
## Validation Result
- Ejecutado: `python -m compileall backend/app`
- Resultado: compilacion correcta de los modulos del backend.
- Ejecutado: `python -c "from app.server_targets import load_a2s_targets; print(load_a2s_targets())"`
- Resultado: el registro carga el target por defecto con la estructura esperada.
## Decision Notes
- Se eligio JSON en variable de entorno para mantener el override simple, local y sin introducir un nuevo formato de configuracion ni dependencias externas.

View File

@@ -0,0 +1,74 @@
# TASK-026-a2s-backed-snapshot-collection
## Goal
Hacer que el colector de snapshots del backend pueda capturar datos reales desde servidores HLL consultables por A2S y persistir esos snapshots en la base local ya preparada.
## Context
El proyecto ya tiene persistencia local, consultas históricas mínimas y un bootstrap de colector. Tras introducir un cliente A2S y una configuración de targets, el siguiente paso es cerrar el primer flujo real de captura y persistencia sobre servidores actuales de HLL.
## Steps
1. Revisar el colector actual, la persistencia local y el cliente A2S.
2. Integrar el cliente A2S dentro del flujo de colecta de snapshots.
3. Hacer que el colector capture datos desde los targets configurados y los normalice al modelo interno.
4. Persistir los snapshots reales usando la capa de almacenamiento ya existente.
5. Manejar de forma controlada los casos de timeout, servidor inaccesible o respuesta inválida.
6. Mantener la posibilidad de usar datos controlados o fallback de desarrollo si la fuente real no responde.
7. Actualizar la documentación backend para explicar el flujo de captura real en esta fase.
8. Mantener el alcance centrado en captura y persistencia, no en nuevas visualizaciones.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- ai/architecture-index.md
- docs/current-hll-data-ingestion-plan.md
- docs/stats-database-schema-foundation.md
- backend/README.md
- backend/app/__init__.py
- backend/app/config.py
- backend/app/collector.py
- backend/app/normalizers.py
- backend/app/snapshots.py
- backend/app/storage.py
- backend/app/a2s_client.py
- backend/app/server_targets.py si existe
## Expected Files to Modify
- backend/README.md
- backend/app/collector.py
- backend/app/normalizers.py
- backend/app/storage.py
- opcionalmente archivos auxiliares nuevos si son estrictamente necesarios para mantener claridad
## Constraints
- No tocar frontend en esta task.
- No introducir todavía estadísticas complejas.
- No añadir scraping de terceros.
- No hacer cambios destructivos.
- Mantener el pipeline claro, comprobable y pequeño.
## Validation
- El colector puede capturar snapshots reales desde al menos un target A2S configurado.
- Los snapshots se persisten en la base local actual.
- Los errores de consulta quedan manejados de forma razonable.
- La documentación backend refleja cómo ejecutar esta captura en local.
## Change Budget
- Preferir menos de 6 archivos modificados o creados.
- Preferir menos de 240 líneas cambiadas.
## Outcome
- `backend/app/collector.py` ahora puede ejecutar captura en modo `a2s`, `controlled` o `auto`, persistiendo snapshots reales cuando las consultas A2S tienen exito.
- El colector registra errores por target sin abortar todo el lote y usa fallback controlado cuando no obtiene respuestas reales y el fallback esta habilitado.
- `backend/app/storage.py` persiste `source_name` por snapshot para conservar la procedencia efectiva de cada captura.
- `backend/README.md` documenta como ejecutar captura real, captura controlada y captura automatica con fallback en local.
## Validation Result
- Ejecutado: `python -m compileall backend/app`
- Resultado: compilacion correcta de los modulos del backend.
- Ejecutado: validacion con `collect_server_snapshots(source_mode="a2s", persist=True, probe_target=stub_probe)` sobre SQLite temporal.
- Resultado: flujo A2S simulado persistio 1 snapshot y `list_snapshot_history()` devolvio 1 registro.
- Ejecutado: validacion con `collect_server_snapshots(source_mode="auto", timeout=0.1, persist=True)` usando el target local por defecto.
- Resultado: el colector registro 1 error de consulta, activo fallback controlado y persistio 3 snapshots de desarrollo.
## Decision Notes
- Se mantuvo un fallback explicito de desarrollo para no romper el flujo local mientras los targets A2S reales siguen siendo configurables y potencialmente inestables.

View File

@@ -0,0 +1,61 @@
# TASK-027-a2s-history-visibility-polish
## Goal
Ajustar la capa de visualización actual para que la web pueda distinguir de forma clara cuándo muestra datos históricos procedentes de snapshots A2S reales frente a placeholders o fallbacks estáticos.
## Context
Una vez que la captura A2S real esté operativa, conviene hacer visible en frontend la procedencia efectiva de los datos sin convertir la landing en un dashboard complejo. Esta task debe mejorar la claridad del bloque actual de servidores y estadísticas sin rediseñarlo completamente.
## Steps
1. Revisar el frontend actual y el bloque de estadísticas/servidores existente.
2. Revisar el formato real de los endpoints históricos tras la integración A2S.
3. Añadir una mejora visual o de estado que permita reflejar claramente si los datos mostrados son:
- snapshots reales recientes
- datos persistidos antiguos
- fallback estático
4. Mantener la mejora discreta y coherente con la landing actual.
5. No convertir la página en una aplicación compleja ni añadir navegación nueva.
6. Preservar el fallback si el backend no está disponible.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- docs/frontend-data-consumption-plan.md
- frontend/index.html
- frontend/assets/js/main.js
- frontend/assets/css/styles.css
- backend/app/routes.py
- backend/app/payloads.py
## Expected Files to Modify
- frontend/index.html
- frontend/assets/js/main.js
- frontend/assets/css/styles.css
## Constraints
- No rediseñar toda la landing.
- No añadir librerías nuevas.
- No tocar backend salvo referencias documentales mínimas si hicieran falta.
- No hacer cambios destructivos.
- Mantener la mejora contenida y alineada con la fase actual.
## Validation
- La web distingue visualmente entre datos reales A2S, históricos persistidos y fallback estático cuando aplique.
- La integración visual sigue siendo coherente con la landing.
- El frontend sigue funcionando si el backend no responde.
## Change Budget
- Preferir menos de 4 archivos modificados.
- Preferir menos de 180 líneas cambiadas.
## Outcome
- `frontend/index.html` incorpora un bloque pequeno de procedencia para explicar si el panel muestra fallback estatico, historico persistido o snapshots A2S recientes.
- `frontend/assets/js/main.js` clasifica el estado visible segun la procedencia y la antiguedad de los snapshots historicos sin romper el fallback actual.
- `frontend/assets/css/styles.css` integra esos estados con una presentacion tactica discreta dentro del panel existente de servidores.
## Validation Result
- Ejecutado: `node --check frontend/assets/js/main.js`
- Resultado: sintaxis valida en el script principal del frontend.
## Decision Notes
- La distincion entre `snapshots A2S recientes` y `historico persistido` se resuelve de forma ligera usando `source_name` y la antiguedad de `captured_at`, evitando cambios de contrato backend en esta fase.

View File

@@ -0,0 +1,72 @@
# TASK-028-real-a2s-target-onboarding
## Goal
Dar de alta el primer target A2S real verificado del proyecto, correspondiente a Comunidad Hispana #01, integrandolo en la configuracion del backend de forma clara, mantenible y documentada.
## Context
El backend ya dispone de cliente A2S, registro configurable de targets y colector integrado con persistencia. Ademas, ya se ha identificado un target real consultable por A2S:
- Comunidad Hispana #01
- Host/IP: 152.114.195.174
- Query Port: 7778
- Game Port: 7777
El objetivo de esta task es incorporar ese target real a la configuracion del proyecto sin asumir otros servidores aun no verificados.
## Steps
1. Revisar la configuracion actual de targets A2S del backend.
2. Anadir el target real verificado de Comunidad Hispana #01 usando:
- nombre amigable claro
- host/IP: `152.114.195.174`
- query port: `7778`
- etiqueta o contexto de fuente coherente con el proyecto
3. Asegurar que la configuracion no dependa de valores hardcodeados dispersos fuera del registro o capa configurada.
4. Mantener separada la nocion de query port y game port para evitar confusiones.
5. Reflejar en la documentacion del backend como esta registrado este primer target real.
6. Dejar claro en documentacion que Comunidad Hispana #02 no debe anadirse aun sin confirmar su query port real.
7. Mantener el cambio pequeno y estrictamente centrado en onboarding de target.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- ai/architecture-index.md
- backend/README.md
- backend/app/config.py
- backend/app/server_targets.py
- backend/app/collector.py
- backend/app/a2s_client.py
## Expected Files to Modify
- backend/README.md
- backend/app/config.py
- backend/app/server_targets.py
- opcionalmente backend/app/collector.py si necesita alineacion menor para consumir el target de forma limpia
## Constraints
- No tocar frontend.
- No introducir nuevos targets no verificados.
- No asumir datos de Comunidad Hispana #02.
- No anadir scraping.
- No hacer cambios destructivos.
- Mantener la configuracion simple y clara.
## Validation
- El backend contiene el target real de Comunidad Hispana #01 correctamente registrado.
- El query port configurado es `7778`.
- La documentacion deja claro como esta definido este target.
- No se han introducido targets inciertos o ambiguos.
## Change Budget
- Preferir menos de 4 archivos modificados.
- Preferir menos de 140 lineas cambiadas.
## Outcome
- `backend/app/server_targets.py` registra por defecto solo `Comunidad Hispana #01` con `host` real, `query_port` verificado y `game_port` separado como dato opcional de referencia.
- `backend/app/config.py` centraliza el `source_name` por defecto para evitar valores dispersos en el registro A2S.
- `backend/README.md` documenta el target real incorporado y deja explicito que Comunidad Hispana #02 no debe anadirse hasta confirmar su `query_port`.
## Validation Result
- Ejecutado desde `backend/`: `python -` importando `load_a2s_targets()`.
- Resultado: el registro carga `Comunidad Hispana #01` con `host=152.114.195.174`, `query_port=7778`, `game_port=7777` y `source_name=community-hispana-a2s`.
## Decision Notes
- Se mantiene `game_port` fuera de la logica de consulta A2S y solo como metadato opcional del target para evitar confundirlo con `query_port`.

View File

@@ -0,0 +1,86 @@
# TASK-029-real-a2s-capture-validation
## Goal
Validar una captura A2S real extremo a extremo contra Comunidad Hispana #01, confirmando que el colector puede consultar el servidor, normalizar la respuesta y persistir snapshots utiles en la base local.
## Context
El proyecto ya tiene:
- cliente A2S
- targets configurables
- persistencia local
- endpoints historicos
- primer target real verificado para Comunidad Hispana #01
Ahora hay que comprobar que el flujo real funciona de verdad con ese servidor:
A2S -> colector -> persistencia local
## Steps
1. Revisar el target real configurado para Comunidad Hispana #01.
2. Ejecutar el colector o flujo equivalente contra ese target real.
3. Confirmar que el backend consulta correctamente el host `152.114.195.174` con query port `7778`.
4. Validar que la respuesta A2S obtenida se normaliza al modelo interno del proyecto.
5. Persistir al menos un snapshot real en la base local actual.
6. Verificar que se registran de forma razonable:
- nombre del servidor
- timestamp de captura
- mapa actual si esta disponible
- jugadores actuales
- capacidad maxima
- procedencia efectiva del snapshot
7. Verificar el comportamiento si la consulta falla, timeout o devuelve datos parciales.
8. Actualizar la documentacion minima necesaria sobre como ejecutar esta validacion en local.
9. Mantener el alcance centrado en validacion del flujo real, no en nuevas features.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- ai/architecture-index.md
- backend/README.md
- backend/app/a2s_client.py
- backend/app/server_targets.py
- backend/app/collector.py
- backend/app/normalizers.py
- backend/app/snapshots.py
- backend/app/storage.py
## Expected Files to Modify
- backend/README.md
- backend/app/collector.py
- backend/app/normalizers.py
- backend/app/storage.py
- opcionalmente otros archivos backend si son estrictamente necesarios para una captura real robusta
## Constraints
- No tocar frontend.
- No anadir analitica avanzada.
- No anadir scraping de terceros.
- No introducir complejidad innecesaria.
- No hacer cambios destructivos.
- Mantener la validacion centrada en Comunidad Hispana #01.
## Validation
- Se realiza una captura real A2S sobre Comunidad Hispana #01.
- Se persiste al menos un snapshot real en la base local.
- El flujo real queda documentado y es repetible en local.
- Los errores de consulta estan razonablemente manejados.
## Change Budget
- Preferir menos de 5 archivos modificados.
- Preferir menos de 180 lineas cambiadas.
## Outcome
- `backend/README.md` documenta el comando exacto de validacion A2S real y el resultado esperado cuando `Comunidad Hispana #01` responde.
- No fue necesario cambiar `collector.py`, `normalizers.py` ni `storage.py` porque el flujo real ya normaliza y persiste correctamente.
- La validacion tambien dejo ver el comportamiento de error: en entorno sandbox con UDP restringido el colector devuelve timeout controlado y `success_count: 0`.
## Validation Result
- Ejecutado en sandbox: `python -m app.collector --source a2s --no-fallback`.
- Resultado en sandbox: timeout controlado hacia `152.114.195.174:7778`, `success_count: 0`, sin snapshots reales persistidos.
- Ejecutado fuera del sandbox: `python -m app.collector --source a2s --no-fallback`.
- Resultado fuera del sandbox: `success_count: 1`, un snapshot persistido para `comunidad-hispana-01` en `backend/data/hll_vietnam_dev.sqlite3`.
- Snapshot validado en SQLite: `server_name=#01 [ESP] Comunidad Hispana - discord.comunidadhll.es - Spa Onl`, `current_map=Remagen`, `players=0`, `max_players=100`, `source_name=community-hispana-a2s`, `captured_at=2026-03-20T11:23:08.431142Z`.
- Endpoints revisados desde Python: `/api/servers/latest`, `/api/servers/history` y `/api/servers/{id}/history` ya exponen el snapshot real persistido.
## Decision Notes
- El timeout observado en sandbox no se trato como bug de backend porque la misma consulta funciono fuera del sandbox sin cambios de codigo.
- La procedencia efectiva del snapshot queda reflejada en `source_name=community-hispana-a2s`, mientras que el payload superior del colector mantiene `collection_mode=a2s`.

View File

@@ -0,0 +1,84 @@
# TASK-030-historical-snapshot-quality-pass
## Goal
Revisar y ajustar la calidad de los snapshots persistidos tras la primera captura A2S real, asegurando consistencia de datos, timestamps, procedencia y utilidad de cara a historico y visualizacion.
## Context
Una vez incorporado el target real y validada una primera captura, el siguiente paso es comprobar la calidad del dato persistido. No basta con guardar snapshots; deben ser consistentes y utiles para construir historico, estadisticas y visualizacion sin ruido innecesario.
## Steps
1. Revisar snapshots persistidos a partir de la captura real de Comunidad Hispana #01.
2. Verificar consistencia de campos clave:
- nombre del servidor
- host o referencia de origen
- timestamp
- mapa actual
- jugadores
- capacidad
- fuente efectiva
3. Revisar si hay problemas de calidad como:
- duplicados innecesarios
- timestamps incoherentes
- normalizacion deficiente
- mezcla poco clara entre fallback y captura real
4. Ajustar la capa de persistencia, normalizacion o payloads si fuera necesario para mejorar claridad.
5. Revisar el impacto en endpoints historicos existentes:
- `/api/servers/latest`
- `/api/servers/history`
- `/api/servers/{id}/history`
6. Mantener el cambio centrado en calidad y coherencia del historico, no en nuevas visualizaciones.
7. Actualizar documentacion minima si el modelo efectivo de snapshot cambia ligeramente.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- ai/architecture-index.md
- backend/README.md
- backend/app/collector.py
- backend/app/normalizers.py
- backend/app/storage.py
- backend/app/routes.py
- backend/app/payloads.py
- datos persistidos generados en la task anterior
## Expected Files to Modify
- backend/app/normalizers.py
- backend/app/storage.py
- backend/app/routes.py
- backend/app/payloads.py
- backend/README.md
- opcionalmente documentacion tecnica relacionada si la claridad del historico necesita ajuste
## Constraints
- No tocar frontend en esta task.
- No anadir nuevas fuentes externas.
- No introducir complejidad analitica alta.
- No hacer cambios destructivos.
- Mantener el resultado centrado en calidad de datos historicos.
## Validation
- Los snapshots reales persistidos son consistentes y utiles.
- Los endpoints historicos reflejan correctamente el dato real almacenado.
- La distincion entre captura real y fallback queda razonablemente clara.
- El backend queda listo para una siguiente task de mejora visual o estadistica sobre datos reales.
## Change Budget
- Preferir menos de 5 archivos modificados.
- Preferir menos de 180 lineas cambiadas.
## Outcome
- `backend/app/normalizers.py` anade `snapshot_origin` y `source_ref` al modelo normalizado para que la procedencia no se infiera de forma ambigua en historico.
- `backend/app/snapshots.py` conserva esos campos en cada snapshot persistible.
- `backend/app/storage.py` migra la tabla SQLite sin destruir datos, serializa ambos campos en las consultas historicas y backfilla referencias A2S registradas para snapshots ya existentes.
- `backend/README.md` documenta los nuevos metadatos historicos y el valor esperado para la captura real de Comunidad Hispana #01.
## Validation Result
- Ejecutado: lectura de `list_latest_snapshots()`, `list_snapshot_history()` y `list_server_history('comunidad-hispana-01')` desde Python.
- Resultado: los snapshots historicos exponen `snapshot_origin` con `real-a2s` o `controlled-fallback` y `source_ref` coherente.
- Ejecutado fuera del sandbox: `python -m app.collector --source a2s --no-fallback`.
- Resultado: nuevo snapshot real persistido con `source_ref=a2s://152.114.195.174:7778`, `current_map=Remagen`, `players=0`, `max_players=100`.
- Endpoints verificados: `/api/servers/latest`, `/api/servers/history` y `/api/servers/{id}/history` devuelven el snapshot real mas reciente con la nueva metadata de procedencia.
## Decision Notes
- No se tocaron `routes.py` ni `payloads.py` porque ya reutilizan directamente los registros serializados de almacenamiento y heredaron la mejora sin cambios de contrato adicionales.
- No se introdujo deduplicacion agresiva de snapshots porque dos capturas reales con distinto `captured_at` siguen siendo historico valido; el ajuste se centro en claridad y consistencia de procedencia.

View File

@@ -0,0 +1,70 @@
# TASK-031-local-cors-alignment-for-frontend-dev
## Goal
Corregir y alinear la configuracion CORS del backend para permitir que el frontend local de desarrollo consuma la API desde origenes locales habituales sin caer en fallback por bloqueo del navegador.
## Context
El backend responde correctamente a `/health`, `/api/servers/latest` y `/api/servers/history`, y los snapshots reales A2S ya estan persistidos. Sin embargo, el frontend servido localmente con `python -m http.server 8080` no puede consumir la API porque el navegador bloquea las respuestas por falta de la cabecera `Access-Control-Allow-Origin` para `http://localhost:8080`.
La correccion debe centrarse en CORS de desarrollo local, sin cambiar la arquitectura funcional del producto.
## Steps
1. Revisar la configuracion actual de CORS del backend.
2. Confirmar como se comparan y validan los origenes permitidos.
3. Anadir soporte correcto para los origenes locales de desarrollo mas comunes, incluyendo al menos:
- `http://localhost:8080`
- `http://127.0.0.1:8080`
4. Revisar si tambien conviene permitir otros puertos locales habituales documentados por el proyecto, sin abrir el backend de forma innecesaria.
5. Asegurar que las respuestas GET del backend incluyan `Access-Control-Allow-Origin` cuando el origen este permitido.
6. Asegurar que el backend maneje correctamente `OPTIONS` si el flujo actual lo requiere.
7. Actualizar la documentacion del backend para dejar claro como probar frontend y backend juntos en local.
8. Mantener el alcance centrado en desarrollo local y correccion CORS.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- backend/README.md
- backend/app/config.py
- backend/app/main.py
- backend/app/routes.py
- frontend/assets/js/main.js
## Expected Files to Modify
- backend/README.md
- backend/app/config.py
- backend/app/main.py
- opcionalmente otros archivos backend si la logica CORS esta centralizada en otro sitio
## Constraints
- No tocar visualmente el frontend.
- No cambiar endpoints ni payloads.
- No introducir dependencias nuevas innecesarias.
- No hacer cambios destructivos.
- Mantener la solucion pequena y claramente orientada a desarrollo local.
## Validation
- Una peticion desde `http://localhost:8080` a `http://localhost:8000/api/servers/latest` ya no falla por CORS.
- Una peticion desde `http://127.0.0.1:8080` tambien funciona si se decidio soportarla.
- La landing deja de caer al fallback estatico cuando el backend esta disponible.
- La documentacion refleja como ejecutar el stack local correctamente.
## Change Budget
- Preferir menos de 4 archivos modificados.
- Preferir menos de 140 lineas cambiadas.
## Outcome
- `backend/app/config.py` amplia la allowlist CORS local por defecto para cubrir `http://localhost:8080` y `http://127.0.0.1:8080`, manteniendo tambien `null` y los origenes ya usados en `5500`.
- `backend/app/config.py` normaliza espacios y barras finales en `HLL_BACKEND_ALLOWED_ORIGINS` para que un override local no falle por formato.
- `backend/README.md` documenta la prueba local recomendada con `python -m app.main` y `python -m http.server 8080`, y deja explicito que origenes locales quedan soportados por defecto.
- No fue necesario cambiar `backend/app/main.py` ni `backend/app/routes.py` porque la emision de `Access-Control-Allow-Origin` y el manejo de `OPTIONS` ya estaban correctamente centralizados en el handler HTTP.
## Validation Result
- Ejecutado: validacion aislada desde Python levantando `app.main.create_server()` en `127.0.0.1:8010` dentro del mismo proceso.
- Resultado `GET` con `Origin: http://localhost:8080`: `200 OK`, `Access-Control-Allow-Origin: http://localhost:8080`, `Vary: Origin`.
- Resultado `GET` con `Origin: http://127.0.0.1:8080`: `200 OK`, `Access-Control-Allow-Origin: http://127.0.0.1:8080`, `Vary: Origin`.
- Resultado `OPTIONS` con `Origin: http://localhost:8080`: `204 No Content`, `Access-Control-Allow-Origin: http://localhost:8080`, `Access-Control-Allow-Methods: GET, OPTIONS`, `Access-Control-Allow-Headers: Content-Type`.
- Observacion de entorno: `127.0.0.1:8000` ya estaba ocupado por otro proceso ajeno a esta task, asi que la validacion final se hizo en `:8010` para comprobar exactamente el codigo modificado sin depender de ese proceso.
## Decision Notes
- Se mantuvo una allowlist cerrada de desarrollo local en lugar de abrir CORS globalmente, porque la task pedia corregir el flujo local sin introducir una configuracion de produccion prematura.
- No se anadieron puertos arbitrarios extra; se alineo la configuracion con `5500`, `8080` y `null`, que cubren los flujos locales ya usados o documentados por el proyecto.

View File

@@ -0,0 +1,69 @@
# TASK-032-connect-button-for-real-server-cards
## Goal
Añadir en la web un botón de conexión directa `steam://connect/...` para tarjetas de servidores reales, usando el game port correcto y manteniendo el comportamiento actual del panel de servidores.
## Context
La landing ya muestra snapshots reales A2S y distingue entre datos reales, históricos y fallback. Además, ya se han verificado servidores reales de Comunidad Hispana con separación entre game port y query port. El siguiente paso útil para producto es añadir un botón Connect en las tarjetas de servidores reales para permitir una acción directa desde la web.
## Steps
1. Revisar el frontend actual y cómo se renderizan las tarjetas de servidores.
2. Revisar el backend y comprobar si el frontend dispone actualmente del `game_port` necesario para construir `steam://connect/...`.
3. Si el dato no está disponible en los payloads actuales, ajustar la capa backend mínima necesaria para exponerlo de forma clara y estable.
4. Añadir un botón Connect solo cuando la tarjeta represente un servidor real con datos suficientes.
5. Usar siempre el `game_port`, nunca el `query_port`.
6. Mantener el botón visualmente coherente con la landing actual.
7. Preservar el fallback actual si un servidor no tiene datos reales o no dispone de `game_port`.
8. Validar que los enlaces queden correctos al menos para:
- Comunidad Hispana #01`steam://connect/152.114.195.174:7777`
- Comunidad Hispana #02`steam://connect/152.114.195.150:7877`
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- frontend/index.html
- frontend/assets/js/main.js
- frontend/assets/css/styles.css
- backend/app/routes.py
- backend/app/payloads.py
- backend/app/server_targets.py
- backend/README.md
## Expected Files to Modify
- frontend/index.html
- frontend/assets/js/main.js
- frontend/assets/css/styles.css
- backend/app/routes.py
- backend/app/payloads.py
- opcionalmente backend/app/server_targets.py si hace falta alinear metadata
## Constraints
- No rediseñar toda la landing.
- No romper el fallback actual.
- No usar `query_port` para el enlace de conexión.
- No añadir librerías nuevas.
- No hacer cambios destructivos.
- Mantener la mejora centrada en el panel de servidores y acción de conexión.
## Validation
- Las tarjetas de servidores reales muestran un botón Connect cuando corresponde.
- El enlace de conexión usa `steam://connect/<host>:<game_port>`.
- Los placeholders o tarjetas sin datos reales no rompen el render.
- La mejora visual se integra con la landing actual.
## Change Budget
- Preferir menos de 5 archivos modificados.
- Preferir menos de 180 líneas cambiadas.
## Outcome
- `backend/app/payloads.py` enriquece los snapshots reales con `host`, `query_port` y `game_port` a partir del registro A2S ya verificado, sin tocar la persistencia.
- `frontend/assets/js/main.js` muestra un boton `Connect` solo para tarjetas `real-a2s` que dispongan de `host` y `game_port`, generando `steam://connect/<host>:<game_port>`.
- `frontend/assets/css/styles.css` integra visualmente la nueva accion sin redisenar el panel ni afectar placeholders.
## Validation Result
- Ejecutado desde `backend/`: comprobacion de `build_server_latest_payload()` sobre `comunidad-hispana-01` y `comunidad-hispana-02`.
- Resultado: ambos snapshots reales exponen `host`, `query_port` y `game_port`, conservando `snapshot_origin=real-a2s`.
- Ejecutado desde `backend/`: validacion de enlaces esperados `steam://connect/152.114.195.174:7777` y `steam://connect/152.114.195.150:7877`.
- Resultado: los enlaces calculados coinciden exactamente con los valores requeridos para `#01` y `#02`.
## Decision Notes
- Se evita duplicar `host` y `game_port` en SQLite porque ya existen como metadata estable del target A2S y basta con enriquecer el payload de lectura.

View File

@@ -0,0 +1,62 @@
# TASK-033-real-vs-placeholder-server-panel-separation
## Goal
Mejorar la claridad visual y funcional del panel de servidores separando o priorizando de forma más evidente los snapshots reales A2S frente a los placeholders controlados.
## Context
El panel de servidores ya consume datos del backend y actualmente mezcla snapshots reales persistidos con tarjetas de fallback controlado. El sistema funciona, pero ahora conviene dejar visualmente más claro qué servidores son reales y cuáles siguen siendo referencias provisionales.
## Steps
1. Revisar el panel actual de servidores y su lógica de renderizado.
2. Identificar cómo distingue actualmente el frontend entre:
- `real-a2s`
- `controlled-fallback`
- histórico persistido
3. Mejorar la composición del panel para que los snapshots reales queden más destacados o separados de los placeholders.
4. Mantener el cambio contenido y coherente con el diseño actual.
5. Asegurar que la información de procedencia siga siendo clara.
6. No eliminar el fallback si todavía es útil para el proyecto.
7. No rediseñar la landing completa.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- frontend/index.html
- frontend/assets/js/main.js
- frontend/assets/css/styles.css
- backend/app/payloads.py
- backend/app/routes.py
## Expected Files to Modify
- frontend/index.html
- frontend/assets/js/main.js
- frontend/assets/css/styles.css
## Constraints
- No tocar backend salvo si una referencia documental mínima fuera imprescindible.
- No eliminar soporte para fallback.
- No introducir librerías nuevas.
- No hacer cambios destructivos.
- Mantener la mejora centrada en claridad de datos y jerarquía visual.
## Validation
- El panel distingue mejor entre servidores reales y placeholders.
- Los snapshots reales ganan prioridad o separación visual clara.
- El fallback sigue funcionando.
- La landing mantiene coherencia visual.
## Change Budget
- Preferir menos de 3 archivos modificados.
- Preferir menos de 160 líneas cambiadas.
## Outcome
- `frontend/assets/js/main.js` separa el render historico en dos bloques: `Snapshots reales A2S` y `Referencia provisional e historico auxiliar`, priorizando arriba los snapshots reales.
- `frontend/assets/js/main.js` etiqueta cada tarjeta persistida como `Snapshot real A2S` o `Referencia persistida` segun `snapshot_origin`, manteniendo el fallback sin eliminarlo.
- `frontend/assets/css/styles.css` refuerza la separacion visual con encabezados de seccion y variantes de tarjeta diferenciadas para reales y placeholders.
## Validation Result
- Ejecutado: `node --check frontend/assets/js/main.js`.
- Resultado: sintaxis JavaScript valida tras introducir el render por secciones.
- Revisado en codigo: el panel renderiza una seccion prioritaria `Snapshots reales A2S` y una seccion separada para referencia/fallback, sin romper la grilla existente.
## Decision Notes
- La separacion se implementa dentro del mismo panel para mantener el alcance pequeno y evitar un redisenio estructural de la landing.

View File

@@ -0,0 +1,72 @@
# TASK-034-real-a2s-target-onboarding-comunidad-hispana-02
## Goal
Dar de alta el segundo target A2S real verificado del proyecto, correspondiente a Comunidad Hispana #02, integrándolo en la configuración del backend de forma clara, mantenible y coherente con el target real ya existente de Comunidad Hispana #01.
## Context
El backend ya tiene onboarding real de Comunidad Hispana #01 y se ha validado la separación entre `game_port` y `query_port`. Ahora también se ha verificado un segundo servidor real:
- Comunidad Hispana #02
- Host/IP: `152.114.195.150`
- Game Port: `7877`
- Query Port: `7878`
La task debe incorporarlo correctamente a la configuración sin introducir ambigüedades entre puertos ni mezclarlo con fuentes no verificadas.
## Steps
1. Revisar la configuración actual de targets A2S.
2. Añadir Comunidad Hispana #02 con:
- nombre amigable claro
- host/IP: `152.114.195.150`
- query port: `7878`
- game port: `7877`
- contexto o etiqueta coherente con el proyecto
3. Mantener la configuración desacoplada y limpia.
4. Documentar el alta del nuevo target en el backend.
5. Verificar que el target anterior (#01) sigue correcto y sin regresiones.
6. Mantener el cambio acotado a onboarding de target real.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- ai/architecture-index.md
- backend/README.md
- backend/app/config.py
- backend/app/server_targets.py
- backend/app/collector.py
- backend/app/a2s_client.py
## Expected Files to Modify
- backend/README.md
- backend/app/server_targets.py
- backend/app/config.py
- opcionalmente backend/app/collector.py si necesita una alineación menor
## Constraints
- No tocar frontend.
- No añadir targets no verificados.
- No confundir `game_port` con `query_port`.
- No añadir scraping.
- No hacer cambios destructivos.
- Mantener la configuración simple y clara.
## Validation
- El backend contiene el target real de Comunidad Hispana #02 correctamente registrado.
- El query port configurado es `7878`.
- El game port registrado es `7877`.
- La documentación deja claro cómo está definido este target.
- No se ha roto el target existente de Comunidad Hispana #01.
## Change Budget
- Preferir menos de 4 archivos modificados.
- Preferir menos de 140 líneas cambiadas.
## Outcome
- `backend/app/server_targets.py` registra `Comunidad Hispana #02` junto a `#01` con `host`, `query_port`, `game_port` y `external_server_id` verificados.
- `backend/README.md` documenta ambos targets reales por defecto y mantiene separada la semantica de `query_port` frente a `game_port`.
- No fue necesario tocar `backend/app/config.py` ni `backend/app/collector.py` porque la configuracion existente ya consume el registro de forma desacoplada.
## Validation Result
- Ejecutado desde `backend/`: `python -c "from app.server_targets import load_a2s_targets; print([(t.name, t.host, t.query_port, t.game_port, t.external_server_id) for t in load_a2s_targets()])"`.
- Resultado: el registro carga `Comunidad Hispana #01` y `Comunidad Hispana #02` con `query_port=7778/7878` y `game_port=7777/7877` respectivamente.
## Decision Notes
- Se mantiene un unico `source_name` para la misma familia de servidores reales porque la distincion operativa ya queda en `external_server_id`, host y puertos.

View File

@@ -0,0 +1,80 @@
# TASK-035-real-a2s-capture-validation-comunidad-hispana-02
## Goal
Validar una captura A2S real extremo a extremo contra Comunidad Hispana #02, confirmando que el colector puede consultar el servidor, normalizar la respuesta y persistir snapshots útiles junto a los ya existentes de Comunidad Hispana #01.
## Context
El proyecto ya ha validado el flujo real A2S con Comunidad Hispana #01. Ahora se ha verificado un segundo servidor real de Comunidad Hispana con:
- Host/IP: `152.114.195.150`
- Query Port: `7878`
- Game Port: `7877`
El objetivo es confirmar que el pipeline soporta múltiples targets reales y que la persistencia y los endpoints históricos siguen siendo coherentes.
## Steps
1. Revisar la configuración actual de targets A2S y el target recién añadido para Comunidad Hispana #02.
2. Ejecutar el colector o flujo equivalente contra este target real.
3. Confirmar que el backend consulta correctamente el host `152.114.195.150` con query port `7878`.
4. Validar que la respuesta A2S obtenida se normaliza al modelo interno.
5. Persistir al menos un snapshot real de Comunidad Hispana #02 en la base local actual.
6. Verificar que los endpoints históricos reflejan este nuevo servidor junto al ya existente.
7. Revisar el comportamiento si la consulta falla, timeout o devuelve datos parciales.
8. Actualizar la documentación mínima necesaria sobre cómo repetir esta validación en local.
9. Mantener el alcance centrado en validación del flujo real, no en nuevas features.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- ai/architecture-index.md
- backend/README.md
- backend/app/a2s_client.py
- backend/app/server_targets.py
- backend/app/collector.py
- backend/app/normalizers.py
- backend/app/snapshots.py
- backend/app/storage.py
- backend/app/routes.py
- backend/app/payloads.py
## Expected Files to Modify
- backend/README.md
- backend/app/collector.py
- backend/app/normalizers.py
- backend/app/storage.py
- backend/app/routes.py
- backend/app/payloads.py
- opcionalmente otros archivos backend si son estrictamente necesarios para soportar mejor múltiples targets reales
## Constraints
- No tocar frontend salvo que una referencia mínima fuera imprescindible para mostrar el nuevo target una vez persistido.
- No añadir analítica avanzada.
- No añadir scraping de terceros.
- No introducir complejidad innecesaria.
- No hacer cambios destructivos.
- Mantener la validación centrada en Comunidad Hispana #02 y coexistencia con #01.
## Validation
- Se realiza una captura real A2S sobre Comunidad Hispana #02.
- Se persiste al menos un snapshot real en la base local.
- El flujo real queda documentado y es repetible en local.
- Los endpoints históricos reflejan ambos targets reales cuando existan snapshots.
- Los errores de consulta están razonablemente manejados.
## Change Budget
- Preferir menos de 6 archivos modificados.
- Preferir menos de 180 líneas cambiadas.
## Outcome
- `backend/app/a2s_client.py` eleva el timeout A2S por defecto de `3.0s` a `6.0s` para reducir timeouts transitorios al consultar varios targets reales seguidos.
- `backend/README.md` documenta la validacion local extremo a extremo con ambos targets reales por defecto y el resultado esperado cuando responden `#01` y `#02`.
- No fue necesario cambiar `collector.py`, `normalizers.py`, `storage.py`, `routes.py` ni `payloads.py` porque el pipeline ya normalizaba, persistia y exponia historico multi-target correctamente.
## Validation Result
- Ejecutado desde `backend/`: `python -m app.a2s_client 152.114.195.150 7878 --timeout 6`.
- Resultado: respuesta valida de `Comunidad Hispana #02` con `server_name=#02 [ESP] Comunidad Hispana - discord.comunidadhll.es - Spa Onl`, `map_name=StMarie`, `players=0`, `max_players=100`.
- Ejecutado desde `backend/`: `python -m app.collector --source a2s --no-fallback`.
- Resultado: `target_count: 2`, `success_count: 2`, snapshots persistidos para `comunidad-hispana-01` y `comunidad-hispana-02` en `backend/data/hll_vietnam_dev.sqlite3`.
- Ejecutado desde `backend/`: `python -c "from app.payloads import build_server_history_payload, build_server_detail_history_payload; ..."` para revisar historico.
- Resultado: `/api/servers/history` refleja ambos targets reales y `/api/servers/comunidad-hispana-02/history` devuelve el snapshot persistido de `#02`.
## Decision Notes
- Se ajusta el timeout por defecto en lugar de introducir reintentos o complejidad adicional porque la captura ya era funcional y el problema observado fue de sensibilidad temporal, no de arquitectura.

View File

@@ -0,0 +1,73 @@
# TASK-036-hide-placeholders-when-real-servers-exist-and-localize-connect-label
## Goal
Ajustar el panel de servidores para ocultar los placeholders o referencias provisionales cuando ya existan snapshots reales A2S utilizables, y cambiar la etiqueta visible del botón de conexión de “Connect” a “Conectar”.
## Context
La landing ya muestra snapshots reales A2S de Comunidad Hispana #01 y #02. En este punto, mantener visibles los placeholders como bloque principal o paralelo degrada la claridad del producto y da una sensación de estado provisional que ya no corresponde al nivel real del sistema. Los placeholders deben quedar como fallback técnico, no como contenido normal cuando haya datos reales disponibles. Además, la CTA visible de conexión debe localizarse al español.
## Steps
1. Revisar el render actual del panel de servidores en frontend.
2. Identificar la lógica que separa:
- snapshots `real-a2s`
- snapshots históricos persistidos
- placeholders o `controlled-fallback`
3. Ajustar la lógica para que:
- si existen snapshots reales A2S utilizables, solo se muestren esos servidores reales en la vista principal
- los placeholders queden ocultos en la vista normal
- el fallback provisional solo se muestre cuando no existan datos reales o el backend no aporte snapshots utilizables
4. Mantener clara la procedencia del dato real sin sobrecargar la UI.
5. Cambiar la etiqueta visible del botón de conexión de:
- `Connect`
a:
- `Conectar`
6. Mantener intacto el uso técnico del enlace `steam://connect/<host>:<game_port>`.
7. Revisar el copy mínimo del bloque para que encaje con una fase más madura del producto.
8. Mantener el ajuste visual contenido, sin rediseñar toda la landing.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- frontend/index.html
- frontend/assets/js/main.js
- frontend/assets/css/styles.css
- backend/app/payloads.py
- backend/app/routes.py
## Expected Files to Modify
- frontend/index.html
- frontend/assets/js/main.js
- frontend/assets/css/styles.css
## Constraints
- No tocar backend salvo referencia documental mínima si fuera imprescindible.
- No romper el fallback cuando no haya datos reales.
- No quitar soporte para placeholders a nivel interno; solo ocultarlos cuando existan snapshots reales utilizables.
- No añadir librerías nuevas.
- No hacer cambios destructivos.
- Mantener el ajuste centrado en claridad de producto y localización de la CTA.
## Validation
- Cuando existen snapshots `real-a2s`, la vista principal del panel muestra solo servidores reales.
- Los placeholders ya no aparecen en la vista normal si hay datos reales disponibles.
- Si no hay datos reales, el fallback sigue funcionando.
- El botón visible muestra “Conectar”.
- La landing gana claridad visual y semántica.
## Change Budget
- Preferir menos de 3 archivos modificados.
- Preferir menos de 140 líneas cambiadas.
## Outcome
- `frontend/assets/js/main.js` filtra la vista principal del panel para mostrar solo snapshots `real-a2s` cuando existen, manteniendo el fallback persistido solo para escenarios sin capturas reales utilizables.
- `frontend/assets/js/main.js` localiza la CTA visible de conexión a `Conectar` sin alterar el esquema técnico `steam://connect/<host>:<game_port>`.
- `frontend/index.html` y `frontend/assets/css/styles.css` ajustan el copy y la presentación base del bloque para que el estado por defecto sea más neutro y menos provisional.
## Validation Result
- Ejecutado: `node --check frontend/assets/js/main.js`.
- Resultado: sintaxis JavaScript válida tras el ajuste de filtrado y localización.
- Revisado en código: la vista enriquecida usa `selectPrimaryServerItems(...)`, por lo que los placeholders quedan ocultos cuando hay snapshots reales y el fallback sigue siendo la ruta visible si no existen capturas reales utilizables.
- Revisado en código: la CTA visible del enlace de conexión muestra `Conectar`.
## Decision Notes
- La ocultación de placeholders se resolvió en la capa de selección de items visibles, sin tocar backend ni eliminar soporte interno de fallback.

View File

@@ -0,0 +1,62 @@
# TASK-037-scheduled-local-snapshot-refresh
## Goal
Preparar una forma simple, clara y repetible de refrescar snapshots A2S reales de forma periódica en entorno local, reduciendo la dependencia de ejecución manual del colector.
## Context
El proyecto ya dispone de cliente A2S, targets reales verificados, colector funcional, persistencia SQLite y visualización en frontend. Actualmente el flujo depende de lanzar manualmente el colector para actualizar snapshots. El siguiente paso útil es dejar preparado un mecanismo sencillo de refresco periódico local que permita validar mejor el comportamiento histórico y la evolución de los datos.
## Steps
1. Revisar el flujo actual de captura manual de snapshots.
2. Definir una estrategia simple de refresco local, adecuada al estado actual del proyecto.
3. Preparar una forma clara de ejecutar capturas periódicas sin introducir infraestructura compleja de producción.
4. Mantener el diseño desacoplado para que en el futuro pueda reemplazarse por un scheduler más serio si hiciera falta.
5. Documentar cómo arrancar este refresco local y cómo detenerlo.
6. Si hace falta, añadir un modo seguro o flags para evitar comportamiento inesperado en desarrollo.
7. Mantener el alcance en refresco local y desarrollo, no en despliegue productivo.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- backend/README.md
- backend/app/config.py
- backend/app/collector.py
- backend/app/server_targets.py
- backend/app/storage.py
## Expected Files to Modify
- backend/README.md
- backend/app/config.py
- backend/app/collector.py
- opcionalmente archivos nuevos si mejoran claridad, por ejemplo:
- backend/app/scheduler.py
- backend/scripts/run_local_refresh.py
## Constraints
- No tocar frontend.
- No introducir un sistema de colas o infraestructura pesada.
- No añadir complejidad innecesaria.
- No hacer cambios destructivos.
- Mantener la solución simple y enfocada a entorno local.
## Validation
- Existe una forma documentada y funcional de refrescar snapshots periódicamente en local.
- El flujo sigue siendo controlable y fácil de detener.
- La solución encaja con el backend actual sin sobredimensionarlo.
- El sistema queda preparado para capturas más frecuentes y validación histórica.
## Change Budget
- Preferir menos de 5 archivos modificados o creados.
- Preferir menos de 180 líneas cambiadas.
## Outcome
- `backend/app/scheduler.py` añade un bucle local de refresco periódico reutilizando el colector existente, sin introducir infraestructura de producción ni dependencias nuevas.
- `backend/app/config.py` centraliza el intervalo por defecto mediante `HLL_BACKEND_REFRESH_INTERVAL_SECONDS`.
- `backend/README.md` documenta cómo arrancar el refresco local, cómo detenerlo con `Ctrl+C` y qué flags de seguridad usar (`--interval`, `--no-fallback`, `--max-runs`).
## Validation Result
- Ejecutado: `python -m app.scheduler --source controlled --interval 1 --max-runs 1` desde `backend/`.
- Resultado: se ejecutó un ciclo persistido correctamente sobre `backend/data/hll_vietnam_dev.sqlite3` y el proceso terminó al alcanzar el límite seguro de ejecuciones.
- Revisado en código: el refresco local queda desacoplado del servidor HTTP y reutiliza `collect_server_snapshots(...)` sin duplicar lógica del colector.
## Decision Notes
- El refresco periódico se resolvió como un bucle local explícito y controlable para desarrollo, en lugar de introducir un scheduler embebido en el backend HTTP o infraestructura más pesada.

View File

@@ -0,0 +1,72 @@
# TASK-038-server-history-summary-cards
## Goal
Añadir al backend y al frontend una primera capa de métricas históricas resumidas para los servidores reales, de forma que el panel muestre información más útil que el simple último snapshot.
## Context
La web ya muestra snapshots reales A2S y dispone de histórico persistido. Sin embargo, el valor actual del panel sigue siendo limitado si solo enseña la última captura y una tendencia básica. El siguiente paso lógico es construir resúmenes históricos ligeros a partir de lo ya almacenado, sin convertir aún la landing en un dashboard complejo.
## Steps
1. Revisar los endpoints históricos actuales y el panel de servidores existente.
2. Identificar qué métricas históricas resumidas son viables con los snapshots ya persistidos, por ejemplo:
- última vez visto online
- número de capturas recientes
- promedio reciente de jugadores
- valor máximo reciente de población
- tiempo desde la última captura
3. Añadir la capa mínima necesaria en backend para exponer estas métricas de forma clara.
4. Integrar esas métricas en las tarjetas de servidores reales del frontend.
5. Mantener la mejora ligera y coherente con el diseño actual.
6. No introducir cálculos pesados ni analítica compleja.
7. Preservar el comportamiento correcto si aún hay pocos snapshots disponibles.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- backend/README.md
- backend/app/routes.py
- backend/app/payloads.py
- backend/app/storage.py
- frontend/index.html
- frontend/assets/js/main.js
- frontend/assets/css/styles.css
## Expected Files to Modify
- backend/app/routes.py
- backend/app/payloads.py
- backend/app/storage.py
- frontend/index.html
- frontend/assets/js/main.js
- frontend/assets/css/styles.css
## Constraints
- No rediseñar toda la landing.
- No añadir gráficas complejas.
- No introducir librerías nuevas.
- No romper el fallback actual.
- No hacer cambios destructivos.
- Mantener la mejora centrada en valor histórico resumido.
## Validation
- El backend expone métricas históricas resumidas útiles para servidores reales.
- El frontend las muestra de forma clara y legible.
- La UI sigue funcionando aunque el histórico disponible sea limitado.
- El panel gana valor funcional sin perder claridad.
## Change Budget
- Preferir menos de 6 archivos modificados.
- Preferir menos de 220 líneas cambiadas.
## Outcome
- `backend/app/storage.py` adjunta a cada snapshot más reciente un `history_summary` ligero con capturas recientes, promedio de jugadores, pico, última vez visto online y minutos desde la última captura.
- `backend/app/payloads.py` expone esas métricas en `GET /api/servers/latest` sin abrir un endpoint nuevo.
- `frontend/assets/js/main.js` integra las métricas resumidas en las tarjetas reales e históricas ya renderizadas por la landing.
- `frontend/assets/css/styles.css` añade el bloque visual compacto para mostrar esos resúmenes sin convertir la landing en un dashboard pesado.
## Validation Result
- Ejecutado: import directo de `build_server_latest_payload()` desde `backend/`.
- Resultado: el payload devuelve `history_summary` por servidor con valores coherentes incluso cuando hay pocas capturas disponibles.
- Ejecutado: `node --check frontend/assets/js/main.js`.
- Resultado: sintaxis JavaScript válida tras integrar el bloque de métricas resumidas.
## Decision Notes
- Las métricas históricas resumidas se calculan sobre una ventana corta de snapshots recientes y se entregan junto al payload de último estado para mantener el backend simple y evitar analítica o endpoints adicionales en esta fase.

View File

@@ -0,0 +1,67 @@
# TASK-039-real-server-panel-data-density-pass
## Goal
Mejorar la densidad informativa y la claridad de las tarjetas de servidores reales, aprovechando mejor los datos ya disponibles sin recargar visualmente la landing.
## Context
Tras ocultar los placeholders cuando existen snapshots reales y preparar una CTA de conexión en español, el panel real ya tiene buena base. Sin embargo, todavía puede ganar valor mostrando mejor la información disponible y organizando mejor la jerarquía interna de las tarjetas.
## Steps
1. Revisar la composición actual de las tarjetas de servidores reales.
2. Revisar qué datos ya están disponibles o pasarán a estar disponibles tras la task de resúmenes históricos.
3. Mejorar la jerarquía interna de cada tarjeta:
- nombre
- estado
- mapa
- jugadores
- región
- última captura
- CTA
- métricas resumidas si existen
4. Hacer que la tarjeta transmita más información útil sin volverse pesada.
5. Mantener coherencia visual con la landing actual.
6. Mejorar legibilidad en desktop y móvil.
7. No rediseñar toda la página ni añadir nuevas secciones grandes.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- frontend/index.html
- frontend/assets/js/main.js
- frontend/assets/css/styles.css
- backend/app/payloads.py
- backend/app/routes.py
## Expected Files to Modify
- frontend/index.html
- frontend/assets/js/main.js
- frontend/assets/css/styles.css
## Constraints
- No tocar backend salvo si es estrictamente necesario por alineación menor con payloads ya existentes.
- No añadir librerías nuevas.
- No romper el fallback ni la CTA Conectar.
- No hacer cambios destructivos.
- Mantener la mejora centrada en claridad, jerarquía y densidad informativa.
## Validation
- Las tarjetas reales muestran mejor la información útil.
- La jerarquía interna mejora.
- La experiencia visual sigue siendo limpia.
- El panel se entiende mejor en desktop y móvil.
## Change Budget
- Preferir menos de 3 archivos modificados.
- Preferir menos de 180 líneas cambiadas.
## Outcome
- `frontend/assets/js/main.js` reorganiza la jerarquía interna de las tarjetas reales para destacar nombre, estado, población actual, mapa, región, última captura, CTA y métricas resumidas.
- `frontend/assets/css/styles.css` refuerza la densidad informativa con una columna compacta de estado/población y una fila de quick facts legible en desktop y móvil.
- La CTA `Conectar`, la tendencia reciente y el fallback existente se mantienen operativos.
## Validation Result
- Ejecutado: `node --check frontend/assets/js/main.js`.
- Resultado: sintaxis JavaScript válida tras el ajuste de jerarquía y densidad visual.
- Revisado en código: la versión enriquecida de la tarjeta conserva `steam://connect/<host>:<game_port>` para snapshots reales y mantiene el fallback cuando no hay datos reales utilizables.
## Decision Notes
- La mejora de densidad se concentró dentro de la tarjeta existente, sin añadir nuevas secciones grandes ni cambiar la arquitectura del panel.

View File

@@ -0,0 +1,60 @@
# TASK-040-full-width-page-shell-and-section-width-rebalance
## Goal
Reorganizar el layout general de la landing para aprovechar mucho mejor el ancho de página disponible, separando el ancho útil de las distintas secciones y eliminando la sensación de página excesivamente estrecha.
## Context
La UI actual ya funciona y muestra datos reales, pero la composición general está demasiado constreñida en ancho. Esto provoca mucho espacio muerto lateral, reduce el impacto visual del diseño y comprime en exceso el panel de servidores. La landing necesita una estructura de anchuras más flexible y más coherente con un formato panorámico de desktop.
## Steps
1. Revisar la estructura general de contenedores de `frontend/index.html`.
2. Revisar el sistema de anchuras, `max-width`, paddings laterales y shells actuales en `frontend/assets/css/styles.css`.
3. Reorganizar la página para que no todas las secciones dependan del mismo ancho máximo.
4. Definir al menos una separación clara entre:
- ancho del hero
- ancho del tráiler
- ancho del panel de servidores
5. Aumentar el ancho útil del bloque de servidores para aprovechar mejor desktop amplio.
6. Mantener el diseño responsive y sin romper móvil o tablet.
7. Preservar la identidad visual actual sin rediseñar completamente la página.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- frontend/index.html
- frontend/assets/css/styles.css
- frontend/assets/js/main.js
## Expected Files to Modify
- frontend/index.html
- frontend/assets/css/styles.css
## Constraints
- No tocar backend.
- No añadir librerías nuevas.
- No romper el comportamiento actual del frontend.
- No hacer cambios destructivos.
- Mantener la landing coherente con el tono visual actual.
## Validation
- La página ocupa mejor el ancho disponible en desktop.
- El panel de servidores gana anchura real.
- El hero y el tráiler mantienen buen equilibrio visual.
- La UI sigue funcionando correctamente en tablet y móvil.
## Change Budget
- Preferir menos de 3 archivos modificados.
- Preferir menos de 180 líneas cambiadas.
## Outcome
- `frontend/assets/css/styles.css` separa el ancho del hero, del bloque de trailer y del panel de servidores con shells independientes.
- El `page-shell` deja de imponer un unico ancho maximo a toda la landing y el panel de servidores gana mas ancho util en desktop.
- La mejora se mantuvo en CSS para no introducir cambios estructurales innecesarios en el HTML.
## Validation Result
- Revisado en diff: el ajuste queda limitado a `frontend/assets/css/styles.css` y al propio archivo de task.
- Revisado en codigo: hero, trailer y panel de servidores usan anchuras maximas distintas y el layout movil conserva los resets responsive existentes.
- Limitacion: no se ejecuto una comprobacion visual automatizada del navegador en esta tarea.
## Decision Notes
- La separacion de shells se resolvio con anchuras por seccion en lugar de crear nuevos wrappers HTML, para mantener la landing estable y reducir riesgo sobre el frontend actual.

View File

@@ -0,0 +1,68 @@
# TASK-041-real-server-card-layout-reorganization
## Goal
Reorganizar internamente las tarjetas de servidores reales para que aprovechen mejor el espacio horizontal, reduzcan compresión vertical y mejoren la legibilidad de los datos.
## Context
Las tarjetas actuales contienen información útil, pero en una composición demasiado estrecha y alta. Eso hace que varias métricas queden apelotonadas, con mala jerarquía y sensación de desborde. Esta task debe rediseñar la distribución interna de la tarjeta, no la arquitectura funcional del panel.
## Steps
1. Revisar la estructura HTML y JS que renderiza las tarjetas de servidores reales.
2. Reorganizar la composición interna para que la tarjeta tenga una lectura más horizontal y clara.
3. Priorizar visualmente:
- nombre del servidor
- estado
- botón Conectar
- jugadores
- mapa
- región
- última captura
- métricas resumidas
4. Reducir altura innecesaria y mejorar la distribución interna de bloques.
5. Mantener la CTA de conexión visible y bien integrada.
6. Mejorar el comportamiento de nombres largos y textos densos.
7. No romper el fallback ni la lógica actual del panel.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- frontend/index.html
- frontend/assets/js/main.js
- frontend/assets/css/styles.css
- backend/app/payloads.py
## Expected Files to Modify
- frontend/index.html
- frontend/assets/js/main.js
- frontend/assets/css/styles.css
## Constraints
- No tocar backend salvo alineación mínima si fuera imprescindible.
- No añadir librerías nuevas.
- No romper la CTA Conectar.
- No hacer cambios destructivos.
- Mantener la mejora centrada en layout y legibilidad.
## Validation
- Las tarjetas reales son más legibles y aprovechan mejor el ancho.
- La jerarquía interna mejora claramente.
- El texto largo deja de sentirse comprimido o desbordado.
- La UI mantiene coherencia visual con el resto de la landing.
## Change Budget
- Preferir menos de 3 archivos modificados.
- Preferir menos de 180 líneas cambiadas.
## Outcome
- `frontend/assets/js/main.js` reorganiza la tarjeta real en dos zonas: cabecera prioritaria con estado, poblacion y CTA, y cuerpo con quick facts, resumen historico y tendencia.
- `frontend/assets/css/styles.css` adapta la tarjeta a una lectura mas horizontal en desktop y endurece el wrapping de nombres y valores largos.
- No fue necesario tocar `frontend/index.html` ni alinear payloads del backend.
## Validation Result
- Ejecutado: `node --check frontend/assets/js/main.js`.
- Resultado: sintaxis JavaScript valida tras la reorganizacion del render.
- Revisado en codigo: la CTA `Conectar` sigue ligada a `steam://connect/<host>:<game_port>` y solo aparece para snapshots reales A2S.
- Limitacion: no se ejecuto una comprobacion visual automatizada del navegador en esta tarea.
## Decision Notes
- La reduccion de altura se resolvio moviendo resumen y tendencia a una composicion interna mas horizontal, sin cambiar la logica de secciones del panel ni el fallback existente.

View File

@@ -0,0 +1,62 @@
# TASK-042-server-panel-grid-and-overflow-hardening
## Goal
Fortalecer la rejilla del panel de servidores y corregir problemas de compresión, desborde y distribución espacial en desktop, tablet y móvil.
## Context
El panel de servidores ya consume datos reales y funciona correctamente, pero la rejilla actual no está aprovechando bien el espacio horizontal y deja tarjetas demasiado estrechas. Además, la estructura necesita endurecerse frente a nombres largos, métricas densas y cambios de anchura de viewport.
## Steps
1. Revisar la rejilla actual del panel de servidores.
2. Ajustar el sistema de columnas para permitir una distribución más robusta y flexible.
3. Usar una estrategia de `grid` adecuada para:
- desktop ancho
- desktop medio
- tablet
- móvil
4. Revisar `min-width`, `max-width`, `gap`, `overflow-wrap` y cualquier punto de compresión o desborde.
5. Asegurar que las tarjetas no se rompan visualmente con contenido real.
6. Mantener una composición limpia, con buen aire y buen equilibrio.
7. No rediseñar toda la landing fuera del panel de servidores.
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- frontend/index.html
- frontend/assets/css/styles.css
- frontend/assets/js/main.js
## Expected Files to Modify
- frontend/assets/css/styles.css
- frontend/index.html
- opcionalmente frontend/assets/js/main.js si hay que ajustar clases o hooks del render
## Constraints
- No tocar backend.
- No añadir librerías nuevas.
- No romper el comportamiento actual del panel.
- No hacer cambios destructivos.
- Mantener el trabajo centrado en grid, responsive y overflow.
## Validation
- El panel de servidores ocupa mejor el ancho disponible.
- Las tarjetas ya no se sienten excesivamente estrechas.
- Se corrigen problemas de compresión y desborde.
- El comportamiento responsive mejora en desktop, tablet y móvil.
## Change Budget
- Preferir menos de 3 archivos modificados.
- Preferir menos de 160 líneas cambiadas.
## Outcome
- `frontend/assets/css/styles.css` sustituye la rejilla fija de dos columnas por un `auto-fit` con `minmax`, permitiendo mejor reparto en desktop ancho y desktop medio.
- El mismo archivo endurece el panel con `min-width: 0`, `max-width: 100%`, `overflow: hidden` y subrejillas flexibles para quick facts y resumen.
- No fue necesario tocar `frontend/index.html` ni `frontend/assets/js/main.js`.
## Validation Result
- Revisado en codigo: el panel ahora define comportamiento especifico para desktop ancho, desktop medio, tablet y movil mediante breakpoints en `1120px`, `760px` y el ajuste movil existente.
- Revisado en diff: la task queda limitada a `frontend/assets/css/styles.css` y al archivo de task.
- Limitacion: no se ejecuto una comprobacion visual automatizada del navegador en esta tarea.
## Decision Notes
- La hardening del panel se resolvio a nivel de grid y overflow sin volver a rediseñar las tarjetas ni tocar la logica de render ya ajustada en la task anterior.

View File

@@ -1,48 +0,0 @@
# TASK-001-platform-readiness-check
## Goal
Validate that the AI development platform is integrated into HLL Vietnam with the required documentation, task structure and automation entry points.
## Context
This task exists only as a technical readiness reference. It is not a product feature task and should not change the landing or add backend behavior.
## Steps
1. Verify that `AGENTS.md` points to the task workflow.
2. Verify that `ai/` contains context, architecture, prompts and orchestrator role docs.
3. Verify that `scripts/` contains the expected platform scripts.
4. Verify that the repository remains consistent after integration.
## Files to Read First
- `AGENTS.md`
- `ai/README.md`
- `ai/repo-context.md`
- `ai/architecture-index.md`
- `ai/orchestrator/README.md`
## Expected Files to Modify
- None unless a platform consistency issue is found
## Constraints
- Do not modify product-facing files unless a platform integration issue requires it.
- Do not add new product tasks.
- Keep this task as a platform validation reference only.
## Validation
Before completing the task ensure:
- required platform files exist
- task paths are present
- scripts are present
- repository documentation is coherent
## Change Budget
- Prefer zero code changes.
- If any correction is needed, keep it minimal and platform-scoped.

1
ai/worker.lock Normal file
View File

@@ -0,0 +1 @@
39568

View File

@@ -1,5 +1,389 @@
# Backend # Backend
Esta carpeta queda preparada para la futura implementación del backend principal en Python. Esta carpeta contiene el bootstrap minimo del futuro backend principal en Python para HLL Vietnam.
En esta fase inicial no se incluye lógica funcional ni servicios activos. La intención es reservar una estructura clara para incorporar la aplicación, dependencias y configuración sin reorganizar el repositorio más adelante. ## 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
|-- 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`
Valor por defecto de `HLL_BACKEND_ALLOWED_ORIGINS`:
- `null`
- `http://127.0.0.1:5500`
- `http://127.0.0.1:8080`
- `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/servers` devuelve en esta fase un payload controlado con servidores
actuales de Hell Let Loose usados como referencia provisional. La respuesta
incluye `title`, `context`, `source` e `items`, y no representa todavia datos
reales de HLL Vietnam ni una integracion externa validada.
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__.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.
- `config.py` centraliza host, puerto y allowlist minima de origenes locales.
- `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`
Por defecto el archivo se crea en:
```text
backend/data/hll_vietnam_dev.sqlite3
```
Variable opcional:
- `HLL_BACKEND_STORAGE_PATH`
- `HLL_BACKEND_A2S_TARGETS`
La base logica sigue documentada en
`docs/stats-database-schema-foundation.md`. Esta implementacion no introduce
ORM, migraciones ni una decision de almacenamiento productivo.
## 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 `300` 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
```
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`.
## 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:8080`
- `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.
## 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`.

View File

@@ -1 +1,59 @@
"""Base package for the future HLL Vietnam Python backend.""" """Minimal bootstrap package for the HLL Vietnam Python backend."""
from .config import get_allowed_origins, get_bind_address
from .main import create_server, run
from .normalizers import normalize_a2s_server_info, normalize_server_record
from .payloads import build_health_payload
from .routes import resolve_get_payload
from .snapshots import build_server_snapshot, build_snapshot_batch, utc_now
from .storage import initialize_storage, persist_snapshot_batch
def collect_server_snapshots(*args: object, **kwargs: object) -> dict[str, object]:
"""Proxy collector access without importing the module during package init."""
from .collector import collect_server_snapshots as _collect_server_snapshots
return _collect_server_snapshots(*args, **kwargs)
def fetch_a2s_probe(*args: object, **kwargs: object) -> dict[str, object]:
"""Proxy A2S probe access without importing the collector during package init."""
from .collector import fetch_a2s_probe as _fetch_a2s_probe
return _fetch_a2s_probe(*args, **kwargs)
def query_server_info(*args: object, **kwargs: object) -> object:
"""Proxy A2S info queries without importing the module during package init."""
from .a2s_client import query_server_info as _query_server_info
return _query_server_info(*args, **kwargs)
def fetch_controlled_server_source() -> tuple[dict[str, object], ...]:
"""Proxy the controlled source without importing the module during package init."""
from .collector import (
fetch_controlled_server_source as _fetch_controlled_server_source,
)
return tuple(_fetch_controlled_server_source())
__all__ = [
"build_health_payload",
"build_server_snapshot",
"build_snapshot_batch",
"collect_server_snapshots",
"create_server",
"fetch_a2s_probe",
"fetch_controlled_server_source",
"get_allowed_origins",
"get_bind_address",
"initialize_storage",
"normalize_a2s_server_info",
"normalize_server_record",
"persist_snapshot_batch",
"query_server_info",
"resolve_get_payload",
"run",
"utc_now",
]

176
backend/app/a2s_client.py Normal file
View File

@@ -0,0 +1,176 @@
"""Minimal Steam A2S info client for development-time HLL server probes."""
from __future__ import annotations
import argparse
import json
import socket
import struct
from dataclasses import asdict, dataclass
DEFAULT_A2S_TIMEOUT = 6.0
_A2S_PREFIX = b"\xFF\xFF\xFF\xFF"
_A2S_INFO_REQUEST = _A2S_PREFIX + b"\x54Source Engine Query\x00"
_A2S_CHALLENGE_RESPONSE = 0x41
_A2S_INFO_RESPONSE = 0x49
class A2SError(RuntimeError):
"""Base error for A2S query failures."""
class A2STimeoutError(A2SError):
"""Raised when an A2S query does not complete before the timeout."""
class A2SProtocolError(A2SError):
"""Raised when an A2S server returns an unexpected payload."""
@dataclass(frozen=True, slots=True)
class A2SServerInfo:
"""Minimal metadata returned by an A2S info query."""
host: str
query_port: int
server_name: str
map_name: str | None
players: int
max_players: int
protocol: int
folder: str | None = None
game: str | None = None
version: str | None = None
def query_server_info(
host: str,
query_port: int,
*,
timeout: float = DEFAULT_A2S_TIMEOUT,
) -> A2SServerInfo:
"""Query one server using A2S_INFO and return minimal reusable metadata."""
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as udp_socket:
udp_socket.settimeout(timeout)
address = (host, query_port)
try:
udp_socket.sendto(_A2S_INFO_REQUEST, address)
payload = _receive_packet(udp_socket)
if _is_challenge_packet(payload):
challenge = payload[5:9]
udp_socket.sendto(_A2S_INFO_REQUEST + challenge, address)
payload = _receive_packet(udp_socket)
except socket.timeout as error:
raise A2STimeoutError(
f"A2S query to {host}:{query_port} timed out after {timeout:.1f}s."
) from error
except OSError as error:
raise A2SError(
f"A2S query to {host}:{query_port} failed: {error}."
) from error
return _parse_info_payload(payload, host=host, query_port=query_port)
def main() -> None:
"""Allow a direct development-time probe of one A2S target."""
parser = argparse.ArgumentParser(description="Probe one server with A2S_INFO.")
parser.add_argument("host", help="Server hostname or IPv4 address.")
parser.add_argument("query_port", type=int, help="Server Steam query port.")
parser.add_argument(
"--timeout",
type=float,
default=DEFAULT_A2S_TIMEOUT,
help="Socket timeout in seconds.",
)
args = parser.parse_args()
payload = asdict(
query_server_info(args.host, args.query_port, timeout=args.timeout)
)
print(json.dumps(payload, indent=2))
def _receive_packet(udp_socket: socket.socket) -> bytes:
payload, _ = udp_socket.recvfrom(4096)
return payload
def _is_challenge_packet(payload: bytes) -> bool:
return (
len(payload) >= 9
and payload.startswith(_A2S_PREFIX)
and payload[4] == _A2S_CHALLENGE_RESPONSE
)
def _parse_info_payload(
payload: bytes,
*,
host: str,
query_port: int,
) -> A2SServerInfo:
if len(payload) < 6 or not payload.startswith(_A2S_PREFIX):
raise A2SProtocolError("A2S response did not include the expected packet header.")
if payload[4] != _A2S_INFO_RESPONSE:
raise A2SProtocolError(
f"A2S response type {payload[4]!r} is not an info response."
)
protocol = payload[5]
offset = 6
server_name, offset = _read_c_string(payload, offset)
map_name, offset = _read_c_string(payload, offset)
folder, offset = _read_c_string(payload, offset)
game, offset = _read_c_string(payload, offset)
offset += 2 # app id
players = _read_byte(payload, offset)
max_players = _read_byte(payload, offset + 1)
offset += 6 # players, max, bots, server type, environment, visibility
offset += 1 # vac
version, offset = _read_c_string(payload, offset)
if offset < len(payload):
extra_data_flag = payload[offset]
offset += 1
if extra_data_flag & 0x80:
offset += 2
if extra_data_flag & 0x10:
_, offset = _read_c_string(payload, offset)
if extra_data_flag & 0x40:
offset += 2
offset += 8
if extra_data_flag & 0x20:
offset += 8
return A2SServerInfo(
host=host,
query_port=query_port,
server_name=server_name or "Unknown server",
map_name=map_name or None,
players=players,
max_players=max_players,
protocol=protocol,
folder=folder or None,
game=game or None,
version=version or None,
)
def _read_c_string(payload: bytes, offset: int) -> tuple[str, int]:
end = payload.find(b"\x00", offset)
if end == -1:
raise A2SProtocolError("A2S response ended before a null-terminated string.")
return payload[offset:end].decode("utf-8", errors="replace"), end + 1
def _read_byte(payload: bytes, offset: int) -> int:
if offset >= len(payload):
raise A2SProtocolError("A2S response ended before expected integer fields.")
return struct.unpack_from("<B", payload, offset)[0]
if __name__ == "__main__":
main()

269
backend/app/collector.py Normal file
View File

@@ -0,0 +1,269 @@
"""Minimal collector bootstrap for provisional server snapshots."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Callable, Mapping, Sequence
from .a2s_client import DEFAULT_A2S_TIMEOUT, query_server_info
from .normalizers import normalize_a2s_server_info, normalize_server_record
from .server_targets import A2SServerTarget, load_a2s_targets
from .snapshots import build_snapshot_batch, utc_now
from .storage import persist_snapshot_batch
RawSourceFetcher = Callable[[], Sequence[Mapping[str, object]]]
TargetProbe = Callable[[A2SServerTarget, float], Mapping[str, object]]
CONTROLLED_RAW_SERVER_SOURCE: tuple[dict[str, object], ...] = (
{
"external_server_id": "hll-esp-tactical-rotation",
"server_name": "HLL ESP Tactical Rotation",
"status": "online",
"players": 74,
"max_players": 100,
"current_map": "Sainte-Marie-du-Mont",
"region": "EU",
},
{
"external_server_id": "hll-latam-night-offensive",
"server_name": "HLL LATAM Night Offensive",
"status": "online",
"players": 51,
"max_players": 100,
"current_map": "Carentan",
"region": "LATAM",
},
{
"external_server_id": "hll-community-reserve",
"server_name": "HLL Community Reserve",
"status": "offline",
"players": 0,
"max_players": 100,
"current_map": None,
"region": "EU",
},
)
def fetch_controlled_server_source() -> Sequence[Mapping[str, object]]:
"""Return the controlled development source used by the collector bootstrap."""
return CONTROLLED_RAW_SERVER_SOURCE
def fetch_a2s_probe(
host: str,
query_port: int,
*,
timeout: float = DEFAULT_A2S_TIMEOUT,
source_name: str = "a2s-info",
external_server_id: str | None = None,
region: str | None = None,
) -> dict[str, object]:
"""Probe one A2S target and normalize its metadata for the collector model."""
server_info = query_server_info(host, query_port, timeout=timeout)
return normalize_a2s_server_info(
server_info,
source_name=source_name,
external_server_id=external_server_id,
region=region,
)
def fetch_configured_a2s_probes(
*,
timeout: float = DEFAULT_A2S_TIMEOUT,
probe_target: TargetProbe | None = None,
) -> tuple[dict[str, object], ...]:
"""Probe the configured A2S targets without hardcoding them in collector logic."""
probe = probe_target or _probe_configured_target
return tuple(
dict(probe(target, timeout))
for target in load_a2s_targets()
)
def collect_server_snapshots(
*,
fetch_raw_source: RawSourceFetcher = fetch_controlled_server_source,
source_name: str = "controlled-placeholder",
source_mode: str = "controlled",
timeout: float = DEFAULT_A2S_TIMEOUT,
allow_controlled_fallback: bool = True,
probe_target: TargetProbe | None = None,
persist: bool = False,
db_path: Path | None = None,
) -> dict[str, object]:
"""Collect snapshot batches from controlled data, A2S, or auto mode."""
normalized_records, collection_details = _collect_normalized_records(
fetch_raw_source=fetch_raw_source,
source_name=source_name,
source_mode=source_mode,
timeout=timeout,
allow_controlled_fallback=allow_controlled_fallback,
probe_target=probe_target,
)
captured_at = utc_now()
payload = {
"source_name": collection_details["source_name"],
"collection_mode": collection_details["collection_mode"],
"fallback_used": collection_details["fallback_used"],
"target_count": collection_details["target_count"],
"success_count": collection_details["success_count"],
"errors": collection_details["errors"],
"captured_at": captured_at.isoformat().replace("+00:00", "Z"),
"snapshots": build_snapshot_batch(
normalized_records,
captured_at=captured_at,
),
}
if persist:
payload["storage"] = persist_snapshot_batch(
payload["snapshots"],
source_name=payload["source_name"],
captured_at=payload["captured_at"],
db_path=db_path,
)
return payload
def main() -> None:
"""Allow manual collector execution during development."""
parser = argparse.ArgumentParser(description="Collect development server snapshots.")
parser.add_argument(
"--source",
choices=("controlled", "a2s", "auto"),
default="auto",
help="Choose controlled data, configured A2S targets, or auto with fallback.",
)
parser.add_argument(
"--timeout",
type=float,
default=DEFAULT_A2S_TIMEOUT,
help="Socket timeout in seconds for A2S probes.",
)
parser.add_argument(
"--no-fallback",
action="store_true",
help="Disable fallback to controlled data when A2S fails.",
)
args = parser.parse_args()
payload = collect_server_snapshots(
source_mode=args.source,
timeout=args.timeout,
allow_controlled_fallback=not args.no_fallback,
persist=True,
)
print(json.dumps(payload, indent=2))
def _collect_normalized_records(
*,
fetch_raw_source: RawSourceFetcher,
source_name: str,
source_mode: str,
timeout: float,
allow_controlled_fallback: bool,
probe_target: TargetProbe | None,
) -> tuple[list[dict[str, object]], dict[str, object]]:
if source_mode == "controlled":
raw_records = fetch_raw_source()
return (
[
normalize_server_record(record, source_name=source_name)
for record in raw_records
],
{
"source_name": source_name,
"collection_mode": "controlled",
"fallback_used": False,
"target_count": 0,
"success_count": 0,
"errors": [],
},
)
configured_targets = load_a2s_targets()
records: list[dict[str, object]] = []
errors: list[dict[str, object]] = []
probe = probe_target or _probe_configured_target
for target in configured_targets:
try:
records.append(dict(probe(target, timeout)))
except Exception as error: # noqa: BLE001 - keep collector failures controlled
errors.append(
{
"target": target.name,
"host": target.host,
"query_port": target.query_port,
"message": str(error),
}
)
if records:
return (
records,
{
"source_name": "a2s-info",
"collection_mode": "a2s",
"fallback_used": False,
"target_count": len(configured_targets),
"success_count": len(records),
"errors": errors,
},
)
if source_mode == "a2s" or not allow_controlled_fallback:
return (
[],
{
"source_name": "a2s-info",
"collection_mode": "a2s",
"fallback_used": False,
"target_count": len(configured_targets),
"success_count": 0,
"errors": errors,
},
)
raw_records = fetch_raw_source()
normalized_records = [
normalize_server_record(record, source_name=source_name)
for record in raw_records
]
return (
normalized_records,
{
"source_name": source_name,
"collection_mode": "controlled-fallback",
"fallback_used": True,
"target_count": len(configured_targets),
"success_count": 0,
"errors": errors,
},
)
def _probe_configured_target(
target: A2SServerTarget,
timeout: float,
) -> dict[str, object]:
return fetch_a2s_probe(
target.host,
target.query_port,
timeout=timeout,
source_name=target.source_name,
external_server_id=target.external_server_id,
region=target.region,
)
if __name__ == "__main__":
main()

77
backend/app/config.py Normal file
View File

@@ -0,0 +1,77 @@
"""Local development configuration for the HLL Vietnam backend bootstrap."""
from __future__ import annotations
import os
from pathlib import Path
DEFAULT_HOST = "127.0.0.1"
DEFAULT_PORT = 8000
DEFAULT_STORAGE_FILENAME = "hll_vietnam_dev.sqlite3"
DEFAULT_REFRESH_INTERVAL_SECONDS = 300
DEFAULT_ALLOWED_ORIGINS = (
"null",
"http://127.0.0.1:5500",
"http://127.0.0.1:8080",
"http://localhost:5500",
"http://localhost:8080",
)
DEFAULT_A2S_TARGETS_ENV_VAR = "HLL_BACKEND_A2S_TARGETS"
DEFAULT_A2S_SOURCE_NAME = "community-hispana-a2s"
def get_bind_address() -> tuple[str, int]:
"""Return the host and port used by the local backend bootstrap."""
host = os.getenv("HLL_BACKEND_HOST", DEFAULT_HOST)
port = int(os.getenv("HLL_BACKEND_PORT", str(DEFAULT_PORT)))
return host, port
def get_allowed_origins() -> tuple[str, ...]:
"""Return the small allowlist used for local frontend development."""
raw_origins = os.getenv(
"HLL_BACKEND_ALLOWED_ORIGINS",
",".join(DEFAULT_ALLOWED_ORIGINS),
)
origins = []
for origin in raw_origins.split(","):
normalized_origin = _normalize_origin(origin)
if normalized_origin:
origins.append(normalized_origin)
return tuple(origins) or DEFAULT_ALLOWED_ORIGINS
def _normalize_origin(origin: str) -> str:
"""Normalize configured origins so env overrides match browser Origin values."""
return origin.strip().rstrip("/")
def get_storage_path() -> Path:
"""Return the local SQLite path used for development snapshot persistence."""
default_path = Path(__file__).resolve().parent.parent / "data" / DEFAULT_STORAGE_FILENAME
configured_path = os.getenv("HLL_BACKEND_STORAGE_PATH")
return Path(configured_path) if configured_path else default_path
def get_refresh_interval_seconds() -> int:
"""Return the default interval used by the local refresh loop."""
configured_value = os.getenv(
"HLL_BACKEND_REFRESH_INTERVAL_SECONDS",
str(DEFAULT_REFRESH_INTERVAL_SECONDS),
)
interval_seconds = int(configured_value)
if interval_seconds <= 0:
raise ValueError("HLL_BACKEND_REFRESH_INTERVAL_SECONDS must be positive.")
return interval_seconds
def get_a2s_targets_payload() -> str | None:
"""Return the optional JSON payload that overrides local A2S targets."""
raw_payload = os.getenv(DEFAULT_A2S_TARGETS_ENV_VAR)
if raw_payload is None:
return None
normalized = raw_payload.strip()
return normalized or None

73
backend/app/main.py Normal file
View File

@@ -0,0 +1,73 @@
"""Minimal HTTP entrypoint for the HLL Vietnam backend bootstrap."""
from __future__ import annotations
import json
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from .config import get_allowed_origins, get_bind_address
from .routes import resolve_get_payload
class HealthHandler(BaseHTTPRequestHandler):
"""Serve the minimal routes required for the backend bootstrap."""
server_version = "HLLVietnamBackend/0.1"
def do_OPTIONS(self) -> None: # noqa: N802 - BaseHTTPRequestHandler interface
self.send_response(HTTPStatus.NO_CONTENT)
self._send_default_headers()
self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.end_headers()
def do_GET(self) -> None: # noqa: N802 - BaseHTTPRequestHandler interface
status, payload = resolve_get_payload(self.path)
if status is None:
self._write_json(
HTTPStatus.NOT_FOUND,
{"status": "error", "message": "Route not found"},
)
return
self._write_json(status, payload)
def log_message(self, format: str, *args: object) -> None:
# Keep local startup output clean unless future tasks need request logging.
return
def _write_json(self, status: HTTPStatus, payload: dict[str, object]) -> None:
body = json.dumps(payload).encode("utf-8")
self.send_response(status)
self._send_default_headers(content_length=len(body))
self.end_headers()
self.wfile.write(body)
def _send_default_headers(self, content_length: int | None = None) -> None:
origin = self.headers.get("Origin")
if origin in get_allowed_origins():
self.send_header("Access-Control-Allow-Origin", origin)
self.send_header("Vary", "Origin")
self.send_header("Content-Type", "application/json; charset=utf-8")
if content_length is not None:
self.send_header("Content-Length", str(content_length))
def create_server() -> ThreadingHTTPServer:
"""Build the HTTP server using the package-supported handler and bind settings."""
host, port = get_bind_address()
return ThreadingHTTPServer((host, port), HealthHandler)
def run() -> None:
"""Start the local bootstrap server."""
host, port = get_bind_address()
server = create_server()
print(f"HLL Vietnam backend bootstrap listening on http://{host}:{port}")
server.serve_forever()
if __name__ == "__main__":
run()

View File

@@ -0,0 +1,89 @@
"""Normalization helpers for provisional server collection flows."""
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import Mapping
if TYPE_CHECKING:
from .a2s_client import A2SServerInfo
def normalize_server_record(
raw_record: Mapping[str, object],
*,
source_name: str,
) -> dict[str, object]:
"""Normalize a raw server record into the collector's internal shape."""
external_server_id = _string_or_none(raw_record.get("external_server_id"))
return {
"external_server_id": external_server_id,
"server_name": _string_or_default(raw_record.get("server_name"), "Unknown server"),
"status": _normalize_status(raw_record.get("status")),
"players": _coerce_int(raw_record.get("players")),
"max_players": _coerce_int(raw_record.get("max_players")),
"current_map": _string_or_none(raw_record.get("current_map")),
"region": _string_or_none(raw_record.get("region")),
"source_name": source_name,
"snapshot_origin": "controlled-fallback",
"source_ref": external_server_id or source_name,
}
def normalize_a2s_server_info(
server_info: "A2SServerInfo",
*,
source_name: str,
external_server_id: str | None = None,
region: str | None = None,
) -> dict[str, object]:
"""Normalize a probed A2S payload into the collector's internal shape."""
resolved_external_id = external_server_id or (
f"a2s:{server_info.host}:{server_info.query_port}"
)
return {
"external_server_id": resolved_external_id,
"server_name": server_info.server_name or "Unknown server",
"status": "online",
"players": server_info.players,
"max_players": server_info.max_players,
"current_map": server_info.map_name,
"region": region,
"source_name": source_name,
"snapshot_origin": "real-a2s",
"source_ref": f"a2s://{server_info.host}:{server_info.query_port}",
}
def _normalize_status(value: object) -> str:
if not isinstance(value, str):
return "unknown"
normalized = value.strip().lower()
if normalized in {"online", "offline", "unknown"}:
return normalized
return "unknown"
def _coerce_int(value: object) -> int | None:
if value is None:
return None
try:
return int(value)
except (TypeError, ValueError):
return None
def _string_or_none(value: object) -> str | None:
if not isinstance(value, str):
return None
stripped = value.strip()
return stripped or None
def _string_or_default(value: object, default: str) -> str:
normalized = _string_or_none(value)
return normalized or default

180
backend/app/payloads.py Normal file
View File

@@ -0,0 +1,180 @@
"""Placeholder payload builders for the HLL Vietnam backend."""
from __future__ import annotations
from .server_targets import load_a2s_targets
from .storage import list_latest_snapshots, list_server_history, list_snapshot_history
def build_health_payload() -> dict[str, str]:
"""Return a small status payload without committing to business contracts."""
return {
"status": "ok",
"service": "hll-vietnam-backend",
"phase": "bootstrap",
}
def build_community_payload() -> dict[str, object]:
"""Return placeholder community content aligned with the documented contract."""
return {
"status": "ok",
"data": {
"title": "Comunidad Hispana HLL Vietnam",
"summary": "Punto de encuentro para jugadores, escuadras y comunidad.",
"discord_invite_url": "https://discord.com/invite/PedEqZ2Xsa",
},
}
def build_trailer_payload() -> dict[str, object]:
"""Return placeholder trailer metadata for future frontend consumption."""
return {
"status": "ok",
"data": {
"video_url": "https://www.youtube.com/embed/JzYzYNVWZ_A",
"title": "Trailer HLL Vietnam",
"provider": "youtube",
},
}
def build_discord_payload() -> dict[str, object]:
"""Return public Discord placeholder data without real integration."""
return {
"status": "ok",
"data": {
"invite_url": "https://discord.com/invite/PedEqZ2Xsa",
"label": "Unirse al Discord",
"availability": "manual",
},
}
def build_servers_payload() -> dict[str, object]:
"""Return a controlled placeholder for current Hell Let Loose servers."""
return {
"status": "ok",
"data": {
"title": "Servidores actuales de Hell Let Loose",
"context": "current-hll-reference",
"source": "controlled-placeholder",
"items": [
{
"server_name": "HLL ESP Tactical Rotation",
"status": "online",
"players": 74,
"max_players": 100,
"current_map": "Sainte-Marie-du-Mont",
"region": "EU",
},
{
"server_name": "HLL LATAM Night Offensive",
"status": "online",
"players": 51,
"max_players": 100,
"current_map": "Carentan",
"region": "LATAM",
},
{
"server_name": "HLL Community Reserve",
"status": "offline",
"players": 0,
"max_players": 100,
"current_map": None,
"region": "EU",
},
],
},
}
def build_server_latest_payload() -> dict[str, object]:
"""Return the latest persisted snapshot for each known server."""
items = _enrich_server_items(list_latest_snapshots())
return {
"status": "ok",
"data": {
"title": "Ultimo estado conocido de servidores",
"context": "current-hll-history",
"source": "local-snapshot-storage",
"summary_window_size": 6,
"items": items,
},
}
def build_server_history_payload(*, limit: int = 20) -> dict[str, object]:
"""Return recent persisted snapshots across all known servers."""
items = _enrich_server_items(list_snapshot_history(limit=limit))
return {
"status": "ok",
"data": {
"title": "Historial reciente de servidores",
"context": "current-hll-history",
"source": "local-snapshot-storage",
"limit": limit,
"items": items,
},
}
def build_server_detail_history_payload(
server_id: str,
*,
limit: int = 20,
) -> dict[str, object]:
"""Return recent persisted snapshots for one server."""
items = _enrich_server_items(list_server_history(server_id, limit=limit))
return {
"status": "ok",
"data": {
"title": "Historial por servidor",
"context": "current-hll-history",
"source": "local-snapshot-storage",
"server_id": server_id,
"limit": limit,
"items": items,
},
}
def build_error_payload(message: str) -> dict[str, str]:
"""Return the shared error payload shape used by the backend bootstrap."""
return {
"status": "error",
"message": message,
}
def _enrich_server_items(items: list[dict[str, object]]) -> list[dict[str, object]]:
target_index = {
target.external_server_id: target
for target in load_a2s_targets()
if target.external_server_id
}
enriched_items: list[dict[str, object]] = []
for item in items:
enriched_items.append(_enrich_server_item(item, target_index))
return enriched_items
def _enrich_server_item(
item: dict[str, object],
target_index: dict[str, object],
) -> dict[str, object]:
enriched = dict(item)
external_server_id = enriched.get("external_server_id")
snapshot_origin = enriched.get("snapshot_origin")
target = target_index.get(external_server_id)
if not target or snapshot_origin != "real-a2s":
enriched["host"] = None
enriched["query_port"] = None
enriched["game_port"] = None
return enriched
enriched["host"] = target.host
enriched["query_port"] = target.query_port
enriched["game_port"] = target.game_port
return enriched

70
backend/app/routes.py Normal file
View File

@@ -0,0 +1,70 @@
"""Route resolution helpers for the HLL Vietnam backend bootstrap."""
from __future__ import annotations
from http import HTTPStatus
from urllib.parse import parse_qs, urlparse
from .payloads import (
build_community_payload,
build_discord_payload,
build_error_payload,
build_health_payload,
build_server_detail_history_payload,
build_server_history_payload,
build_server_latest_payload,
build_servers_payload,
build_trailer_payload,
)
GET_ROUTES = {
"/health": build_health_payload,
"/api/community": build_community_payload,
"/api/trailer": build_trailer_payload,
"/api/discord": build_discord_payload,
"/api/servers": build_servers_payload,
}
def resolve_get_payload(path: str) -> tuple[HTTPStatus | None, dict[str, object]]:
"""Resolve the JSON payload for a supported GET route."""
parsed = urlparse(path)
if parsed.path == "/api/servers/latest":
return HTTPStatus.OK, build_server_latest_payload()
if parsed.path == "/api/servers/history":
limit = _parse_limit(parsed.query)
if limit is None:
return HTTPStatus.BAD_REQUEST, build_error_payload("Invalid limit parameter")
return HTTPStatus.OK, build_server_history_payload(limit=limit)
builder = GET_ROUTES.get(parsed.path)
if builder is None:
if parsed.path.startswith("/api/servers/") and parsed.path.endswith("/history"):
server_id = parsed.path.removeprefix("/api/servers/").removesuffix("/history")
server_id = server_id.strip("/")
if not server_id:
return HTTPStatus.BAD_REQUEST, build_error_payload("Server id is required")
limit = _parse_limit(parsed.query)
if limit is None:
return HTTPStatus.BAD_REQUEST, build_error_payload("Invalid limit parameter")
return HTTPStatus.OK, build_server_detail_history_payload(server_id, limit=limit)
return None, {}
return HTTPStatus.OK, builder()
def _parse_limit(query: str) -> int | None:
raw_limit = parse_qs(query).get("limit", ["20"])[0]
try:
limit = int(raw_limit)
except ValueError:
return None
if limit < 1 or limit > 100:
return None
return limit

100
backend/app/scheduler.py Normal file
View File

@@ -0,0 +1,100 @@
"""Local development loop for periodic snapshot refreshes."""
from __future__ import annotations
import argparse
import json
import time
from .a2s_client import DEFAULT_A2S_TIMEOUT
from .collector import collect_server_snapshots
from .config import get_refresh_interval_seconds
def run_local_refresh_loop(
*,
interval_seconds: int,
source_mode: str,
timeout: float,
allow_controlled_fallback: bool,
max_runs: int | None = None,
) -> None:
"""Run the collector periodically until interrupted or the run limit is reached."""
completed_runs = 0
print(
"Starting local snapshot refresh loop "
f"(interval={interval_seconds}s, source={source_mode}, persist=true)."
)
print("Press Ctrl+C to stop.")
try:
while max_runs is None or completed_runs < max_runs:
completed_runs += 1
payload = collect_server_snapshots(
source_mode=source_mode,
timeout=timeout,
allow_controlled_fallback=allow_controlled_fallback,
persist=True,
)
print(json.dumps({"run": completed_runs, **payload}, indent=2))
if max_runs is not None and completed_runs >= max_runs:
break
time.sleep(interval_seconds)
except KeyboardInterrupt:
print("\nLocal snapshot refresh loop stopped by user.")
def main() -> None:
"""Allow local scheduled refresh execution without adding external infrastructure."""
parser = argparse.ArgumentParser(
description="Run periodic local snapshot refreshes for development.",
)
parser.add_argument(
"--interval",
type=int,
default=get_refresh_interval_seconds(),
help="Seconds to wait between persisted refresh runs.",
)
parser.add_argument(
"--source",
choices=("controlled", "a2s", "auto"),
default="auto",
help="Choose controlled data, configured A2S targets, or auto with fallback.",
)
parser.add_argument(
"--timeout",
type=float,
default=DEFAULT_A2S_TIMEOUT,
help="Socket timeout in seconds for A2S probes.",
)
parser.add_argument(
"--no-fallback",
action="store_true",
help="Disable fallback to controlled data when A2S fails.",
)
parser.add_argument(
"--max-runs",
type=int,
default=None,
help="Optional safety limit for the number of refresh cycles to execute.",
)
args = parser.parse_args()
if args.interval <= 0:
raise ValueError("--interval must be a positive integer.")
if args.max_runs is not None and args.max_runs <= 0:
raise ValueError("--max-runs must be positive when provided.")
run_local_refresh_loop(
interval_seconds=args.interval,
source_mode=args.source,
timeout=args.timeout,
allow_controlled_fallback=not args.no_fallback,
max_runs=args.max_runs,
)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,106 @@
"""Registry helpers for development-time A2S probe targets."""
from __future__ import annotations
import json
from dataclasses import dataclass
from .config import DEFAULT_A2S_SOURCE_NAME, get_a2s_targets_payload
DEFAULT_A2S_TARGETS = (
{
"name": "Comunidad Hispana #01",
"host": "152.114.195.174",
"query_port": 7778,
"game_port": 7777,
"source_name": DEFAULT_A2S_SOURCE_NAME,
"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": DEFAULT_A2S_SOURCE_NAME,
"external_server_id": "comunidad-hispana-02",
"region": "ES",
},
)
@dataclass(frozen=True, slots=True)
class A2SServerTarget:
"""Minimal configuration needed to query one A2S target."""
name: str
host: str
query_port: int
game_port: int | None
source_name: str
external_server_id: str | None = None
region: str | None = None
def load_a2s_targets() -> tuple[A2SServerTarget, ...]:
"""Load configured A2S targets from env JSON or the local default registry."""
raw_payload = get_a2s_targets_payload()
raw_targets = DEFAULT_A2S_TARGETS if raw_payload is None else _parse_targets(raw_payload)
return tuple(_coerce_target(item) for item in raw_targets)
def _parse_targets(raw_payload: str) -> list[dict[str, object]]:
try:
parsed = json.loads(raw_payload)
except json.JSONDecodeError as error:
raise ValueError("HLL_BACKEND_A2S_TARGETS must be valid JSON.") from error
if not isinstance(parsed, list):
raise ValueError("HLL_BACKEND_A2S_TARGETS must be a JSON array.")
return [item for item in parsed if isinstance(item, dict)]
def _coerce_target(raw_target: dict[str, object]) -> A2SServerTarget:
name = str(raw_target.get("name") or "Unnamed target").strip()
host = str(raw_target.get("host") or "").strip()
source_name = str(raw_target.get("source_name") or DEFAULT_A2S_SOURCE_NAME).strip()
query_port = int(raw_target.get("query_port") or 0)
game_port = _coerce_optional_positive_int(raw_target.get("game_port"))
external_server_id = _string_or_none(raw_target.get("external_server_id"))
region = _string_or_none(raw_target.get("region"))
if not host:
raise ValueError("Each A2S target must define a non-empty host.")
if query_port <= 0:
raise ValueError("Each A2S target must define a valid query_port.")
return A2SServerTarget(
name=name,
host=host,
query_port=query_port,
game_port=game_port,
source_name=source_name or DEFAULT_A2S_SOURCE_NAME,
external_server_id=external_server_id,
region=region,
)
def _string_or_none(value: object) -> str | None:
if not isinstance(value, str):
return None
normalized = value.strip()
return normalized or None
def _coerce_optional_positive_int(value: object) -> int | None:
if value is None:
return None
coerced = int(value)
if coerced <= 0:
raise ValueError("Each A2S target game_port must be positive when defined.")
return coerced

54
backend/app/snapshots.py Normal file
View File

@@ -0,0 +1,54 @@
"""Snapshot builders for normalized provisional server data."""
from __future__ import annotations
from datetime import datetime, timezone
from typing import Iterable, Mapping
def build_server_snapshot(
normalized_record: Mapping[str, object],
*,
captured_at: datetime,
) -> dict[str, object]:
"""Build a consistent snapshot payload for one normalized server."""
timestamp = _as_utc_timestamp(captured_at)
return {
"external_server_id": normalized_record.get("external_server_id"),
"server_name": normalized_record.get("server_name"),
"status": normalized_record.get("status"),
"players": normalized_record.get("players"),
"max_players": normalized_record.get("max_players"),
"current_map": normalized_record.get("current_map"),
"region": normalized_record.get("region"),
"source_name": normalized_record.get("source_name"),
"snapshot_origin": normalized_record.get("snapshot_origin"),
"source_ref": normalized_record.get("source_ref"),
"captured_at": timestamp,
}
def build_snapshot_batch(
normalized_records: Iterable[Mapping[str, object]],
*,
captured_at: datetime,
) -> list[dict[str, object]]:
"""Build snapshots for a batch captured at the same timestamp."""
return [
build_server_snapshot(record, captured_at=captured_at)
for record in normalized_records
]
def utc_now() -> datetime:
"""Return the current UTC timestamp for snapshot capture."""
return datetime.now(timezone.utc)
def _as_utc_timestamp(value: datetime) -> str:
if value.tzinfo is None:
value = value.replace(tzinfo=timezone.utc)
else:
value = value.astimezone(timezone.utc)
return value.isoformat().replace("+00:00", "Z")

514
backend/app/storage.py Normal file
View File

@@ -0,0 +1,514 @@
"""Local SQLite persistence for provisional server snapshots."""
from __future__ import annotations
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from typing import Iterable, Mapping
from .config import get_storage_path
DEFAULT_GAME_SOURCE = {
"slug": "current-hll",
"display_name": "Current Hell Let Loose",
"provider_kind": "development",
}
SUMMARY_SNAPSHOT_LIMIT = 6
def initialize_storage(*, db_path: Path | None = None) -> Path:
"""Create the local database file and minimal schema when missing."""
resolved_path = db_path or get_storage_path()
resolved_path.parent.mkdir(parents=True, exist_ok=True)
with _connect(resolved_path) as connection:
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS game_sources (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL,
provider_kind TEXT NOT NULL,
is_active INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS servers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
game_source_id INTEGER NOT NULL,
external_server_id TEXT,
server_name TEXT NOT NULL,
region TEXT,
first_seen_at TEXT NOT NULL,
last_seen_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (game_source_id, external_server_id),
FOREIGN KEY (game_source_id) REFERENCES game_sources(id)
);
CREATE TABLE IF NOT EXISTS server_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
server_id INTEGER NOT NULL,
captured_at TEXT NOT NULL,
status TEXT NOT NULL,
players INTEGER,
max_players INTEGER,
current_map TEXT,
source_name TEXT NOT NULL,
snapshot_origin TEXT,
source_ref TEXT,
raw_payload_ref TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (server_id) REFERENCES servers(id)
);
CREATE INDEX IF NOT EXISTS idx_server_snapshots_server_time
ON server_snapshots(server_id, captured_at);
"""
)
_ensure_server_snapshot_columns(connection)
return resolved_path
def persist_snapshot_batch(
snapshots: Iterable[Mapping[str, object]],
*,
source_name: str,
captured_at: str,
game_source: Mapping[str, str] | None = None,
db_path: Path | None = None,
) -> dict[str, object]:
"""Persist a batch of normalized snapshots into local SQLite storage."""
resolved_path = initialize_storage(db_path=db_path)
source_definition = dict(DEFAULT_GAME_SOURCE)
if game_source is not None:
source_definition.update(game_source)
persisted = 0
with _connect(resolved_path) as connection:
game_source_id = _upsert_game_source(connection, source_definition)
for snapshot in snapshots:
server_id = _upsert_server(
connection,
game_source_id=game_source_id,
snapshot=snapshot,
captured_at=captured_at,
)
connection.execute(
"""
INSERT INTO server_snapshots (
server_id,
captured_at,
status,
players,
max_players,
current_map,
source_name,
snapshot_origin,
source_ref,
raw_payload_ref
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
server_id,
captured_at,
snapshot.get("status"),
snapshot.get("players"),
snapshot.get("max_players"),
snapshot.get("current_map"),
snapshot.get("source_name") or source_name,
snapshot.get("snapshot_origin"),
snapshot.get("source_ref"),
None,
),
)
persisted += 1
return {
"db_path": str(resolved_path),
"captured_at": captured_at,
"persisted_snapshots": persisted,
"game_source_slug": source_definition["slug"],
}
def list_latest_snapshots(*, db_path: Path | None = None) -> list[dict[str, object]]:
"""Return the latest persisted snapshot for each known server."""
resolved_path = initialize_storage(db_path=db_path)
with _connect(resolved_path) as connection:
rows = connection.execute(
"""
SELECT
servers.id AS server_id,
servers.external_server_id,
servers.server_name,
servers.region,
game_sources.slug AS context,
server_snapshots.source_name,
server_snapshots.snapshot_origin,
server_snapshots.source_ref,
server_snapshots.captured_at,
server_snapshots.status,
server_snapshots.players,
server_snapshots.max_players,
server_snapshots.current_map
FROM servers
INNER JOIN game_sources
ON game_sources.id = servers.game_source_id
INNER JOIN server_snapshots
ON server_snapshots.server_id = servers.id
INNER JOIN (
SELECT server_id, MAX(captured_at) AS latest_captured_at
FROM server_snapshots
GROUP BY server_id
) AS latest
ON latest.server_id = server_snapshots.server_id
AND latest.latest_captured_at = server_snapshots.captured_at
ORDER BY servers.server_name ASC
"""
).fetchall()
items = [_serialize_snapshot_row(row) for row in rows]
return _attach_history_summaries(connection, items)
def list_snapshot_history(
*,
db_path: Path | None = None,
limit: int = 20,
) -> list[dict[str, object]]:
"""Return recent persisted snapshots across all servers."""
resolved_path = initialize_storage(db_path=db_path)
with _connect(resolved_path) as connection:
rows = connection.execute(
"""
SELECT
servers.id AS server_id,
servers.external_server_id,
servers.server_name,
servers.region,
game_sources.slug AS context,
server_snapshots.source_name,
server_snapshots.snapshot_origin,
server_snapshots.source_ref,
server_snapshots.captured_at,
server_snapshots.status,
server_snapshots.players,
server_snapshots.max_players,
server_snapshots.current_map
FROM server_snapshots
INNER JOIN servers
ON servers.id = server_snapshots.server_id
INNER JOIN game_sources
ON game_sources.id = servers.game_source_id
ORDER BY server_snapshots.captured_at DESC, servers.server_name ASC
LIMIT ?
""",
(limit,),
).fetchall()
return [_serialize_snapshot_row(row) for row in rows]
def list_server_history(
server_id: str,
*,
db_path: Path | None = None,
limit: int = 20,
) -> list[dict[str, object]]:
"""Return recent history for one server by numeric id or external id."""
resolved_path = initialize_storage(db_path=db_path)
server_filter, server_value = _build_server_filter(server_id)
with _connect(resolved_path) as connection:
rows = connection.execute(
f"""
SELECT
servers.id AS server_id,
servers.external_server_id,
servers.server_name,
servers.region,
game_sources.slug AS context,
server_snapshots.source_name,
server_snapshots.snapshot_origin,
server_snapshots.source_ref,
server_snapshots.captured_at,
server_snapshots.status,
server_snapshots.players,
server_snapshots.max_players,
server_snapshots.current_map
FROM server_snapshots
INNER JOIN servers
ON servers.id = server_snapshots.server_id
INNER JOIN game_sources
ON game_sources.id = servers.game_source_id
WHERE {server_filter} = ?
ORDER BY server_snapshots.captured_at DESC
LIMIT ?
""",
(server_value, limit),
).fetchall()
return [_serialize_snapshot_row(row) for row in rows]
def _connect(db_path: Path) -> sqlite3.Connection:
connection = sqlite3.connect(db_path)
connection.row_factory = sqlite3.Row
return connection
def _upsert_game_source(
connection: sqlite3.Connection,
game_source: Mapping[str, str],
) -> int:
connection.execute(
"""
INSERT INTO game_sources (slug, display_name, provider_kind, is_active)
VALUES (?, ?, ?, 1)
ON CONFLICT(slug) DO UPDATE SET
display_name = excluded.display_name,
provider_kind = excluded.provider_kind,
is_active = 1,
updated_at = CURRENT_TIMESTAMP
""",
(
game_source["slug"],
game_source["display_name"],
game_source["provider_kind"],
),
)
row = connection.execute(
"SELECT id FROM game_sources WHERE slug = ?",
(game_source["slug"],),
).fetchone()
if row is None:
raise RuntimeError("Failed to resolve game source during snapshot persistence.")
return int(row["id"])
def _upsert_server(
connection: sqlite3.Connection,
*,
game_source_id: int,
snapshot: Mapping[str, object],
captured_at: str,
) -> int:
external_server_id = snapshot.get("external_server_id")
if not isinstance(external_server_id, str) or not external_server_id.strip():
external_server_id = _build_fallback_external_id(snapshot)
server_name = str(snapshot.get("server_name") or "Unknown server")
region = snapshot.get("region")
connection.execute(
"""
INSERT INTO servers (
game_source_id,
external_server_id,
server_name,
region,
first_seen_at,
last_seen_at
) VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(game_source_id, external_server_id) DO UPDATE SET
server_name = excluded.server_name,
region = excluded.region,
last_seen_at = excluded.last_seen_at,
updated_at = CURRENT_TIMESTAMP
""",
(
game_source_id,
external_server_id,
server_name,
region,
captured_at,
captured_at,
),
)
row = connection.execute(
"""
SELECT id
FROM servers
WHERE game_source_id = ? AND external_server_id = ?
""",
(game_source_id, external_server_id),
).fetchone()
if row is None:
raise RuntimeError("Failed to resolve server during snapshot persistence.")
return int(row["id"])
def _build_fallback_external_id(snapshot: Mapping[str, object]) -> str:
server_name = str(snapshot.get("server_name") or "unknown-server")
normalized = "".join(
character.lower() if character.isalnum() else "-"
for character in server_name
)
compact = "-".join(part for part in normalized.split("-") if part)
return compact or "unknown-server"
def _ensure_server_snapshot_columns(connection: sqlite3.Connection) -> None:
columns = {
str(row["name"])
for row in connection.execute("PRAGMA table_info(server_snapshots)").fetchall()
}
if "snapshot_origin" not in columns:
connection.execute("ALTER TABLE server_snapshots ADD COLUMN snapshot_origin TEXT")
if "source_ref" not in columns:
connection.execute("ALTER TABLE server_snapshots ADD COLUMN source_ref TEXT")
connection.execute(
"""
UPDATE server_snapshots
SET snapshot_origin = CASE
WHEN source_name = 'controlled-placeholder' THEN 'controlled-fallback'
WHEN source_name LIKE '%a2s%' THEN 'real-a2s'
ELSE 'unknown'
END
WHERE snapshot_origin IS NULL OR snapshot_origin = ''
"""
)
connection.execute(
"""
UPDATE server_snapshots
SET source_ref = source_name
WHERE source_ref IS NULL OR source_ref = ''
"""
)
_backfill_registered_a2s_source_refs(connection)
def _backfill_registered_a2s_source_refs(connection: sqlite3.Connection) -> None:
from .server_targets import load_a2s_targets
for target in load_a2s_targets():
if not target.external_server_id:
continue
connection.execute(
"""
UPDATE server_snapshots
SET source_ref = ?
WHERE snapshot_origin = 'real-a2s'
AND source_ref = source_name
AND server_id IN (
SELECT id
FROM servers
WHERE external_server_id = ?
)
""",
(
f"a2s://{target.host}:{target.query_port}",
target.external_server_id,
),
)
def _serialize_snapshot_row(row: sqlite3.Row) -> dict[str, object]:
return {
"server_id": row["server_id"],
"external_server_id": row["external_server_id"],
"server_name": row["server_name"],
"region": row["region"],
"context": row["context"],
"source_name": row["source_name"],
"snapshot_origin": row["snapshot_origin"],
"source_ref": row["source_ref"],
"captured_at": row["captured_at"],
"status": row["status"],
"players": row["players"],
"max_players": row["max_players"],
"current_map": row["current_map"],
}
def _attach_history_summaries(
connection: sqlite3.Connection,
items: list[dict[str, object]],
) -> list[dict[str, object]]:
enriched_items: list[dict[str, object]] = []
for item in items:
enriched = dict(item)
enriched["history_summary"] = _build_history_summary(
connection,
int(item["server_id"]),
)
enriched_items.append(enriched)
return enriched_items
def _build_history_summary(
connection: sqlite3.Connection,
server_id: int,
) -> dict[str, object]:
rows = connection.execute(
"""
SELECT
captured_at,
status,
players
FROM server_snapshots
WHERE server_id = ?
ORDER BY captured_at DESC
LIMIT ?
""",
(server_id, SUMMARY_SNAPSHOT_LIMIT),
).fetchall()
return _summarize_history_rows(rows)
def _summarize_history_rows(rows: list[sqlite3.Row]) -> dict[str, object]:
capture_count = len(rows)
player_values = [
int(row["players"])
for row in rows
if row["players"] is not None
]
online_rows = [row for row in rows if row["status"] == "online"]
latest_captured_at = str(rows[0]["captured_at"]) if rows else None
last_seen_online_at = str(online_rows[0]["captured_at"]) if online_rows else None
return {
"window_size": SUMMARY_SNAPSHOT_LIMIT,
"recent_capture_count": capture_count,
"recent_online_count": len(online_rows),
"recent_average_players": _round_average(player_values),
"recent_peak_players": max(player_values, default=None),
"last_seen_online_at": last_seen_online_at,
"minutes_since_last_capture": _minutes_since_timestamp(latest_captured_at),
}
def _round_average(values: list[int]) -> float | None:
if not values:
return None
return round(sum(values) / len(values), 1)
def _minutes_since_timestamp(timestamp: str | None) -> int | None:
if not timestamp:
return None
normalized = timestamp.replace("Z", "+00:00")
captured_at = datetime.fromisoformat(normalized)
if captured_at.tzinfo is None:
captured_at = captured_at.replace(tzinfo=timezone.utc)
delta = datetime.now(timezone.utc) - captured_at.astimezone(timezone.utc)
return max(0, int(delta.total_seconds() // 60))
def _build_server_filter(server_id: str) -> tuple[str, object]:
normalized = server_id.strip()
if normalized.isdigit():
return "servers.id", int(normalized)
return "servers.external_server_id", normalized

Binary file not shown.

View File

@@ -1 +1 @@
# Dependencias del backend Python se definiran en fases posteriores. # El bootstrap actual usa solo la libreria estandar de Python.

View File

@@ -0,0 +1,130 @@
# Current HLL Data Ingestion Plan
## Objective
Definir una estrategia tecnica reutilizable para ingerir datos del Hell Let
Loose actual como banco de pruebas del futuro ecosistema HLL Vietnam, sin
implementar todavia una ingesta productiva completa.
## Initial Data Scope
Los primeros campos a capturar deben cubrir el bloque provisional de
servidores y preparar historicos minimos:
- `server_name`
- `status`
- `players`
- `max_players`
- `current_map` si la fuente lo permite
- `captured_at`
- `source`
- `external_server_id` o identificador equivalente si la fuente lo ofrece
Campos como `queue`, `ping`, `rotation` o `notes` quedan como opcionales para
fases posteriores y no deben bloquear el bootstrap.
## Snapshot Concept
Un snapshot representa el estado observado de un servidor en un momento
concreto. No es un perfil estatico del servidor, sino una captura puntual con
timestamp.
Cada snapshot debe permitir:
- reconstruir una serie temporal simple por servidor
- detectar cambios de estado online u offline
- medir evolucion basica de jugadores y capacidad
- conservar la procedencia de la captura
El identificador estable del servidor y el `captured_at` deben separar la
identidad del servidor de cada observacion historica.
## Ingestion Source Options
### Phase-safe controlled payload
- Fuente recomendada para el inicio.
- Permite probar el pipeline con datos mock o manuales servidos por backend.
- Fija el contrato de entrada y la normalizacion sin depender de terceros.
### Public external source
- Puede ser una API publica o un listado mantenido por terceros.
- Acerca el banco de pruebas a datos reales.
- Exige validar formato, disponibilidad, limites de uso y estabilidad antes de
consolidarlo.
### Direct server query or intermediary adapter
- Puede ofrecer datos mas cercanos al estado real del servidor.
- Introduce mayor complejidad tecnica, posibles timeouts y dependencia del
protocolo soportado.
- Debe encapsularse detras de un adaptador backend, no exponerse al frontend.
## Normalization Baseline
La captura y la fuente no deben definir el contrato interno final. La
arquitectura debe separar:
1. lectura de datos crudos
2. normalizacion a un modelo comun
3. produccion de snapshots consistentes
La normalizacion inicial debe garantizar:
- naming estable en `snake_case`
- `status` reducido a valores controlados como `online`, `offline` o `unknown`
- enteros para `players` y `max_players` cuando existan
- `captured_at` generado en backend
- conservacion del nombre de fuente para trazabilidad
## Risks And Limits
- Disponibilidad de terceros: una fuente publica puede dejar de responder sin
aviso.
- Cambios de formato: scraping o APIs no oficiales pueden romper el adaptador.
- Rate limits: las consultas frecuentes pueden exigir cache o polling mas
espaciado.
- Latencia: una consulta lenta no debe trasladarse directamente al frontend.
- CORS: el frontend no debe llamar a fuentes externas para este flujo.
- Fiabilidad: diferentes fuentes pueden discrepar en jugadores, mapa o estado.
- Dependencia no oficial: una integracion fragil no debe convertirse en pieza
critica del producto.
## Phased Architecture
### Phase 1: controlled payload and stable structure
- Mantener un payload controlado como base de `/api/servers`.
- Definir el modelo normalizado esperado para servidores y snapshots.
- No almacenar historico real todavia.
### Phase 2: snapshot collector with real or near-real source
- Introducir un colector backend desacoplado de la fuente concreta.
- Permitir ejecucion manual o periodica en entorno de desarrollo.
- Generar snapshots consistentes listos para futura persistencia.
### Phase 3: historical use and basic statistics
- Persistir snapshots.
- Calcular metricas iniciales como actividad por servidor, picos de jugadores o
ultima vez visto online.
- Mantener el modelo generico para reutilizarlo con HLL Vietnam cuando existan
datos mas representativos.
## Explicitly Out Of Scope Now
- ingesta real completa en produccion
- scraping productivo
- base de datos funcional
- tareas periodicas operativas
- metricas avanzadas o paneles analiticos
- cambios visibles en frontend
## Handoff To Following Tasks
- `TASK-019` debe convertir este plan en una base de esquema para persistir
servidores y snapshots.
- `TASK-020` debe preparar un bootstrap pequeno del colector en Python con
separacion entre fuente, normalizacion y snapshot.

View File

@@ -0,0 +1,130 @@
# Current HLL Servers Source Plan
## Objective
Definir como mostrar en la web de HLL Vietnam un bloque provisional con
servidores actuales de Hell Let Loose sin presentarlos como si fueran datos de
HLL Vietnam ni depender todavia de una integracion real externa.
## Product Framing
- El bloque debe presentarse como referencia provisional para la comunidad.
- El copy debe mencionar de forma explicita "servidores actuales de Hell Let
Loose" y evitar formulas ambiguas como "servidores HLL Vietnam".
- La UI debe dejar claro que el bloque sirve mientras no existan datos propios o
mas cercanos al contexto final de HLL Vietnam.
- Si no hay datos disponibles, el estado vacio debe ser neutral y honesto, sin
simular actividad inexistente.
## Recommended Fields For This Phase
Campos utiles para un bloque pequeno y entendible:
- `server_name`
- `status`
- `players`
- `max_players`
- `current_map`
- `region`
Campos opcionales solo si una fuente futura los ofrece de forma estable:
- `queue`
- `ping`
- `notes`
- `last_updated`
## Source Options
### Public external source
- Puede ser una API publica especializada, un listado publico o una consulta de
servidor compatible con el juego actual.
- Ventaja: acerca la web a datos mas reales.
- Riesgo: cambios de formato, limites de uso, CORS, disponibilidad y dependencia
de terceros.
### Controlled placeholder data
- Fuente recomendada para la primera implementacion.
- El backend expone un payload manual con forma realista y semantica estable.
- Permite validar UI, contrato y estados de error sin acoplar la web a una
fuente externa todavia no validada.
### Stronger future integration
- Un adaptador backend dedicado podra sustituir el placeholder cuando exista una
fuente fiable o un dataset controlado mantenido por la comunidad.
- La sustitucion debe preservar el contrato JSON para no romper al frontend.
## Risks And Restrictions
- Disponibilidad: una fuente externa puede caer o degradarse sin aviso.
- CORS: el frontend no debe depender de llamadas directas a terceros.
- Rate limits: una API publica puede limitar frecuencia o volumen.
- Formato: scraping o endpoints no oficiales pueden cambiar sin contrato.
- Mantenimiento: una integracion fragil crearia coste operativo prematuro.
- Identidad: el bloque no puede inducir a pensar que HLL Vietnam ya dispone de
servidores propios o datos oficiales.
## Phased Strategy
### Phase 1: controlled mock
- `GET /api/servers` devuelve datos manuales con estructura estable.
- El payload debe incluir una marca de contexto provisional para indicar que los
datos pertenecen al HLL actual.
- La landing puede consumir el endpoint con fallback local si el backend no esta
disponible.
### Phase 2: backend adapter
- Sustituir el mock por un adaptador backend desacoplado de la fuente concreta.
- Mantener el mismo contrato principal de `items`.
- Introducir validacion basica de campos y fallback controlado si falla la
fuente.
### Phase 3: replacement toward HLL Vietnam
- Reemplazar o mezclar progresivamente el bloque cuando existan datos mas
representativos del contexto HLL Vietnam.
- Revisar naming, copy y campos para no arrastrar supuestos del juego actual.
## Explicitly Out Of Scope Now
- Integrar una fuente externa real.
- Hacer scraping.
- Consultar servidores reales desde el frontend.
- Anadir base de datos, cache o panel administrativo.
- Presentar el bloque como caracteristica definitiva del producto.
## Recommended Contract Shape
Ejemplo minimo de respuesta provisional:
```json
{
"status": "ok",
"data": {
"title": "Servidores actuales de Hell Let Loose",
"context": "current-hll-reference",
"source": "controlled-placeholder",
"items": [
{
"server_name": "HLL ESP Tactical Rotation",
"status": "online",
"players": 74,
"max_players": 100,
"current_map": "Sainte-Marie-du-Mont",
"region": "EU"
}
]
}
}
```
## Handoff To Following Tasks
- Backend task: preparar el adaptador placeholder estable sobre este contrato.
- Frontend task: anadir un panel visual sobrio con etiqueta provisional y
fallback seguro si el endpoint falla o no devuelve items.

View File

@@ -2,20 +2,84 @@
## Decision 001: frontend simple HTML/CSS/JS ## Decision 001: frontend simple HTML/CSS/JS
Se adopta una base estática con HTML, CSS y JavaScript puro para priorizar simplicidad, velocidad de arranque y compatibilidad total al abrir el frontend directamente en navegador. Se adopta una base estatica con HTML, CSS y JavaScript puro para priorizar simplicidad, velocidad de arranque y compatibilidad total al abrir el frontend directamente en navegador.
## Decision 002: backend previsto en Python ## Decision 002: backend previsto en Python
La estructura del repositorio reserva desde el inicio una carpeta de backend porque la implementación futura se realizará en Python. La estructura del repositorio reserva desde el inicio una carpeta de backend porque la implementacion futura se realizara en Python.
## Decision 003: estructura preparada para orquestación por agentes ## Decision 003: estructura preparada para orquestacion por agentes
Se incluye una carpeta `ai/` y un documento `AGENTS.md` para facilitar una futura organización del trabajo por roles, tareas y orquestación. Se incluye una carpeta `ai/` y un documento `AGENTS.md` para facilitar una futura organizacion del trabajo por roles, tareas y orquestacion.
## Decision 004: branding militar Vietnam ## Decision 004: branding militar Vietnam
La dirección visual inicial se alinea con una estética sobria, táctica y militar inspirada en el contexto Vietnam para mantener coherencia temática desde la primera iteración. La direccion visual inicial se alinea con una estetica sobria, tactica y militar inspirada en el contexto Vietnam para mantener coherencia tematica desde la primera iteracion.
## Decision 005: AI Development Platform integrada de forma adaptada ## Decision 005: AI Development Platform integrada de forma adaptada
Se integra una capa de orquestación por tasks inspirada en la plantilla de AI Development Platform, pero adaptada al contexto real de HLL Vietnam y sin arrastrar supuestos genéricos de otros stacks. La plataforma se usa como soporte operativo del repositorio, no como funcionalidad del producto. Se integra una capa de orquestacion por tasks inspirada en la plantilla de AI Development Platform, pero adaptada al contexto real de HLL Vietnam y sin arrastrar supuestos genericos de otros stacks. La plataforma se usa como soporte operativo del repositorio, no como funcionalidad del producto.
## Decision 006: contrato API pequeno antes de integraciones reales
Antes de implementar endpoints de comunidad o integraciones externas, se fija un contrato JSON minimo entre frontend y backend para evitar que la landing y el backend evolucionen con supuestos incompatibles.
La unica ruta implementada hoy es `GET /health`. Las rutas `/api/community`, `/api/trailer`, `/api/discord` y `/api/servers` quedan definidas como contrato previsto o placeholder en `docs/frontend-backend-contract.md`, manteniendo el backend en Python y sin introducir todavia Discord real, servidores reales ni base de datos.
## Decision 007: estrategia por fases para Discord y servidores
Los datos de Discord y de servidores de juego se incorporaran por fases para evitar dependencias prematuras de credenciales, APIs externas o consultas de red todavia no validadas.
La fase inicial debe usar datos manuales o placeholder controlados por el backend para mantener estable el contrato del frontend. Una fase intermedia podra anadir una integracion limitada con fuentes publicas o consultas tecnicas de bajo riesgo. Solo una fase posterior evaluara integraciones mas reales, siempre que queden claras las restricciones de seguridad, disponibilidad, latencia y mantenimiento.
La estrategia detallada de bloques de datos, fuentes posibles, riesgos y orden recomendado de implementacion queda documentada en `docs/discord-and-server-data-plan.md`.
## Decision 008: consumo frontend progresivo con fallback estatico
El frontend no debe depender de datos dinamicos para renderizar la landing base mientras el proyecto siga en fase fundacional.
Cuando se incorporen endpoints del backend, el consumo debe hacerse con `fetch` y JavaScript simple, priorizando bloques independientes y manteniendo contenido estatico o placeholders visuales si falla una llamada. `GET /health` queda reservado para comprobaciones tecnicas y no debe bloquear el render principal.
La estrategia detallada de prioridades de endpoints, estados de carga, errores y orden de migracion queda en `docs/frontend-data-consumption-plan.md`.
## Decision 009: servidores actuales de HLL como referencia provisional
Mientras no existan datos reales o representativos de HLL Vietnam, la web puede
mostrar un bloque provisional con servidores actuales de Hell Let Loose siempre
que quede claramente etiquetado como referencia temporal.
La primera version de ese bloque debe salir de un payload controlado del backend
Python, no de una integracion directa desde frontend ni de scraping prematuro.
Esto permite fijar campos utiles, preservar el tono del producto y evitar que la
landing dependa de una fuente externa aun no validada.
La estrategia de campos, riesgos, fases y sustitucion futura queda documentada
en `docs/current-hll-servers-source-plan.md`.
## Decision 010: ingesta por snapshots y adaptadores desacoplados
La evolucion desde payloads placeholder hacia datos mas realistas debe hacerse
con una arquitectura de snapshots de servidor, no conectando el frontend a una
fuente externa ni acoplando el backend a una integracion unica desde el inicio.
La unidad tecnica base sera un snapshot con `captured_at` y campos normalizados
como estado, jugadores, capacidad y mapa actual cuando exista. La lectura de
fuente, la normalizacion y la produccion del snapshot deben quedar separadas
para poder sustituir mocks por una fuente publica o consulta tecnica posterior
sin romper el contrato interno.
La estrategia detallada de fuentes, riesgos, fases y limites queda documentada
en `docs/current-hll-data-ingestion-plan.md`.
## Decision 011: modelo de almacenamiento logico antes de fijar tecnologia
Antes de introducir una base de datos concreta, el proyecto debe fijar un
modelo logico minimo para identidad de servidores y snapshots historicos.
La base inicial se apoya en entidades genericas como `game_sources`, `servers`
y `server_snapshots`. Las metricas iniciales deben derivarse primero de esos
snapshots en vez de materializar agregados prematuros. Esto mantiene el diseno
reutilizable para HLL actual y para futuras fuentes mas cercanas a HLL Vietnam.
El modelo base y las preguntas abiertas quedan documentados en
`docs/stats-database-schema-foundation.md`.

View File

@@ -0,0 +1,119 @@
# Discord And Server Data Plan
## Objective
Definir una base tecnica para exponer en la web datos de Discord y de futuros servidores de juego sin implementar todavia integraciones reales ni depender de servicios externos en esta fase.
## Discord Data Candidates
Bloques con sentido para la web:
- `invite_url`: enlace principal para entrar en la comunidad.
- `community_name`: nombre visible de la comunidad o del servidor.
- `cta_label`: texto de llamada a la accion para el boton de acceso.
- `approx_presence`: presencia aproximada o estado publico solo si existe una fuente publica fiable.
- `public_summary`: breve descripcion publica, reglas resumidas o mensaje de bienvenida.
## Game Server Data Candidates
Bloques con sentido para la web:
- `server_name`: nombre visible del servidor.
- `status`: online u offline.
- `current_map`: mapa actual si la fuente lo permite.
- `rotation`: rotacion o proximo mapa si la fuente es estable.
- `players`: jugadores conectados.
- `max_players`: capacidad maxima.
- `ping`: latencia aproximada si la consulta la devuelve.
- `region` o `notes`: metadatos operativos simples para la comunidad.
## Possible Discord Sources
### Public widget
- Util para obtener datos publicos basicos si el servidor lo tiene habilitado.
- Bueno para presencia aproximada o nombre visible.
- Limitado por la configuracion del propio servidor y por el alcance real del widget.
### External API or third-party integration
- Puede simplificar algunas lecturas, pero introduce dependencia de terceros, cambios de servicio y posibles limites de uso.
- Debe considerarse solo si aporta estabilidad y evita exponer credenciales en frontend.
### Own bot
- Da mas control a largo plazo.
- Exige credenciales, despliegue, permisos y operacion continua.
- No encaja en la fase actual del repositorio.
### Manual configured data
- Fuente mas segura para la primera fase.
- Sirve para `invite_url`, nombre de comunidad y textos publicos.
- Permite validar el contrato API y el consumo frontend sin depender de Discord real.
## Possible Game Server Sources
### Direct server queries
- Pueden dar estado, jugadores, mapa o ping segun el protocolo disponible.
- Exigen validar compatibilidad real con el juego, frecuencia de consulta y tolerancia a timeouts.
### External API
- Puede simplificar el acceso si existe una fuente especializada.
- Introduce dependencia externa, disponibilidad ajena y posible coste o rate limit.
### Mock or placeholder data
- Opcion recomendada para la primera fase.
- Permite fijar formato JSON, estados y experiencia de frontend sin acoplarse a infraestructura real.
### Manual updates
- Util para mostrar estado controlado o informacion operativa minima mientras no exista integracion tecnica fiable.
- Reduce riesgo en una etapa donde el backend aun es preparatorio.
## Risks And Restrictions
- Credenciales: bots o APIs privadas requieren secretos y una estrategia de almacenamiento segura.
- Rate limits: Discord o terceros pueden limitar frecuencia de consulta.
- Availability: widgets, APIs o consultas de servidor pueden fallar o cambiar sin previo aviso.
- Security: nunca debe exponerse en frontend una credencial ni una ruta administrativa.
- CORS: el frontend no deberia depender de llamadas directas a servicios externos si eso obliga a resolver CORS en cliente.
- Latency: consultas en tiempo real pueden degradar la web si no se amortiguan en backend.
- External dependency: cada integracion nueva aumenta coste operativo y puntos de fallo.
## Phased Strategy
### Phase 1: controlled placeholders
- Backend Python devuelve datos manuales o mock para `/api/discord` y `/api/servers`.
- La web usa esos datos solo cuando futuras tasks lo indiquen.
- No hay consultas reales a Discord ni a servidores.
### Phase 2: limited technical integration
- Evaluar una unica fuente publica o consulta sencilla por dominio.
- Mantener fallback manual si la fuente falla.
- Introducir observabilidad minima antes de ampliar alcance.
### Phase 3: real integration if justified
- Considerar bot propio, polling controlado o una integracion mas rica solo si aporta valor real a la comunidad.
- Revisar seguridad, operacion, cache y mantenimiento antes de consolidarlo.
## What Is Explicitly Out Of Scope Now
- Integrar Discord real.
- Consultar servidores reales de juego.
- Anadir base de datos.
- Implementar autenticacion o panel administrativo.
- Hacer llamadas directas desde el frontend a servicios externos.
## Recommended Implementation Order
1. Consolidar placeholders backend para `community`, `discord`, `trailer` y `servers`.
2. Definir consumo frontend con fallbacks visuales y orden de prioridad.
3. Validar una fuente publica o consulta tecnica pequena para Discord o servidores.
4. Decidir si merece la pena ampliar integraciones reales.

View File

@@ -0,0 +1,250 @@
# Frontend Backend Contract
## Objetivo
Definir un contrato inicial y pequeno entre la landing actual y el futuro backend Python sin implementar todavia integraciones reales ni comprometer detalles de infraestructura antes de tiempo.
## Estado actual
- Frontend: landing estatica sin consumo de API
- Backend: bootstrap Python con `GET /health`
- Integraciones reales: no implementadas
## Convenciones generales
- Todas las respuestas usan JSON.
- Los nombres de campos usan `snake_case`.
- `status` es obligatorio en todas las respuestas.
- Las respuestas exitosas usan `status: "ok"`.
- Las respuestas de error usan `status: "error"` y un campo `message`.
- Cuando un endpoint sea solo placeholder o aun no tenga datos reales, puede responder datos controlados o quedar documentado como previsto hasta una task posterior.
## Estructura base de respuesta
Respuesta correcta:
```json
{
"status": "ok",
"data": {}
}
```
Respuesta de error minima:
```json
{
"status": "error",
"message": "Route not found"
}
```
## Endpoints
### `GET /health`
- Proposito: comprobar que el backend bootstrap esta levantado.
- Metodo HTTP: `GET`
- Ruta: `/health`
- Estado actual: implementado
Ejemplo JSON:
```json
{
"status": "ok",
"service": "hll-vietnam-backend",
"phase": "bootstrap"
}
```
### `GET /api/community`
- Proposito: devolver contenido resumido de presentacion de la comunidad para bloques de texto o estadisticas futuras.
- Metodo HTTP: `GET`
- Ruta: `/api/community`
- Estado actual: previsto
Ejemplo JSON:
```json
{
"status": "ok",
"data": {
"title": "Comunidad Hispana HLL Vietnam",
"summary": "Punto de encuentro para jugadores, escuadras y comunidad.",
"discord_invite_url": "https://discord.com/invite/PedEqZ2Xsa"
}
}
```
### `GET /api/trailer`
- Proposito: exponer la informacion del trailer que hoy esta fija en la landing.
- Metodo HTTP: `GET`
- Ruta: `/api/trailer`
- Estado actual: previsto
Ejemplo JSON:
```json
{
"status": "ok",
"data": {
"video_url": "https://www.youtube.com/embed/JzYzYNVWZ_A",
"title": "Trailer HLL Vietnam",
"provider": "youtube"
}
}
```
### `GET /api/discord`
- Proposito: centralizar la informacion publica del acceso a Discord sin integrar todavia datos reales del servidor.
- Metodo HTTP: `GET`
- Ruta: `/api/discord`
- Estado actual: placeholder
Ejemplo JSON:
```json
{
"status": "ok",
"data": {
"invite_url": "https://discord.com/invite/PedEqZ2Xsa",
"label": "Unirse al Discord",
"availability": "manual"
}
}
```
### `GET /api/servers`
- Proposito: exponer un bloque provisional de servidores actuales de Hell Let Loose como referencia temporal para la comunidad.
- Metodo HTTP: `GET`
- Ruta: `/api/servers`
- Estado actual: placeholder implementado
Ejemplo JSON:
```json
{
"status": "ok",
"data": {
"title": "Servidores actuales de Hell Let Loose",
"context": "current-hll-reference",
"source": "controlled-placeholder",
"items": [
{
"server_name": "HLL ESP Tactical Rotation",
"status": "online",
"players": 74,
"max_players": 100,
"current_map": "Sainte-Marie-du-Mont",
"region": "EU"
}
]
}
}
```
Notas del placeholder actual:
- El contenido representa servidores actuales de Hell Let Loose, no servidores de HLL Vietnam.
- `context` permite al frontend etiquetar el bloque como referencia provisional.
- `source` indica que la respuesta actual sale de datos controlados del backend.
### `GET /api/servers/latest`
- Proposito: devolver el ultimo snapshot conocido por servidor desde la persistencia local.
- Metodo HTTP: `GET`
- Ruta: `/api/servers/latest`
- Estado actual: implementado para validacion tecnica
Ejemplo JSON:
```json
{
"status": "ok",
"data": {
"title": "Ultimo estado conocido de servidores",
"context": "current-hll-history",
"source": "local-snapshot-storage",
"items": [
{
"server_id": 1,
"external_server_id": "hll-esp-tactical-rotation",
"server_name": "HLL ESP Tactical Rotation",
"region": "EU",
"captured_at": "2026-03-20T08:45:20.802006Z",
"status": "online",
"players": 74,
"max_players": 100,
"current_map": "Sainte-Marie-du-Mont"
}
]
}
}
```
### `GET /api/servers/history`
- Proposito: devolver una ventana simple de snapshots recientes desde la persistencia local.
- Metodo HTTP: `GET`
- Ruta: `/api/servers/history`
- Parametros opcionales: `limit` entre `1` y `100`
- Estado actual: implementado para validacion tecnica
Ejemplo JSON:
```json
{
"status": "ok",
"data": {
"title": "Historial reciente de servidores",
"context": "current-hll-history",
"source": "local-snapshot-storage",
"limit": 20,
"items": []
}
}
```
### `GET /api/servers/{id}/history`
- Proposito: devolver una historia basica de snapshots para un servidor concreto.
- Metodo HTTP: `GET`
- Ruta: `/api/servers/{id}/history`
- Parametros opcionales: `limit` entre `1` y `100`
- Identificadores aceptados: `server_id` numerico interno o `external_server_id`
- Estado actual: implementado para validacion tecnica
Ejemplo JSON:
```json
{
"status": "ok",
"data": {
"title": "Historial por servidor",
"context": "current-hll-history",
"source": "local-snapshot-storage",
"server_id": "hll-esp-tactical-rotation",
"limit": 20,
"items": []
}
}
```
## Consumo previsto desde frontend
- El frontend deberia llamar primero a `GET /health` solo para comprobaciones tecnicas o entornos de desarrollo, no para condicionar el render basico de la landing.
- Los endpoints de contenido (`/api/community`, `/api/trailer`, `/api/discord`, `/api/servers`) deberian consumirse con `fetch`.
- Si una llamada falla, la landing debe conservar un fallback estatico mientras exista contenido fijo en `index.html`.
- La futura migracion debe reemplazar valores hardcoded de forma incremental, endpoint por endpoint.
## Notas de alcance
- Este contrato no introduce autenticacion.
- Este contrato no define base de datos.
- Este contrato no integra Discord ni servidores reales.
- La implementacion de estos endpoints queda para tasks posteriores.

View File

@@ -0,0 +1,73 @@
# Frontend Data Consumption Plan
## Objective
Definir como evolucionara la landing de HLL Vietnam desde contenido estatico hacia bloques alimentados por el backend sin romper simplicidad, branding ni compatibilidad al abrir `frontend/index.html` directamente.
## Current Frontend Blocks With Future Dynamic Potential
- Hero principal: titulo, resumen y CTA de Discord podran leer `community` y `discord`.
- Bloque de trailer: podra leer `trailer` para desacoplar video y titulo del HTML.
- Estado de servidores: queda reservado para una futura seccion y no debe forzarse en la landing actual.
## Recommended Consumption Strategy
- Usar `fetch` nativo cuando una task habilite consumo real.
- Mantener JavaScript simple en `frontend/assets/js/main.js` o dividir en modulos ligeros solo si el numero de bloques dinamicos ya lo justifica.
- Centralizar la URL base del backend en una configuracion minima si el frontend deja de ser puramente estatico en un entorno concreto.
- No llamar a servicios externos desde el navegador; el frontend debe hablar con el backend Python.
## UI State Rules
### Loading
- No bloquear el render inicial de la landing.
- Mostrar skeletons o placeholders ligeros solo en bloques futuros que ya dependan del backend.
### Error
- Si falla una llamada, conservar el contenido estatico existente o un mensaje tactico breve y no intrusivo.
- Registrar el error en consola durante desarrollo sin degradar toda la pagina.
### Empty state
- Si `servers.items` llega vacio, mostrar un estado neutral de "informacion disponible mas adelante".
- Si un bloque opcional no tiene datos, ocultarlo o dejar un placeholder discreto en lugar de mostrar errores tecnicos.
### Fallback
- Mantener el Discord CTA hardcoded hasta que `/api/discord` sea estable.
- Mantener el iframe del trailer fijo hasta validar `/api/trailer`.
- No hacer depender el hero de `/health`.
## Endpoint Priority
1. `/api/community`
2. `/api/trailer`
3. `/api/discord`
4. `/api/servers`
5. `/health` solo para checks tecnicos o diagnostico en desarrollo
## Progressive Migration Path
### Step 1
- Introducir una capa minima de lectura para `community` y `trailer`.
- Reutilizar el HTML actual como fallback.
### Step 2
- Sustituir el CTA de Discord por datos de `/api/discord` cuando el placeholder backend sea estable.
- Mantener la URL actual como respaldo local.
### Step 3
- Anadir una seccion de servidores solo cuando exista diseno, contrato y placeholder suficientemente claros.
- Evitar reservar complejidad en la landing antes de que ese bloque aporte valor real.
## Explicitly Out Of Scope Now
- Implementar `fetch` real.
- Cambiar el comportamiento visible de la landing.
- Introducir librerias de estado o frameworks frontend.
- Conectar el navegador directamente con Discord o con APIs de servidores.

View File

@@ -2,7 +2,7 @@
## Vision del proyecto ## Vision del proyecto
HLL Vietnam busca convertirse en la base de una web de comunidad para centralizar la presencia digital de una comunidad hispana alrededor del juego, con una identidad visual sobria, táctica y coherente con el universo Vietnam. HLL Vietnam busca convertirse en la base de una web de comunidad para centralizar la presencia digital de una comunidad hispana alrededor del juego, con una identidad visual sobria, tactica y coherente con el universo Vietnam.
## Objetivo inicial ## Objetivo inicial
@@ -11,10 +11,10 @@ Publicar una landing simple que permita presentar la comunidad, mostrar el trail
## Alcance actual ## Alcance actual
- Estructura inicial del repositorio. - Estructura inicial del repositorio.
- Landing estática en HTML, CSS y JavaScript. - Landing estatica en HTML, CSS y JavaScript.
- Documentación base para organizar el crecimiento del proyecto. - Documentacion base para organizar el crecimiento del proyecto.
- Preparación de carpetas para backend y orquestación futura. - Preparacion de carpetas para backend y orquestacion futura.
- Plataforma de tasks y orquestación integrada para coordinar trabajo técnico. - Plataforma de tasks y orquestacion integrada para coordinar trabajo tecnico.
## Stack actual ## Stack actual
@@ -26,5 +26,17 @@ Publicar una landing simple que permita presentar la comunidad, mostrar el trail
## Stack futuro previsto ## Stack futuro previsto
- Backend principal en Python - Backend principal en Python
- Integraciones de comunidad y automatización - Integraciones de comunidad y automatizacion
- Posible ampliación de paneles administrativos y servicios internos - Posible ampliacion de paneles administrativos y servicios internos
## Contrato inicial frontend backend
El repositorio define un contrato API inicial en `docs/frontend-backend-contract.md` para alinear la futura comunicacion entre la landing y el backend Python.
En esta fase solo existe `GET /health` como endpoint implementado. Las rutas de comunidad, trailer, Discord y servidores quedan documentadas como contrato previsto para futuras tasks sin cambiar todavia el comportamiento visible del frontend.
## Evolucion prevista del frontend
La landing debe seguir siendo funcional al abrirse directamente en navegador mientras los datos dinamicos se introducen de forma incremental. La estrategia de consumo prevista usa `fetch` y JavaScript simple cuando una task lo requiera, siempre conservando fallbacks estaticos mientras se valida cada endpoint.
La planificacion detallada de prioridades de consumo, estados de carga, errores y placeholders queda en `docs/frontend-data-consumption-plan.md`.

View File

@@ -3,29 +3,33 @@
## Fase 1: base del repo ## Fase 1: base del repo
- Crear estructura inicial profesional. - Crear estructura inicial profesional.
- Definir documentación base del proyecto. - Definir documentacion base del proyecto.
- Publicar la primera landing estática. - Publicar la primera landing estatica.
## Fase 2: landing mejorada ## Fase 2: landing mejorada
- Incorporar branding definitivo y recursos visuales. - Incorporar branding definitivo y recursos visuales.
- Añadir más secciones informativas de comunidad. - Anadir mas secciones informativas de comunidad.
- Mejorar experiencia responsive y contenido. - Mejorar experiencia responsive y contenido.
## Fase 3: backend Python ## Fase 3: backend Python
- Definir arquitectura del backend. - Definir arquitectura del backend.
- Incorporar servicios base en Python. - Incorporar servicios base en Python.
- Preparar configuración, entornos y despliegue inicial. - Preparar configuracion, entornos y despliegue inicial.
## Fase 4: integración de datos de Discord/servidores ## Fase 4: integracion de datos de Discord/servidores
- Estudiar integraciones viables con Discord. - Documentar el plan tecnico de datos para Discord y servidores antes de integrar fuentes reales.
- Incorporar datos de comunidad o estado de servicios. - Empezar por placeholders o datos manuales controlados desde el backend Python.
- Añadir automatizaciones controladas y trazables. - Incorporar integraciones limitadas y trazables solo despues de validar fuentes, limites y seguridad.
- Diferenciar de forma explicita los servidores actuales de Hell Let Loose frente al futuro contexto HLL Vietnam.
- Sustituir el bloque provisional de servidores actuales cuando existan datos mas cercanos al producto final.
- Definir snapshots de servidores como unidad base para historicos y estadisticas basicas antes de persistir datos reales.
- Separar por fases la ingesta, la normalizacion y la futura explotacion historica para no acoplar el frontend a fuentes externas.
## Fase 5: panel/admin y automatización ## Fase 5: panel/admin y automatizacion
- Construir panel interno o administrativo. - Construir panel interno o administrativo.
- Añadir flujos de gestión y publicación. - Anadir flujos de gestion y publicacion.
- Integrar sistema de tareas y orquestación del proyecto. - Ampliar y madurar el sistema de tasks y orquestacion ya integrado en el repositorio.

View File

@@ -0,0 +1,151 @@
# Stats Database Schema Foundation
## Objective
Definir una base de almacenamiento simple y reutilizable para snapshots de
servidores y estadisticas iniciales, sin comprometer todavia una base de datos
productiva concreta.
## Design Principles
- naming generico reutilizable para HLL actual y futuro HLL Vietnam
- separacion entre identidad de servidor y observaciones historicas
- persistir primero solo lo necesario para reconstruir actividad basica
- dejar espacio para multiples fuentes sin acoplar el modelo a una integracion
unica
## Proposed Core Entities
### `game_sources`
Proposito:
describir el contexto del juego o dominio de origen de los datos.
Campos principales:
- `id`
- `slug`
- `display_name`
- `provider_kind`
- `is_active`
- `created_at`
- `updated_at`
Notas:
- `slug` puede tomar valores como `current-hll` y en el futuro otros contextos
mas cercanos a HLL Vietnam.
- Esta entidad evita incrustar el juego en cada nombre de tabla.
### `servers`
Proposito:
mantener la identidad estable de cada servidor observado.
Campos principales:
- `id`
- `game_source_id`
- `external_server_id` nullable
- `server_name`
- `region` nullable
- `first_seen_at`
- `last_seen_at`
- `created_at`
- `updated_at`
Claves y relaciones:
- primary key en `id`
- foreign key a `game_sources.id`
- unique recomendado sobre `game_source_id` + `external_server_id` cuando el
origen entregue identificador externo fiable
Notas:
- `server_name` no debe usarse como clave unica porque puede cambiar.
- `last_seen_at` resume la ultima observacion conocida sin sustituir a los
snapshots historicos.
### `server_snapshots`
Proposito:
registrar cada captura puntual normalizada de un servidor.
Campos principales:
- `id`
- `server_id`
- `captured_at`
- `status`
- `players`
- `max_players`
- `current_map` nullable
- `source_name`
- `raw_payload_ref` nullable
- `created_at`
Claves y relaciones:
- primary key en `id`
- foreign key a `servers.id`
- index recomendado sobre `server_id` + `captured_at`
Notas:
- `status`, `players`, `max_players` y `current_map` son la base a persistir
desde la primera fase.
- `raw_payload_ref` queda como referencia opcional para trazabilidad futura si
el backend decide guardar artefactos crudos fuera de esta tabla.
## Initial Statistics Layer
No es necesario persistir metricas complejas desde el inicio. La primera capa
de estadisticas puede documentarse como derivada de `server_snapshots`.
Vistas o agregaciones recomendadas para una siguiente fase:
- ultima observacion por servidor
- pico de jugadores por servidor en una ventana temporal
- numero de snapshots online por servidor
- ultima vez visto online
Si mas adelante aparecen necesidades de rendimiento o cuadros de mando
persistentes, podra anadirse una tabla de agregados sin cambiar la base del
modelo.
## What To Persist First
Persistir por snapshot:
- `server_id`
- `captured_at`
- `status`
- `players`
- `max_players`
- `current_map` cuando exista
- `source_name`
Puede derivarse despues:
- tendencias
- medias por periodo
- picos historicos
- porcentaje de disponibilidad
- rankings
## Technology Position
El repositorio todavia no fija una tecnologia de persistencia productiva. La
base del esquema debe entenderse como modelo logico compatible con el backend en
Python y trasladable despues a la opcion de almacenamiento que se valide en una
task especifica.
En esta fase no se anaden migraciones, ORM ni ficheros de base de datos.
## Open Questions For Future Tasks
- que fuente aportara un identificador externo suficientemente estable
- con que frecuencia debe capturarse un snapshot
- si conviene guardar payload crudo completo o solo referencias
- cuando merece la pena materializar agregados persistentes

View File

@@ -1,14 +1,22 @@
:root { :root {
--bg: #0f120d; --bg: #0f120d;
--bg-deep: #090b08;
--bg-elevated: rgba(27, 33, 24, 0.92); --bg-elevated: rgba(27, 33, 24, 0.92);
--panel: rgba(21, 26, 19, 0.94); --panel: rgba(21, 26, 19, 0.94);
--panel-soft: rgba(30, 36, 27, 0.7);
--border: rgba(159, 168, 141, 0.24); --border: rgba(159, 168, 141, 0.24);
--border-strong: rgba(183, 201, 125, 0.2);
--text: #e7e0cf; --text: #e7e0cf;
--muted: #a9ad9a; --muted: #a9ad9a;
--text-soft: #c8ccb8;
--accent: #8ea062; --accent: #8ea062;
--accent-strong: #b7c97d; --accent-strong: #b7c97d;
--shadow: 0 24px 60px rgba(0, 0, 0, 0.35); --accent-warm: #d2b676;
--max-width: 1120px; --shadow: 0 28px 72px rgba(0, 0, 0, 0.42);
--shadow-soft: 0 18px 40px rgba(0, 0, 0, 0.24);
--hero-shell-width: 1240px;
--video-shell-width: 1080px;
--servers-shell-width: 1380px;
--font-main: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; --font-main: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
} }
@@ -25,10 +33,13 @@ body {
min-height: 100vh; min-height: 100vh;
font-family: var(--font-main); font-family: var(--font-main);
color: var(--text); color: var(--text);
background-color: var(--bg);
background: background:
linear-gradient(rgba(9, 11, 8, 0.68), rgba(9, 11, 8, 0.92)), linear-gradient(rgba(9, 11, 8, 0.62), rgba(9, 11, 8, 0.96)),
radial-gradient(circle at top, rgba(84, 96, 59, 0.22), transparent 36%), radial-gradient(circle at top, rgba(84, 96, 59, 0.2), transparent 34%),
linear-gradient(135deg, #161c14 0%, #0c100b 100%); radial-gradient(circle at 85% 10%, rgba(210, 182, 118, 0.08), transparent 24%),
radial-gradient(circle at bottom, rgba(210, 182, 118, 0.07), transparent 28%),
linear-gradient(145deg, #171d15 0%, var(--bg-deep) 100%);
} }
img { img {
@@ -42,18 +53,20 @@ a {
} }
.page-shell { .page-shell {
width: min(100%, var(--max-width)); width: 100%;
margin: 0 auto; padding: 34px 20px 72px;
padding: 32px 20px 56px;
} }
.hero { .hero {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
width: min(100%, var(--hero-shell-width));
margin: 0 auto;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 24px; border-radius: 24px;
background: background:
linear-gradient(180deg, rgba(23, 29, 19, 0.86), rgba(13, 16, 12, 0.95)), linear-gradient(180deg, rgba(24, 31, 20, 0.84), rgba(11, 14, 10, 0.97)),
radial-gradient(circle at top center, rgba(183, 201, 125, 0.06), transparent 32%),
repeating-linear-gradient( repeating-linear-gradient(
90deg, 90deg,
rgba(255, 255, 255, 0.015) 0, rgba(255, 255, 255, 0.015) 0,
@@ -64,50 +77,132 @@ a {
box-shadow: var(--shadow); box-shadow: var(--shadow);
} }
.hero::before {
content: "";
position: absolute;
inset: 18px;
border: 1px solid rgba(210, 182, 118, 0.1);
border-radius: 18px;
pointer-events: none;
}
.hero::after {
content: "";
position: absolute;
inset: auto 32px 0;
height: 1px;
background: linear-gradient(
90deg,
transparent,
rgba(183, 201, 125, 0.2),
transparent
);
pointer-events: none;
}
.hero__overlay { .hero__overlay {
position: absolute; position: absolute;
inset: 0; inset: 0;
background: background:
radial-gradient(circle at top right, rgba(142, 160, 98, 0.14), transparent 28%), radial-gradient(circle at top right, rgba(142, 160, 98, 0.16), transparent 28%),
linear-gradient(135deg, transparent 0%, rgba(0, 0, 0, 0.2) 100%); radial-gradient(circle at left center, rgba(210, 182, 118, 0.08), transparent 26%),
linear-gradient(135deg, transparent 0%, rgba(0, 0, 0, 0.2) 100%),
linear-gradient(180deg, rgba(0, 0, 0, 0) 55%, rgba(0, 0, 0, 0.28) 100%);
pointer-events: none; pointer-events: none;
} }
.hero__content { .hero__content {
position: relative; position: relative;
z-index: 1; z-index: 1;
padding: 56px 28px; max-width: 860px;
margin: 0 auto;
padding: 68px 32px 78px;
display: grid; display: grid;
justify-items: center; justify-items: center;
text-align: center; text-align: center;
gap: 18px; gap: 18px;
} }
.hero__content::before {
content: "";
position: absolute;
inset: 28px 80px auto;
height: 140px;
border-radius: 999px;
background: radial-gradient(
circle,
rgba(210, 182, 118, 0.16) 0%,
rgba(142, 160, 98, 0.08) 42%,
transparent 78%
);
filter: blur(14px);
pointer-events: none;
}
.logo-frame { .logo-frame {
width: min(240px, 72vw); width: min(336px, 78vw);
aspect-ratio: 1 / 1; min-height: 220px;
padding: 16px; padding: 20px 24px;
display: grid; display: grid;
place-items: center; place-items: center;
border: 1px dashed rgba(183, 201, 125, 0.45); border: 1px dashed rgba(183, 201, 125, 0.45);
border-radius: 18px; border-radius: 18px;
background: rgba(12, 16, 10, 0.58); background:
linear-gradient(180deg, rgba(19, 24, 16, 0.9), rgba(10, 13, 9, 0.72));
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.04),
0 18px 40px rgba(0, 0, 0, 0.24);
position: relative;
isolation: isolate;
}
.logo-frame::before {
content: "";
position: absolute;
inset: 10px;
border: 1px solid rgba(210, 182, 118, 0.14);
border-radius: 12px;
pointer-events: none;
}
.logo-frame::after {
content: "";
position: absolute;
inset: auto 18% -24px;
height: 48px;
background: radial-gradient(circle, rgba(0, 0, 0, 0.42), transparent 72%);
filter: blur(8px);
z-index: -1;
pointer-events: none;
} }
.logo-frame__image { .logo-frame__image {
width: 100%; width: auto;
height: 100%; height: auto;
max-width: 100%;
max-height: 220px;
object-fit: contain; object-fit: contain;
filter:
drop-shadow(0 10px 20px rgba(0, 0, 0, 0.28))
drop-shadow(0 0 18px rgba(210, 182, 118, 0.08));
} }
.eyebrow { .eyebrow {
margin: 0; margin: 0;
font-size: 0.8rem; padding: 0.35rem 0.75rem;
letter-spacing: 0.22em; border: 1px solid rgba(183, 201, 125, 0.22);
border-radius: 999px;
background: rgba(142, 160, 98, 0.08);
font-size: 0.76rem;
letter-spacing: 0.2em;
text-transform: uppercase; text-transform: uppercase;
color: var(--accent-strong); color: var(--accent-strong);
} }
.eyebrow--section {
margin-bottom: 0.85rem;
}
h1, h1,
h2 { h2 {
margin: 0; margin: 0;
@@ -117,18 +212,56 @@ h2 {
h1 { h1 {
max-width: 12ch; max-width: 12ch;
font-size: clamp(2.2rem, 5vw, 4.6rem); font-size: clamp(2.2rem, 5vw, 4.6rem);
letter-spacing: 0.02em;
text-shadow: 0 10px 28px rgba(0, 0, 0, 0.32);
} }
h2 { h2 {
font-size: clamp(1.5rem, 2.8vw, 2.2rem); font-size: clamp(1.5rem, 2.8vw, 2.2rem);
} }
.hero__title-accent {
display: block;
color: var(--accent-warm);
}
.hero__text { .hero__text {
max-width: 640px; max-width: 640px;
margin: 0; margin: 0;
color: var(--text-soft);
font-size: 1.06rem;
line-height: 1.85;
}
.hero__actions {
display: grid;
justify-items: center;
gap: 14px;
margin-top: 4px;
}
.status-chip {
margin: 0;
padding: 0.45rem 0.85rem;
border: 1px solid rgba(159, 168, 141, 0.22);
border-radius: 999px;
background: rgba(21, 26, 19, 0.72);
font-size: 0.76rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted); color: var(--muted);
font-size: 1.05rem; }
line-height: 1.7;
.status-chip--ok {
border-color: rgba(183, 201, 125, 0.34);
background: rgba(142, 160, 98, 0.12);
color: var(--accent-strong);
}
.status-chip--fallback {
border-color: rgba(210, 182, 118, 0.24);
background: rgba(210, 182, 118, 0.08);
color: var(--accent-warm);
} }
.discord-button { .discord-button {
@@ -136,13 +269,15 @@ h2 {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 52px; min-height: 52px;
padding: 0 24px; min-width: 220px;
padding: 0 28px;
border: 1px solid rgba(183, 201, 125, 0.45); border: 1px solid rgba(183, 201, 125, 0.45);
border-radius: 999px; border-radius: 999px;
background: linear-gradient(180deg, #8ea062 0%, #6e7f48 100%); background: linear-gradient(180deg, #8ea062 0%, #6e7f48 100%);
color: #11150f; color: #11150f;
font-weight: 700; font-weight: 700;
letter-spacing: 0.04em; letter-spacing: 0.08em;
text-transform: uppercase;
transition: transition:
transform 160ms ease, transform 160ms ease,
box-shadow 160ms ease, box-shadow 160ms ease,
@@ -158,35 +293,610 @@ h2 {
} }
.content { .content {
margin-top: 28px; width: 100%;
margin-top: -34px;
position: relative;
z-index: 2;
display: grid;
gap: 24px;
} }
.panel { .panel {
padding: 28px; position: relative;
width: 100%;
padding: 32px;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 24px; border-radius: 24px;
background: var(--panel); background:
linear-gradient(180deg, rgba(23, 28, 21, 0.96), rgba(13, 16, 12, 0.99));
box-shadow: var(--shadow); box-shadow: var(--shadow);
} }
.panel::before {
content: "";
position: absolute;
inset: 0 auto auto 32px;
width: 120px;
height: 1px;
background: linear-gradient(90deg, rgba(210, 182, 118, 0.7), transparent);
}
.panel__header { .panel__header {
margin-bottom: 18px; margin-bottom: 22px;
}
.panel__header--servers {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.panel__intro {
margin: 0 0 20px;
max-width: 70ch;
color: var(--text-soft);
line-height: 1.75;
}
.panel__intro--tight {
margin-bottom: 0;
max-width: 58ch;
}
.servers-source {
margin-bottom: 20px;
padding: 14px 16px;
border: 1px solid rgba(159, 168, 141, 0.18);
border-radius: 16px;
background: rgba(10, 12, 9, 0.26);
}
.servers-source[data-state="live"] {
border-color: rgba(183, 201, 125, 0.3);
background: rgba(142, 160, 98, 0.08);
}
.servers-source[data-state="historical"] {
border-color: rgba(210, 182, 118, 0.24);
background: rgba(210, 182, 118, 0.06);
}
.servers-source__label,
.servers-source__meta {
margin: 0;
}
.servers-source__label {
color: var(--text);
font-size: 0.82rem;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.servers-source__meta {
margin-top: 8px;
color: var(--text-soft);
line-height: 1.6;
}
.stats-preview {
margin-bottom: 20px;
padding: 16px 18px;
border: 1px solid rgba(159, 168, 141, 0.18);
border-radius: 18px;
background:
linear-gradient(180deg, rgba(28, 34, 25, 0.92), rgba(14, 18, 13, 0.95));
}
.stats-preview__eyebrow {
margin: 0 0 14px;
color: var(--accent-warm);
font-size: 0.74rem;
letter-spacing: 0.16em;
text-transform: uppercase;
}
.stats-preview__items {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.stats-preview__item {
padding: 14px;
border: 1px solid rgba(159, 168, 141, 0.14);
border-radius: 14px;
background: rgba(10, 12, 9, 0.34);
}
.stats-preview__item p {
margin: 0 0 8px;
color: var(--muted);
font-size: 0.75rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.stats-preview__item strong {
display: block;
font-size: 1rem;
color: var(--text);
}
.panel--video {
position: relative;
max-width: var(--video-shell-width);
margin: 0 auto;
backdrop-filter: blur(4px);
}
.panel--servers {
max-width: var(--servers-shell-width);
margin: 0 auto;
padding-inline: 38px;
} }
.video-wrapper { .video-wrapper {
position: relative; position: relative;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
border: 1px solid rgba(159, 168, 141, 0.18); padding: 12px;
border-radius: 18px; border: 1px solid rgba(159, 168, 141, 0.2);
background: #0a0c09; border-radius: 20px;
background:
linear-gradient(180deg, rgba(29, 35, 26, 0.95), rgba(10, 12, 9, 0.98));
aspect-ratio: 16 / 9; aspect-ratio: 16 / 9;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.04),
0 24px 44px rgba(0, 0, 0, 0.3);
}
.video-wrapper::before {
content: "";
position: absolute;
inset: 0;
border-radius: 20px;
border: 1px solid rgba(210, 182, 118, 0.12);
pointer-events: none;
}
.video-wrapper::after {
content: "";
position: absolute;
inset: 12px auto auto 12px;
width: 132px;
height: 28px;
border-radius: 999px;
background:
linear-gradient(90deg, rgba(12, 16, 10, 0.88), rgba(12, 16, 10, 0.24));
pointer-events: none;
} }
.video-wrapper iframe { .video-wrapper iframe {
width: 100%; width: 100%;
height: 100%; height: 100%;
border: 0; border: 0;
border-radius: 10px;
}
.servers-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
gap: 20px;
}
.servers-grid--section {
grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr));
align-items: stretch;
}
.server-panel-section + .server-panel-section {
margin-top: 22px;
}
.server-panel-section__header {
margin-bottom: 14px;
padding: 0 2px;
}
.server-panel-section__header h3,
.server-panel-section__header p {
margin: 0;
}
.server-panel-section__header h3 {
font-size: 1rem;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.server-panel-section__header p {
margin-top: 8px;
color: var(--text-soft);
line-height: 1.6;
max-width: 62ch;
}
.server-panel-section--real .server-panel-section__header h3 {
color: var(--accent-strong);
}
.server-panel-section--reference .server-panel-section__header h3 {
color: var(--accent-warm);
}
.server-card {
min-width: 0;
padding: 20px;
border: 1px solid rgba(159, 168, 141, 0.18);
border-radius: 20px;
background:
linear-gradient(180deg, rgba(28, 34, 25, 0.94), rgba(15, 18, 13, 0.98));
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.03),
var(--shadow-soft);
}
.server-card--real {
border-color: rgba(183, 201, 125, 0.24);
background:
linear-gradient(180deg, rgba(32, 40, 28, 0.96), rgba(14, 18, 13, 0.98));
}
.server-card--reference {
border-color: rgba(210, 182, 118, 0.18);
background:
linear-gradient(180deg, rgba(31, 28, 22, 0.94), rgba(16, 14, 11, 0.98));
}
.server-card__top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
}
.server-card__top--stats {
align-items: stretch;
gap: 18px;
}
.server-card__identity {
min-width: 0;
display: grid;
gap: 6px;
}
.server-card__eyebrow {
margin: 0;
color: var(--accent-warm);
font-size: 0.72rem;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.server-card h3 {
margin: 0;
font-size: 1.08rem;
line-height: 1.4;
max-width: none;
overflow-wrap: anywhere;
}
.server-card__meta {
margin: 0 0 14px;
color: var(--muted);
font-size: 0.8rem;
letter-spacing: 0.04em;
line-height: 1.6;
}
.server-card__status-column {
display: grid;
align-content: start;
justify-items: end;
gap: 10px;
min-width: 150px;
}
.server-card__population {
margin: 0;
padding: 10px 12px;
max-width: 100%;
min-width: 120px;
border: 1px solid rgba(159, 168, 141, 0.16);
border-radius: 14px;
background: rgba(10, 12, 9, 0.3);
color: var(--accent-strong);
font-size: 1rem;
font-weight: 700;
letter-spacing: 0.04em;
text-align: center;
}
.server-card__actions {
margin: 0;
width: 100%;
display: flex;
justify-content: flex-end;
}
.server-connect-button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
max-width: 100%;
min-height: 42px;
min-width: 136px;
padding: 0 16px;
border: 1px solid rgba(183, 201, 125, 0.38);
border-radius: 999px;
background: linear-gradient(180deg, rgba(183, 201, 125, 0.18), rgba(110, 127, 72, 0.24));
color: var(--accent-strong);
font-size: 0.76rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
transition:
transform 160ms ease,
border-color 160ms ease,
background 160ms ease;
}
.server-connect-button:hover,
.server-connect-button:focus-visible {
transform: translateY(-1px);
border-color: rgba(210, 182, 118, 0.52);
background: linear-gradient(180deg, rgba(210, 182, 118, 0.2), rgba(142, 160, 98, 0.26));
}
.server-state {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 88px;
padding: 0.35rem 0.65rem;
border: 1px solid rgba(159, 168, 141, 0.22);
border-radius: 999px;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.server-state--online {
border-color: rgba(183, 201, 125, 0.34);
background: rgba(142, 160, 98, 0.12);
color: var(--accent-strong);
}
.server-state--offline {
border-color: rgba(210, 182, 118, 0.24);
background: rgba(210, 182, 118, 0.08);
color: var(--accent-warm);
}
.server-card__load {
width: 100%;
height: 8px;
margin-top: 16px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.05);
overflow: hidden;
}
.server-card__load span {
display: block;
height: 100%;
border-radius: inherit;
background:
linear-gradient(90deg, rgba(210, 182, 118, 0.78), rgba(142, 160, 98, 0.92));
box-shadow: 0 0 18px rgba(142, 160, 98, 0.22);
}
.server-card__stats {
margin: 0;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
}
.server-stat {
padding-top: 14px;
border-top: 1px solid rgba(159, 168, 141, 0.12);
}
.server-stat--players dd {
color: var(--accent-strong);
}
.server-card__stats dt {
margin: 0 0 6px;
color: var(--muted);
font-size: 0.75rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.server-card__stats dd {
margin: 0;
font-size: 0.95rem;
line-height: 1.4;
}
.server-card__body {
min-width: 0;
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(220px, 0.85fr);
gap: 16px;
align-items: start;
}
.server-card__facts {
min-width: 0;
display: grid;
gap: 14px;
}
.server-card__quickfacts {
margin-bottom: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
}
.server-card__quickfact {
min-width: 0;
padding: 12px 14px;
border: 1px solid rgba(159, 168, 141, 0.12);
border-radius: 14px;
background: rgba(10, 12, 9, 0.22);
}
.server-card__quickfact p,
.server-card__quickfact strong {
margin: 0;
}
.server-card__quickfact p {
margin-bottom: 6px;
color: var(--muted);
font-size: 0.72rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.server-card__quickfact strong {
display: block;
color: var(--text);
font-size: 0.92rem;
line-height: 1.45;
overflow-wrap: anywhere;
}
.server-summary {
margin: 0;
padding: 16px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
border: 1px solid rgba(159, 168, 141, 0.12);
border-radius: 16px;
background: rgba(10, 12, 9, 0.26);
}
.server-summary__item {
min-width: 0;
}
.server-summary__item dt,
.server-summary__item dd {
margin: 0;
}
.server-summary__item dt {
margin-bottom: 6px;
color: var(--muted);
font-size: 0.72rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.server-summary__item dd {
color: var(--text);
font-size: 0.92rem;
line-height: 1.4;
overflow-wrap: anywhere;
}
.server-trend {
margin-top: 0;
min-width: 0;
padding: 16px;
border: 1px solid rgba(159, 168, 141, 0.12);
border-radius: 16px;
background: rgba(10, 12, 9, 0.26);
}
.server-trend__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.server-trend__header p,
.server-trend__header span {
margin: 0;
font-size: 0.76rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.server-trend__header p {
color: var(--text-soft);
}
.server-trend__header span {
color: var(--muted);
}
.server-trend__bars {
display: flex;
align-items: flex-end;
gap: 8px;
min-height: 70px;
overflow: hidden;
}
.server-trend__bar {
flex: 1 1 0;
min-width: 0;
border-radius: 999px 999px 4px 4px;
background:
linear-gradient(180deg, rgba(210, 182, 118, 0.86), rgba(142, 160, 98, 0.92));
box-shadow: 0 0 18px rgba(142, 160, 98, 0.18);
}
.server-trend__bar--empty {
height: 14%;
opacity: 0.35;
}
.servers-empty {
margin: 0;
padding: 18px;
border: 1px dashed rgba(159, 168, 141, 0.24);
border-radius: 18px;
color: var(--muted);
text-align: center;
}
@media (max-width: 1120px) {
.servers-grid--section {
grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr));
}
.server-card__body {
grid-template-columns: 1fr;
}
}
@media (max-width: 760px) {
.servers-grid,
.servers-grid--section {
grid-template-columns: 1fr;
}
}
@media (max-width: 960px) {
.server-card__body {
grid-template-columns: 1fr;
}
} }
@media (max-width: 640px) { @media (max-width: 640px) {
@@ -196,14 +906,90 @@ h2 {
.hero__content, .hero__content,
.panel { .panel {
padding: 22px 16px; padding: 24px 16px;
}
.hero__content {
padding-top: 44px;
padding-bottom: 52px;
}
.hero__content::before {
inset: 24px 24px auto;
height: 110px;
} }
.hero__text { .hero__text {
font-size: 0.98rem; font-size: 0.98rem;
} }
.logo-frame {
min-height: 180px;
padding: 16px 18px;
}
.logo-frame__image {
max-height: 180px;
}
.discord-button { .discord-button {
width: 100%; width: 100%;
} }
.content {
margin-top: 18px;
gap: 18px;
}
.panel__header--servers,
.server-card__top {
flex-direction: column;
align-items: flex-start;
}
.server-state {
min-width: 0;
}
.server-card__status-column {
width: 100%;
justify-items: start;
min-width: 0;
}
.server-card__population {
min-width: 0;
}
.server-card__actions {
justify-content: start;
}
.server-card__stats {
grid-template-columns: 1fr;
}
.server-card__quickfacts {
grid-template-columns: 1fr;
}
.server-summary {
grid-template-columns: 1fr;
}
.stats-preview__items {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.panel::before {
left: 16px;
}
.video-wrapper {
padding: 6px;
}
.servers-grid {
grid-template-columns: 1fr;
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

@@ -1,4 +1,642 @@
// Inicializacion minima de la landing de HLL Vietnam. // Progressive enhancement for local frontend-backend checks.
const RECENT_SNAPSHOT_WINDOW_MS = 30 * 60 * 1000;
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
console.info("HLL Vietnam frontend ready"); console.info("HLL Vietnam frontend ready");
const backendBaseUrl =
document.body.dataset.backendBaseUrl || "http://127.0.0.1:8000";
const statusNode = document.getElementById("backend-status");
const trailerFrame = document.getElementById("trailer-frame");
const trailerTitle = document.getElementById("trailer-title");
const serversTitle = document.getElementById("servers-title");
const serversNote = document.getElementById("servers-note");
const serversList = document.getElementById("servers-list");
const serversBadge = document.getElementById("servers-badge");
const serversSource = document.getElementById("servers-source");
const serversSourceLabel = document.getElementById("servers-source-label");
const serversSourceMeta = document.getElementById("servers-source-meta");
const statsPreview = document.getElementById("stats-preview");
const statsPreviewItems = document.getElementById("stats-preview-items");
updateBackendStatus(statusNode, "Backend comprobando", "status-chip--idle");
setServersDataState(
{
badgeNode: serversBadge,
sourceNode: serversSource,
labelNode: serversSourceLabel,
metaNode: serversSourceMeta,
},
{ kind: "fallback" },
);
Promise.allSettled([
fetchHealth(backendBaseUrl, statusNode),
hydrateTrailer(backendBaseUrl, trailerFrame, trailerTitle),
hydrateServers(
backendBaseUrl,
serversTitle,
serversNote,
serversList,
serversBadge,
serversSource,
serversSourceLabel,
serversSourceMeta,
statsPreview,
statsPreviewItems,
),
]).catch((error) => {
console.warn("Progressive enhancement failed", error);
});
}); });
async function fetchHealth(backendBaseUrl, statusNode) {
try {
const response = await fetch(`${backendBaseUrl}/health`);
if (!response.ok) {
throw new Error(`Health request failed with ${response.status}`);
}
const payload = await response.json();
if (payload.status === "ok") {
updateBackendStatus(statusNode, "Backend operativo", "status-chip--ok");
return;
}
throw new Error("Unexpected health payload");
} catch (error) {
console.warn("Backend health check unavailable", error);
updateBackendStatus(statusNode, "Modo estatico activo", "status-chip--fallback");
}
}
async function hydrateTrailer(backendBaseUrl, trailerFrame, trailerTitle) {
if (!trailerFrame || !trailerTitle) {
return;
}
try {
const response = await fetch(`${backendBaseUrl}/api/trailer`);
if (!response.ok) {
throw new Error(`Trailer request failed with ${response.status}`);
}
const payload = await response.json();
const trailer = payload.data;
if (!trailer || !trailer.video_url || !trailer.title) {
throw new Error("Trailer payload incomplete");
}
trailerFrame.src = trailer.video_url;
trailerFrame.title = trailer.title;
trailerTitle.textContent = trailer.title;
} catch (error) {
console.warn("Trailer placeholder remains static", error);
}
}
async function hydrateServers(
backendBaseUrl,
serversTitle,
serversNote,
serversList,
serversBadge,
serversSource,
serversSourceLabel,
serversSourceMeta,
statsPreview,
statsPreviewItems,
) {
if (!serversTitle || !serversNote || !serversList || !serversBadge) {
return;
}
try {
const response = await fetch(`${backendBaseUrl}/api/servers`);
if (!response.ok) {
throw new Error(`Servers request failed with ${response.status}`);
}
const payload = await response.json();
const serversData = payload.data;
if (!serversData || !Array.isArray(serversData.items)) {
throw new Error("Servers payload incomplete");
}
serversTitle.textContent =
serversData.title || "Servidores actuales de Hell Let Loose";
setServersDataState(
{
badgeNode: serversBadge,
sourceNode: serversSource,
labelNode: serversSourceLabel,
metaNode: serversSourceMeta,
},
{ kind: "fallback" },
);
if (serversData.context === "current-hll-reference") {
serversNote.textContent =
"Referencia provisional del HLL actual mientras no existan datos reales de HLL Vietnam.";
}
if (serversData.items.length === 0) {
serversList.innerHTML =
'<p class="servers-empty">Informacion de servidores disponible mas adelante.</p>';
return;
}
serversList.innerHTML = serversData.items.map(renderServerCard).join("");
await hydrateServerStats(
backendBaseUrl,
serversTitle,
serversNote,
serversList,
serversBadge,
serversSource,
serversSourceLabel,
serversSourceMeta,
statsPreview,
statsPreviewItems,
);
} catch (error) {
console.warn("Servers panel remains on static fallback", error);
}
}
async function hydrateServerStats(
backendBaseUrl,
serversTitle,
serversNote,
serversList,
serversBadge,
serversSource,
serversSourceLabel,
serversSourceMeta,
statsPreview,
statsPreviewItems,
) {
if (!statsPreview || !statsPreviewItems) {
return;
}
try {
const latestPayload = await fetchJson(`${backendBaseUrl}/api/servers/latest`);
const latestItems = latestPayload?.data?.items;
if (!Array.isArray(latestItems) || latestItems.length === 0) {
return;
}
const histories = await Promise.all(
latestItems.map(async (server) => {
const serverKey = server.external_server_id || server.server_id;
const historyPayload = await fetchJson(
`${backendBaseUrl}/api/servers/${encodeURIComponent(serverKey)}/history?limit=4`,
);
return {
key: serverKey,
items: Array.isArray(historyPayload?.data?.items)
? historyPayload.data.items
: [],
};
}),
);
const historyByServer = new Map(
histories.map((entry) => [String(entry.key), entry.items]),
);
const visibleItems = selectPrimaryServerItems(latestItems);
const latestState = deriveSnapshotState(visibleItems);
const hasRealSnapshots = visibleItems.some(isRealA2SSnapshot);
serversTitle.textContent = hasRealSnapshots
? "Servidores activos con captura real"
: latestPayload.data.title || "Actividad reciente de servidores";
serversNote.textContent = hasRealSnapshots
? "La vista principal muestra solo servidores con snapshots reales A2S utilizables. El fallback provisional queda reservado para cuando el backend no aporta capturas validas."
: "Vista ligera basada en snapshots persistidos del backend. Si no hay capturas reales utilizables, el panel conserva la referencia provisional.";
setServersDataState(
{
badgeNode: serversBadge,
sourceNode: serversSource,
labelNode: serversSourceLabel,
metaNode: serversSourceMeta,
},
latestState,
);
statsPreview.hidden = false;
statsPreviewItems.innerHTML = renderStatsPreview(visibleItems);
serversList.innerHTML = renderServerSections(visibleItems, historyByServer);
} catch (error) {
console.warn("Historical stats preview unavailable", error);
}
}
function updateBackendStatus(statusNode, label, stateClass) {
if (!statusNode) {
return;
}
statusNode.textContent = label;
statusNode.classList.remove("status-chip--ok", "status-chip--fallback");
if (stateClass) {
statusNode.classList.add(stateClass);
}
}
function setServersDataState(nodes, state) {
const { badgeNode, sourceNode, labelNode, metaNode } = nodes;
if (!badgeNode || !sourceNode || !labelNode || !metaNode) {
return;
}
if (state.kind === "live") {
badgeNode.textContent = "Snapshots A2S recientes";
labelNode.textContent = "Captura real reciente";
metaNode.textContent =
state.timestampLabel
? `Ultima captura A2S registrada ${state.timestampLabel}.`
: "El bloque muestra snapshots recientes procedentes del backend.";
badgeNode.classList.remove("status-chip--fallback");
badgeNode.classList.add("status-chip--ok");
sourceNode.dataset.state = "live";
return;
}
if (state.kind === "historical") {
badgeNode.textContent = "Historico persistido";
labelNode.textContent = "Persistencia local disponible";
metaNode.textContent =
state.timestampLabel
? `Se muestran snapshots guardados. Ultima captura ${state.timestampLabel}.`
: "El backend devuelve historico persistido, aunque no sea una captura reciente.";
badgeNode.classList.remove("status-chip--fallback");
badgeNode.classList.add("status-chip--ok");
sourceNode.dataset.state = "historical";
return;
}
badgeNode.textContent = "Fallback estatico";
labelNode.textContent = "Fallback estatico activo";
metaNode.textContent =
"La landing conserva la referencia provisional cuando el backend o el historico aun no aportan snapshots utilizables.";
badgeNode.classList.remove("status-chip--ok");
badgeNode.classList.add("status-chip--fallback");
sourceNode.dataset.state = "fallback";
}
function renderServerCard(server) {
const serverName = server.server_name || "Servidor sin nombre";
const serverStatus =
server.status === "online" ? "Online" : server.status === "offline" ? "Offline" : "Estado pendiente";
const stateClass =
server.status === "online" ? "server-state--online" : "server-state--offline";
const currentMap = server.current_map || "Sin mapa disponible";
const region = server.region || "Region pendiente";
const players = Number.isFinite(server.players) ? server.players : 0;
const maxPlayers = Number.isFinite(server.max_players) ? server.max_players : 0;
const loadPercent =
maxPlayers > 0 ? Math.max(0, Math.min(100, Math.round((players / maxPlayers) * 100))) : 0;
return `
<article class="server-card">
<div class="server-card__top">
<div class="server-card__identity">
<p class="server-card__eyebrow">Servidor de referencia</p>
<h3>${escapeHtml(serverName)}</h3>
</div>
<span class="server-state ${stateClass}">${escapeHtml(serverStatus)}</span>
</div>
<div class="server-card__load" aria-hidden="true">
<span style="width: ${loadPercent}%"></span>
</div>
<dl class="server-card__stats">
<div class="server-stat server-stat--players">
<dt>Jugadores</dt>
<dd>${players} / ${maxPlayers}</dd>
</div>
<div class="server-stat">
<dt>Mapa</dt>
<dd>${escapeHtml(currentMap)}</dd>
</div>
<div class="server-stat">
<dt>Region</dt>
<dd>${escapeHtml(region)}</dd>
</div>
</dl>
</article>
`;
}
function renderServerStatsCard(server, historyItems) {
const serverName = server.server_name || "Servidor sin nombre";
const statusLabel = formatServerStatus(server.status);
const stateClass =
server.status === "online" ? "server-state--online" : "server-state--offline";
const isRealSnapshot = server.snapshot_origin === "real-a2s";
const currentMap = server.current_map || "Sin mapa disponible";
const region = server.region || "Region pendiente";
const players = Number.isFinite(server.players) ? server.players : 0;
const maxPlayers = Number.isFinite(server.max_players) ? server.max_players : 0;
const loadPercent =
maxPlayers > 0 ? Math.max(0, Math.min(100, Math.round((players / maxPlayers) * 100))) : 0;
const updatedAt = server.captured_at
? formatTimestamp(server.captured_at)
: "Sin captura reciente";
const updatedAgo = formatElapsedMinutes(server.history_summary?.minutes_since_last_capture);
const trendMarkup = renderTrend(historyItems);
const connectAction = renderConnectAction(server);
const summaryMarkup = renderHistorySummary(server.history_summary);
const cardVariantClass = isRealSnapshot ? "server-card--real" : "server-card--reference";
const eyebrowLabel = isRealSnapshot ? "Snapshot real A2S" : "Referencia persistida";
const quickFacts = renderQuickFacts([
{ label: "Mapa", value: currentMap },
{ label: "Region", value: region },
{ label: "Ultima captura", value: updatedAgo || updatedAt },
]);
return `
<article class="server-card server-card--stats ${cardVariantClass}">
<div class="server-card__top server-card__top--stats">
<div class="server-card__identity">
<p class="server-card__eyebrow">${escapeHtml(eyebrowLabel)}</p>
<h3>${escapeHtml(serverName)}</h3>
<p class="server-card__meta">Mapa actual, region visible y ultima captura persistida del servidor.</p>
</div>
<div class="server-card__status-column">
<span class="server-state ${stateClass}">${escapeHtml(statusLabel)}</span>
<p class="server-card__population">${escapeHtml(`${players} / ${maxPlayers}`)}</p>
${connectAction}
</div>
</div>
<div class="server-card__body">
<div class="server-card__facts">
${quickFacts}
${summaryMarkup}
</div>
<div class="server-trend">
<div class="server-trend__header">
<p>Tendencia reciente</p>
<span>${historyItems.length} capturas</span>
</div>
<div class="server-trend__bars" aria-hidden="true">${trendMarkup}</div>
</div>
</div>
<div class="server-card__load" aria-hidden="true">
<span style="width: ${loadPercent}%"></span>
</div>
</article>
`;
}
function renderServerSections(latestItems, historyByServer) {
const hasRealSnapshots = latestItems.some(isRealA2SSnapshot);
return renderServerSection(
hasRealSnapshots ? "Snapshots reales A2S" : "Historico persistido",
hasRealSnapshots
? "Servidores reales validados y consultados desde el backend."
: "Snapshots persistidos disponibles mientras no haya capturas reales utilizables.",
hasRealSnapshots ? "server-panel-section--real" : "server-panel-section--reference",
latestItems,
historyByServer,
);
}
function renderServerSection(title, intro, variantClass, items, historyByServer) {
const cards = items
.map((server) =>
renderServerStatsCard(
server,
historyByServer.get(String(server.external_server_id || server.server_id)) || [],
),
)
.join("");
return `
<section class="server-panel-section ${variantClass}">
<div class="server-panel-section__header">
<h3>${escapeHtml(title)}</h3>
<p>${escapeHtml(intro)}</p>
</div>
<div class="servers-grid servers-grid--section">
${cards}
</div>
</section>
`;
}
function renderConnectAction(server) {
if (!server || !isRealA2SSnapshot(server)) {
return "";
}
const host = typeof server.host === "string" ? server.host.trim() : "";
const gamePort = Number.isInteger(server.game_port) ? server.game_port : Number(server.game_port);
if (!host || !Number.isInteger(gamePort) || gamePort <= 0) {
return "";
}
const connectUrl = `steam://connect/${host}:${gamePort}`;
return `
<div class="server-card__actions">
<a class="server-connect-button" href="${escapeHtml(connectUrl)}">Conectar</a>
</div>
`;
}
function renderStatsPreview(items) {
const latestTimestamp = items
.map((item) => item.captured_at)
.filter(Boolean)
.sort()
.at(-1);
const totalPlayers = items.reduce(
(sum, item) => sum + (Number.isFinite(item.players) ? item.players : 0),
0,
);
const onlineServers = items.filter((item) => item.status === "online").length;
return [
renderStatsPreviewItem("Servidores", String(items.length)),
renderStatsPreviewItem("Online", String(onlineServers)),
renderStatsPreviewItem("Jugadores", String(totalPlayers)),
renderStatsPreviewItem(
"Ultima captura",
latestTimestamp ? formatTimestamp(latestTimestamp) : "Pendiente",
),
].join("");
}
function renderHistorySummary(summary) {
if (!summary) {
return "";
}
return `
<dl class="server-summary">
<div class="server-summary__item">
<dt>Visto online</dt>
<dd>${escapeHtml(summary.last_seen_online_at ? formatTimestamp(summary.last_seen_online_at) : "Sin registro")}</dd>
</div>
<div class="server-summary__item">
<dt>Capturas</dt>
<dd>${escapeHtml(`${summary.recent_capture_count || 0}/${summary.window_size || 0}`)}</dd>
</div>
<div class="server-summary__item">
<dt>Promedio</dt>
<dd>${escapeHtml(formatPlayerMetric(summary.recent_average_players))}</dd>
</div>
<div class="server-summary__item">
<dt>Pico</dt>
<dd>${escapeHtml(formatPlayerMetric(summary.recent_peak_players))}</dd>
</div>
</dl>
`;
}
function renderQuickFacts(items) {
return `
<div class="server-card__quickfacts">
${items
.map(
(item) => `
<article class="server-card__quickfact">
<p>${escapeHtml(item.label)}</p>
<strong>${escapeHtml(item.value)}</strong>
</article>
`,
)
.join("")}
</div>
`;
}
function selectPrimaryServerItems(items) {
if (!Array.isArray(items)) {
return [];
}
const realItems = items.filter(isRealA2SSnapshot);
return realItems.length > 0 ? realItems : items;
}
function isRealA2SSnapshot(item) {
return item?.snapshot_origin === "real-a2s";
}
function deriveSnapshotState(items) {
const stateItems = Array.isArray(items) ? items : [];
const realItems = stateItems.filter(isRealA2SSnapshot);
const itemsForState = realItems.length > 0 ? realItems : stateItems;
const latestTimestamp = itemsForState
.map((item) => item.captured_at)
.filter(Boolean)
.sort()
.at(-1);
const latestDate = latestTimestamp ? new Date(latestTimestamp) : null;
const latestTime = latestDate && !Number.isNaN(latestDate.getTime()) ? latestDate.getTime() : null;
const timestampLabel = latestTimestamp ? formatTimestamp(latestTimestamp) : "";
const hasRealA2S = realItems.length > 0;
if (hasRealA2S && latestTime && Date.now() - latestTime <= RECENT_SNAPSHOT_WINDOW_MS) {
return { kind: "live", timestampLabel };
}
return { kind: "historical", timestampLabel };
}
function renderStatsPreviewItem(label, value) {
return `
<article class="stats-preview__item">
<p>${escapeHtml(label)}</p>
<strong>${escapeHtml(value)}</strong>
</article>
`;
}
function renderTrend(historyItems) {
if (!Array.isArray(historyItems) || historyItems.length === 0) {
return '<span class="server-trend__bar server-trend__bar--empty"></span>';
}
const orderedItems = [...historyItems].reverse();
return orderedItems
.map((item) => {
const players = Number.isFinite(item.players) ? item.players : 0;
const maxPlayers = Number.isFinite(item.max_players) ? item.max_players : 0;
const height =
maxPlayers > 0 ? Math.max(14, Math.round((players / maxPlayers) * 100)) : 14;
return `<span class="server-trend__bar" style="height: ${height}%"></span>`;
})
.join("");
}
function formatServerStatus(status) {
if (status === "online") {
return "Online";
}
if (status === "offline") {
return "Offline";
}
return "Estado pendiente";
}
function formatTimestamp(timestamp) {
const value = new Date(timestamp);
if (Number.isNaN(value.getTime())) {
return "Fecha no disponible";
}
return new Intl.DateTimeFormat("es-ES", {
dateStyle: "short",
timeStyle: "short",
}).format(value);
}
function formatElapsedMinutes(minutes) {
if (!Number.isFinite(minutes)) {
return "";
}
if (minutes < 1) {
return "hace menos de 1 min";
}
if (minutes < 60) {
return `hace ${minutes} min`;
}
const hours = Math.floor(minutes / 60);
if (hours < 24) {
return `hace ${hours} h`;
}
const days = Math.floor(hours / 24);
return `hace ${days} d`;
}
function formatPlayerMetric(value) {
return Number.isFinite(value) ? String(value) : "Sin dato";
}
async function fetchJson(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Request failed with ${response.status}`);
}
return response.json();
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}

View File

@@ -10,7 +10,7 @@
<title>Comunidad Hispana - HLL Vietnam</title> <title>Comunidad Hispana - HLL Vietnam</title>
<link rel="stylesheet" href="./assets/css/styles.css" /> <link rel="stylesheet" href="./assets/css/styles.css" />
</head> </head>
<body> <body data-backend-base-url="http://127.0.0.1:8000">
<div class="page-shell"> <div class="page-shell">
<header class="hero"> <header class="hero">
<div class="hero__overlay"></div> <div class="hero__overlay"></div>
@@ -18,16 +18,30 @@
<div class="logo-frame"> <div class="logo-frame">
<img <img
src="./assets/img/logo.png" src="./assets/img/logo.png"
alt="Logo de HLL Vietnam" alt="Logo oficial de la comunidad HLL Vietnam"
class="logo-frame__image" class="logo-frame__image"
width="1024"
height="1044"
decoding="async"
/> />
</div> </div>
<p class="eyebrow">Comunidad táctica hispana</p> <p class="eyebrow">Comunidad táctica hispana</p>
<h1>Comunidad Hispana - HLL Vietnam</h1> <h1 class="hero__title">
Comunidad Hispana
<span class="hero__title-accent">HLL Vietnam</span>
</h1>
<p class="hero__text"> <p class="hero__text">
Punto de encuentro para jugadores, escuadras y comunidad alrededor Punto de encuentro para jugadores, escuadras y comunidad alrededor
del futuro universo HLL Vietnam. del futuro universo HLL Vietnam.
</p> </p>
<div class="hero__actions">
<p
class="status-chip status-chip--idle"
id="backend-status"
aria-live="polite"
>
Backend no verificado
</p>
<a <a
class="discord-button" class="discord-button"
href="https://discord.com/invite/PedEqZ2Xsa" href="https://discord.com/invite/PedEqZ2Xsa"
@@ -37,16 +51,23 @@
Unirse al Discord Unirse al Discord
</a> </a>
</div> </div>
</div>
</header> </header>
<main class="content"> <main class="content">
<section class="panel"> <section class="panel panel--video">
<div class="panel__header"> <div class="panel__header">
<p class="eyebrow">Trailer</p> <p class="eyebrow eyebrow--section">Trailer</p>
<h2>Primer vistazo a HLL Vietnam</h2> <h2 id="trailer-title">Primer vistazo a HLL Vietnam</h2>
<p class="panel__intro panel__intro--tight">
El bloque principal se apoya en una presentacion limpia del
trailer para reforzar el tono tactico y cinematografico del
proyecto.
</p>
</div> </div>
<div class="video-wrapper"> <div class="video-wrapper">
<iframe <iframe
id="trailer-frame"
src="https://www.youtube.com/embed/JzYzYNVWZ_A" src="https://www.youtube.com/embed/JzYzYNVWZ_A"
title="Trailer HLL Vietnam" title="Trailer HLL Vietnam"
loading="lazy" loading="lazy"
@@ -56,6 +77,89 @@
></iframe> ></iframe>
</div> </div>
</section> </section>
<section class="panel panel--servers" aria-labelledby="servers-title">
<div class="panel__header panel__header--servers">
<div>
<p class="eyebrow eyebrow--section">Actividad de servidores</p>
<h2 id="servers-title">Servidores actuales de Hell Let Loose</h2>
</div>
<p class="status-chip status-chip--fallback" id="servers-badge">
Fallback estatico
</p>
</div>
<p class="panel__intro" id="servers-note">
El panel prioriza snapshots reales capturados por el backend y
conserva un fallback provisional solo cuando no hay datos utilizables.
</p>
<div class="servers-source" id="servers-source" data-state="fallback">
<p class="servers-source__label" id="servers-source-label">
Fallback estatico activo
</p>
<p class="servers-source__meta" id="servers-source-meta">
La landing conserva la referencia provisional cuando el backend o
el historico aun no aportan snapshots utilizables.
</p>
</div>
<div class="stats-preview" id="stats-preview" hidden>
<p class="stats-preview__eyebrow">Vista previa historica</p>
<div class="stats-preview__items" id="stats-preview-items"></div>
</div>
<div class="servers-grid" id="servers-list">
<article class="server-card">
<div class="server-card__top">
<div class="server-card__identity">
<p class="server-card__eyebrow">Servidor de referencia</p>
<h3>HLL ESP Tactical Rotation</h3>
</div>
<span class="server-state server-state--online">Online</span>
</div>
<div class="server-card__load" aria-hidden="true">
<span style="width: 74%"></span>
</div>
<dl class="server-card__stats">
<div class="server-stat server-stat--players">
<dt>Jugadores</dt>
<dd>74 / 100</dd>
</div>
<div class="server-stat">
<dt>Mapa</dt>
<dd>Sainte-Marie-du-Mont</dd>
</div>
<div class="server-stat">
<dt>Region</dt>
<dd>EU</dd>
</div>
</dl>
</article>
<article class="server-card">
<div class="server-card__top">
<div class="server-card__identity">
<p class="server-card__eyebrow">Servidor de referencia</p>
<h3>HLL LATAM Night Offensive</h3>
</div>
<span class="server-state server-state--online">Online</span>
</div>
<div class="server-card__load" aria-hidden="true">
<span style="width: 51%"></span>
</div>
<dl class="server-card__stats">
<div class="server-stat server-stat--players">
<dt>Jugadores</dt>
<dd>51 / 100</dd>
</div>
<div class="server-stat">
<dt>Mapa</dt>
<dd>Carentan</dd>
</div>
<div class="server-stat">
<dt>Region</dt>
<dd>LATAM</dd>
</div>
</dl>
</article>
</div>
</section>
</main> </main>
</div> </div>