From c50ab6404779231a7caba3fb9dd84283d465dd50 Mon Sep 17 00:00:00 2001 From: devRaGonSa Date: Fri, 20 Mar 2026 18:54:31 +0100 Subject: [PATCH] Ajustes UI --- .../done/TASK-018-hero-layout-rebalance.md | 57 ++ .../done/TASK-019-atmospheric-depth-pass.md | 56 ++ .../done/TASK-020-component-surface-polish.md | 58 ++ ...ied-page-shell-and-section-width-system.md | 60 ++ ...st-signals-and-technical-summary-strips.md | 59 ++ ...mplify-real-server-cards-for-product-ui.md | 69 +++ .../TASK-046-near-full-width-master-shell.md | 44 ++ ...ASK-047-remove-nested-width-constraints.md | 44 ++ .../TASK-048-wide-desktop-server-grid-pass.md | 43 ++ ...-nonessential-copy-and-technical-badges.md | 62 +++ ...-refresh-and-frontend-polling-alignment.md | 67 +++ ...1-real-server-card-minimal-product-pass.md | 66 +++ ai/worker.lock | 2 +- backend/README.md | 26 +- backend/app/config.py | 2 +- backend/app/scheduler.py | 4 +- frontend/assets/css/styles.css | 523 +++++++----------- frontend/assets/js/main.js | 328 +++-------- frontend/index.html | 254 +++++---- 19 files changed, 1111 insertions(+), 713 deletions(-) create mode 100644 ai/tasks/done/TASK-018-hero-layout-rebalance.md create mode 100644 ai/tasks/done/TASK-019-atmospheric-depth-pass.md create mode 100644 ai/tasks/done/TASK-020-component-surface-polish.md create mode 100644 ai/tasks/done/TASK-043-unified-page-shell-and-section-width-system.md create mode 100644 ai/tasks/done/TASK-044-remove-test-signals-and-technical-summary-strips.md create mode 100644 ai/tasks/done/TASK-045-simplify-real-server-cards-for-product-ui.md create mode 100644 ai/tasks/done/TASK-046-near-full-width-master-shell.md create mode 100644 ai/tasks/done/TASK-047-remove-nested-width-constraints.md create mode 100644 ai/tasks/done/TASK-048-wide-desktop-server-grid-pass.md create mode 100644 ai/tasks/done/TASK-049-remove-nonessential-copy-and-technical-badges.md create mode 100644 ai/tasks/done/TASK-050-live-snapshot-refresh-and-frontend-polling-alignment.md create mode 100644 ai/tasks/done/TASK-051-real-server-card-minimal-product-pass.md diff --git a/ai/tasks/done/TASK-018-hero-layout-rebalance.md b/ai/tasks/done/TASK-018-hero-layout-rebalance.md new file mode 100644 index 0000000..66a721e --- /dev/null +++ b/ai/tasks/done/TASK-018-hero-layout-rebalance.md @@ -0,0 +1,57 @@ +# TASK-018-hero-layout-rebalance + +## Goal +Reequilibrar la composición visual del hero de la landing de HLL Vietnam para darle más impacto, reducir sensación de vacío vertical y mejorar la relación entre logo, titular, subtítulo y CTA principal. + +## Context +La landing ya tiene una base visual sólida, pero el hero actual presenta varios puntos de mejora: demasiado espacio vertical, un titular muy dominante partido en varias líneas, un logo con menos protagonismo del deseado y una composición general menos potente que el resto de la página. Esta task debe centrarse únicamente en recomponer el bloque principal para que se perciba como una cabecera premium, más compacta y más cinematográfica, sin cambiar el alcance funcional actual. + +## Steps +1. Revisar la composición actual del hero en la landing. +2. Evaluar el equilibrio visual entre: + - logo + - eyebrow o etiqueta superior + - título principal + - subtítulo o texto descriptivo + - chip de estado backend si aplica + - CTA principal +3. Reducir la sensación de vacío vertical y mejorar el ritmo del bloque. +4. Ajustar el protagonismo del logo para que se sienta más integrado y relevante. +5. Ajustar el titular para que mantenga fuerza, pero no rompa la composición ni monopolice toda la jerarquía visual. +6. Reordenar o afinar espaciados, anchuras máximas y alineaciones para que el conjunto se perciba más compacto y más intencional. +7. Mantener el hero claro, centrado en comunidad, branding y CTA. +8. Validar que la composición siga funcionando bien en móvil y escritorio. +9. No alterar enlaces, rutas ni comportamiento funcional existente. + +## 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 del Discord. +- No cambiar el enlace del tráiler. +- No añadir nuevas secciones. +- No introducir librerías nuevas. +- No romper el fallback actual. +- No hacer cambios destructivos. +- Mantener la landing simple y coherente con el tono HLL Vietnam. + +## Validation +- El hero se siente más compacto y más fuerte visualmente. +- El logo tiene mejor presencia relativa. +- El titular sigue siendo potente pero está mejor equilibrado. +- El CTA sigue siendo claro y visible. +- La cabecera transmite más sensación de portada principal. +- La composición sigue siendo responsive y estable. + +## Change Budget +- Preferir menos de 2 archivos modificados. +- Preferir menos de 170 líneas cambiadas. diff --git a/ai/tasks/done/TASK-019-atmospheric-depth-pass.md b/ai/tasks/done/TASK-019-atmospheric-depth-pass.md new file mode 100644 index 0000000..ba5d45d --- /dev/null +++ b/ai/tasks/done/TASK-019-atmospheric-depth-pass.md @@ -0,0 +1,56 @@ +# TASK-019-atmospheric-depth-pass + +## Goal +Aumentar la profundidad visual y la atmósfera general de la landing de HLL Vietnam mediante un refinamiento controlado de fondo, overlays, iluminación y separación de planos, sin recargar la interfaz ni romper su sobriedad. + +## Context +La paleta actual funciona y la base visual está bien encaminada, pero el fondo y el marco general aún se perciben algo planos. Hace falta una pasada de atmósfera para reforzar el tono Vietnam/táctico, mejorar la profundidad percibida y dar más cohesión al conjunto, especialmente alrededor del hero y de las secciones principales. + +## Steps +1. Revisar el tratamiento actual del fondo y los overlays. +2. Evaluar cómo se perciben: + - profundidad + - viñeteado + - textura ambiental + - degradados + - separación entre hero y secciones +3. Refinar el sistema de fondo para aportar más atmósfera sin saturar. +4. Aplicar mejoras controladas en: + - overlays + - sombras de entorno + - iluminación indirecta + - separación de bloques + - sensación cinematográfica +5. Mantener una estética oscura, militar y limpia. +6. Evitar efectos exagerados, brillos excesivos o ruido visual. +7. Validar que el resultado no perjudica legibilidad ni rendimiento aparente. +8. Mantener intacto el comportamiento funcional de la landing. + +## Files to Read First +- AGENTS.md +- ai/repo-context.md +- frontend/index.html +- frontend/assets/css/styles.css + +## Expected Files to Modify +- frontend/assets/css/styles.css +- opcionalmente `frontend/index.html` si hiciera falta una envolvente mínima o ajuste estructural pequeño para soportar mejor la profundidad visual + +## Constraints +- No cambiar contenido funcional. +- No añadir imágenes nuevas ni dependencias nuevas salvo que sea estrictamente innecesario. +- No rediseñar completamente la página. +- No tocar backend. +- No hacer cambios destructivos. +- Mantener el tono sobrio y cinematográfico. + +## Validation +- La página transmite mayor profundidad visual. +- El fondo se siente menos plano. +- Hero y secciones se perciben mejor separados y más cohesionados. +- La atmósfera Vietnam/táctica queda reforzada. +- La legibilidad y claridad general se mantienen o mejoran. + +## Change Budget +- Preferir menos de 2 archivos modificados. +- Preferir menos de 160 líneas cambiadas. diff --git a/ai/tasks/done/TASK-020-component-surface-polish.md b/ai/tasks/done/TASK-020-component-surface-polish.md new file mode 100644 index 0000000..a841e16 --- /dev/null +++ b/ai/tasks/done/TASK-020-component-surface-polish.md @@ -0,0 +1,58 @@ +# TASK-020-component-surface-polish + +## Goal +Pulir visualmente superficies y componentes de la landing de HLL Vietnam para mejorar calidad percibida, consistencia y acabado en tarjetas, chips, badges, contenedores y detalles de UI. + +## Context +La landing ya tiene una estructura visual bastante competente, pero aún puede ganar acabado en microdetalles de interfaz. El panel de servidores, las tarjetas, los chips de estado y los contenedores principales necesitan una pasada de consistencia visual para que el conjunto se sienta más refinado y más uniforme. + +## Steps +1. Revisar los componentes visuales actuales de la landing. +2. Evaluar consistencia en: + - radios + - bordes + - sombras + - chips + - badges de estado + - tarjetas + - barras de ocupación + - encabezados de panel +3. Refinar esos elementos para que compartan un lenguaje visual más sólido y coherente. +4. Mejorar calidad percibida sin añadir complejidad innecesaria. +5. Asegurar que el panel de servidores y los componentes de apoyo se lean mejor tanto en estado estático como hidratado por JS. +6. Mantener clara la jerarquía entre componentes primarios y secundarios. +7. Validar responsive y consistencia entre escritorio y móvil. +8. No tocar el contrato del backend ni el contenido funcional de los payloads. + +## 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 endpoints. +- No cambiar payloads backend. +- No añadir librerías nuevas. +- No romper el fallback estático. +- No rediseñar completamente la landing. +- No hacer cambios destructivos. +- Mantener el ajuste centrado en UI y acabado visual. + +## Validation +- Las superficies y componentes se ven más coherentes y mejor acabados. +- Chips, badges y tarjetas tienen una presentación más limpia y consistente. +- El panel de servidores gana claridad y calidad visual. +- La landing mantiene su funcionamiento actual. +- La mejora se aprecia en desktop y mobile. + +## Change Budget +- Preferir menos de 3 archivos modificados. +- Preferir menos de 190 líneas cambiadas. diff --git a/ai/tasks/done/TASK-043-unified-page-shell-and-section-width-system.md b/ai/tasks/done/TASK-043-unified-page-shell-and-section-width-system.md new file mode 100644 index 0000000..2e84bc4 --- /dev/null +++ b/ai/tasks/done/TASK-043-unified-page-shell-and-section-width-system.md @@ -0,0 +1,60 @@ +# TASK-043-unified-page-shell-and-section-width-system + +## Goal +Unificar el sistema de anchuras de la pagina para que hero, trailer y panel de servidores compartan el mismo ancho util y se perciban como parte de una misma experiencia visual, eliminando la sensacion actual de secciones con tamanos distintos. + +## Context +La UI actual ya funciona y muestra datos reales, pero las secciones principales no comparten una anchura consistente. Esto hace que la pagina se sienta fragmentada y reduce la sensacion de producto final. El objetivo de esta task es definir un unico sistema de shell/layout para las secciones principales y aplicarlo de forma consistente. + +## Steps +1. Revisar la estructura actual de shells, contenedores y max-width del frontend. +2. Identificar diferencias de ancho entre: + - hero + - bloque del trailer + - bloque de servidores +3. Definir un unico ancho maestro para las secciones principales en desktop. +4. Hacer que hero, trailer y panel de servidores usen el mismo carril visual principal. +5. Ajustar margenes laterales, paddings y separacion vertical para evitar sensacion de fragmentacion. +6. Mantener el comportamiento responsive correcto en tablet y movil. +7. No redisenar el contenido, solo el sistema de layout y anchuras. + +## 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 anadir librerias nuevas. +- No hacer cambios destructivos. +- Mantener la identidad visual actual. +- Mantener el trabajo centrado en shell/layout. + +## Validation +- Hero, trailer y panel de servidores comparten el mismo ancho util en desktop. +- La pagina se percibe mas unificada. +- No quedan secciones visualmente demasiado estrechas respecto a otras. +- El responsive sigue funcionando. + +## Change Budget +- Preferir menos de 3 archivos modificados. +- Preferir menos de 180 lineas cambiadas. + +## Outcome +- `frontend/index.html` introduce un `panel__shell` comun en trailer y servidores para que ambas secciones compartan el mismo carril interior. +- `frontend/assets/css/styles.css` reemplaza los anchos independientes de hero, trailer y servidores por un sistema unificado con `--page-shell-width` y `--panel-content-width`. +- El ajuste se mantuvo centrado en layout, paddings y separacion vertical, sin redisenar el contenido. + +## Validation Result +- Revisado en diff: la task queda limitada a `frontend/index.html`, `frontend/assets/css/styles.css` y el archivo de task, dentro del scope esperado. +- Revisado en codigo: hero, trailer y panel de servidores pasan a compartir un mismo ancho maestro en desktop y un mismo carril interior para el contenido principal. +- El responsive existente se conserva con los breakpoints moviles previos y con padding especifico para paneles en `max-width: 640px`. + +## Decision Notes +- Se unifico el sistema de shell a nivel de seccion y de carril interior en lugar de tocar el contenido de cada bloque, para cumplir el objetivo visual sin abrir un rediseno mayor. diff --git a/ai/tasks/done/TASK-044-remove-test-signals-and-technical-summary-strips.md b/ai/tasks/done/TASK-044-remove-test-signals-and-technical-summary-strips.md new file mode 100644 index 0000000..9167023 --- /dev/null +++ b/ai/tasks/done/TASK-044-remove-test-signals-and-technical-summary-strips.md @@ -0,0 +1,59 @@ +# TASK-044-remove-test-signals-and-technical-summary-strips + +## Goal +Eliminar de la interfaz principal los elementos, etiquetas y resumenes que den sensacion de entorno de pruebas o validacion tecnica, dejando una experiencia mas limpia y orientada a producto final. + +## Context +La pagina ya esta entrando en una fase mas cercana a producto final. Sin embargo, siguen apareciendo elementos como "captura real reciente", resumenes tecnicos o textos que funcionan bien para desarrollo, pero no para una experiencia publica mas madura. + +## Steps +1. Revisar el bloque actual de servidores y los elementos intermedios previos a la rejilla de tarjetas. +2. Eliminar o simplificar los elementos que indiquen: + - validacion interna + - estado de prueba + - resumenes tecnicos poco utiles para el visitante +3. Quitar la seccion visual equivalente a "captura real reciente". +4. Quitar la fila o bloque de resumen tipo "vista previa historica" si no aporta valor claro al usuario final. +5. Mantener solo el copy que ayuda a entender el bloque como producto real. +6. Asegurar que el panel de servidores quede mas limpio y mas directo. + +## Files to Read First +- AGENTS.md +- ai/repo-context.md +- frontend/index.html +- frontend/assets/js/main.js +- frontend/assets/css/styles.css + +## Expected Files to Modify +- frontend/index.html +- frontend/assets/js/main.js +- frontend/assets/css/styles.css + +## Constraints +- No tocar backend. +- No romper el consumo de datos reales. +- No eliminar fallbacks internos, solo su exposicion innecesaria en la UI. +- No hacer cambios destructivos. +- Mantener el ajuste centrado en limpieza de producto. + +## Validation +- Desaparecen indicios de "modo prueba" en la vista principal. +- El bloque de servidores se percibe mas limpio y mas orientado a usuario final. +- La pagina mantiene funcionalidad completa. + +## Change Budget +- Preferir menos de 3 archivos modificados. +- Preferir menos de 140 lineas cambiadas. + +## Outcome +- `frontend/index.html` elimina el bloque visual de fuente/estado y la franja de resumen historico previa a la rejilla. +- `frontend/assets/js/main.js` mantiene la logica interna de estados, pero reduce la exposicion publica a un badge limpio y copy orientado a producto. +- `frontend/assets/css/styles.css` retira los estilos de los strips tecnicos eliminados. + +## Validation Result +- Revisado en codigo: los datos reales siguen hidratando el panel y la diferenciacion interna entre live, historical y fallback se conserva en JavaScript. +- Validado con `node --check frontend/assets/js/main.js`. +- Revisado en diff: la task queda limitada a `frontend/index.html`, `frontend/assets/js/main.js`, `frontend/assets/css/styles.css` y el archivo de task. + +## Decision Notes +- La limpieza se resolvio ocultando por completo los bloques tecnicos y simplificando el lenguaje de estado, en lugar de introducir nuevas superficies UI intermedias. diff --git a/ai/tasks/done/TASK-045-simplify-real-server-cards-for-product-ui.md b/ai/tasks/done/TASK-045-simplify-real-server-cards-for-product-ui.md new file mode 100644 index 0000000..117bd2d --- /dev/null +++ b/ai/tasks/done/TASK-045-simplify-real-server-cards-for-product-ui.md @@ -0,0 +1,69 @@ +# TASK-045-simplify-real-server-cards-for-product-ui + +## Goal +Simplificar las tarjetas de servidores reales para mostrar solo la informacion mas util para el visitante final, mejorando la claridad y reduciendo ruido visual y tecnico. + +## Context +Las tarjetas actuales ya muestran datos reales A2S, pero siguen ensenando informacion que no aporta suficiente valor para la experiencia principal. La tarjeta debe centrarse en lo esencial: nombre, estado, jugadores, mapa, region, ultima captura y CTA de conexion. Cualquier metrica adicional debe ser secundaria o eliminarse si no mejora claramente la lectura. + +## Steps +1. Revisar la estructura actual de las tarjetas reales. +2. Reducir la informacion visible principal a: + - nombre + - estado + - jugadores actuales + - mapa + - region + - ultima captura + - boton Conectar +3. Evaluar si promedio y pico deben quedar como datos secundarios discretos o desaparecer. +4. Eliminar de la tarjeta elementos como: + - tendencia reciente + - numero de capturas + - bloques densos poco utiles +5. Reorganizar la tarjeta para que se lea rapido y con claridad. +6. Mantener una composicion limpia y coherente con el diseno actual. +7. Asegurar buena legibilidad en desktop y movil. + +## 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 alineacion menor si fuera imprescindible. +- No romper la CTA Conectar. +- No anadir librerias nuevas. +- No hacer cambios destructivos. +- Mantener el ajuste centrado en UX de producto final. + +## Validation +- Las tarjetas reales muestran solo la informacion mas util y legible. +- Desaparece ruido tecnico innecesario. +- La CTA Conectar sigue visible y clara. +- La UI gana sensacion de producto final. + +## Change Budget +- Preferir menos de 3 archivos modificados. +- Preferir menos de 160 lineas cambiadas. + +## Outcome +- `frontend/assets/js/main.js` simplifica el render de tarjetas reales a identidad, estado, poblacion, quick facts y CTA, eliminando resumen historico, tendencia y llamadas auxiliares ya innecesarias. +- `frontend/assets/css/styles.css` elimina estilos asociados a bloques tecnicos densos y deja una composicion mas directa para las quick facts. +- No fue necesario tocar backend ni cambiar la CTA `Conectar`. + +## Validation Result +- Validado con `node --check frontend/assets/js/main.js`. +- Revisado en codigo: las tarjetas mantienen nombre, estado, jugadores, mapa, region, ultima captura y CTA, y desaparecen tendencia, capturas, promedio y pico. +- Revisado en diff: la task queda limitada a `frontend/assets/js/main.js`, `frontend/assets/css/styles.css` y el archivo de task. + +## Decision Notes +- Se opto por eliminar por completo las metricas secundarias en lugar de rebajarlas visualmente, porque seguian cargando la tarjeta y no aportaban suficiente valor a la lectura principal. diff --git a/ai/tasks/done/TASK-046-near-full-width-master-shell.md b/ai/tasks/done/TASK-046-near-full-width-master-shell.md new file mode 100644 index 0000000..7e773de --- /dev/null +++ b/ai/tasks/done/TASK-046-near-full-width-master-shell.md @@ -0,0 +1,44 @@ +# TASK-046-near-full-width-master-shell + +## Goal +Sustituir la columna central demasiado estrecha por un shell maestro mucho más ancho, de forma que la landing use casi todo el ancho útil de desktop y reduzca drásticamente el espacio vacío lateral. + +## Context +La UI ya unificó las secciones bajo un mismo sistema de anchura, pero ese sistema sigue siendo demasiado estrecho. El resultado es una página encajada en el centro con mucho espacio muerto a izquierda y derecha. El objetivo de esta task es ampliar el carril principal de la landing hasta un ancho cercano al total útil de la ventana, manteniendo márgenes razonables. + +## Steps +1. Revisar el shell principal actual y cualquier `max-width` asociado. +2. Sustituir el ancho maestro actual por uno claramente más amplio en desktop. +3. Usar una referencia tipo: + - `width: min(1600px, calc(100vw - 64px))` + o equivalente coherente con el diseño actual. +4. Mantener márgenes laterales razonables sin dejar la web pegada a los bordes. +5. Asegurar que el comportamiento responsive en tablet y móvil siga siendo correcto. +6. No rediseñar contenidos; solo ampliar el carril principal de la landing. + +## Files to Read First +- AGENTS.md +- ai/repo-context.md +- frontend/index.html +- frontend/assets/css/styles.css + +## Expected Files to Modify +- frontend/assets/css/styles.css +- frontend/index.html si fuera necesario alinear wrappers + +## Constraints +- No tocar backend. +- No añadir librerías nuevas. +- No hacer cambios destructivos. +- Mantener la identidad visual actual. +- Mantener el trabajo centrado en el shell principal. + +## Validation +- La página usa mucho mejor el ancho disponible en desktop. +- Disminuye claramente el espacio vacío lateral. +- Hero, tráiler y servidores siguen alineados en un mismo carril visual. +- Tablet y móvil no se rompen. + +## Change Budget +- Preferir menos de 2 archivos modificados. +- Preferir menos de 120 líneas cambiadas. diff --git a/ai/tasks/done/TASK-047-remove-nested-width-constraints.md b/ai/tasks/done/TASK-047-remove-nested-width-constraints.md new file mode 100644 index 0000000..72e451e --- /dev/null +++ b/ai/tasks/done/TASK-047-remove-nested-width-constraints.md @@ -0,0 +1,44 @@ +# TASK-047-remove-nested-width-constraints + +## Goal +Eliminar o corregir restricciones internas de anchura que siguen estrechando hero, tráiler y panel de servidores aunque el shell principal se haya ensanchado. + +## Context +En la UI actual hay indicios claros de que, además del shell principal, existen wrappers interiores o `max-width` secundarios que mantienen las secciones demasiado estrechas. Esta task debe identificar y eliminar esas restricciones internas para que cada sección ocupe realmente el ancho disponible del carril principal. + +## Steps +1. Revisar la estructura HTML y CSS de hero, tráiler y panel de servidores. +2. Identificar wrappers internos con `max-width`, anchuras fijas o márgenes automáticos que estrechen el contenido. +3. Corregir esas restricciones para que: + - hero + - tráiler + - panel de servidores + usen `width: 100%` dentro del shell principal. +4. Mantener una composición limpia y centrada, sin volver a una columna estrecha. +5. Evitar que un wrapper interno rompa el ancho unificado buscado. +6. No rediseñar secciones; solo liberar el ancho real utilizable. + +## Files to Read First +- AGENTS.md +- ai/repo-context.md +- frontend/index.html +- frontend/assets/css/styles.css + +## Expected Files to Modify +- frontend/index.html +- frontend/assets/css/styles.css + +## Constraints +- No tocar backend. +- No añadir librerías nuevas. +- No hacer cambios destructivos. +- Mantener la estructura visual existente salvo correcciones de anchura. + +## Validation +- No quedan wrappers interiores que vuelvan a estrechar las secciones principales. +- Hero, tráiler y servidores ocupan realmente el ancho del shell maestro. +- La sensación de “columna central demasiado angosta” desaparece o se reduce claramente. + +## Change Budget +- Preferir menos de 2 archivos modificados. +- Preferir menos de 120 líneas cambiadas. diff --git a/ai/tasks/done/TASK-048-wide-desktop-server-grid-pass.md b/ai/tasks/done/TASK-048-wide-desktop-server-grid-pass.md new file mode 100644 index 0000000..a2b0b28 --- /dev/null +++ b/ai/tasks/done/TASK-048-wide-desktop-server-grid-pass.md @@ -0,0 +1,43 @@ +# TASK-048-wide-desktop-server-grid-pass + +## Goal +Aprovechar el nuevo ancho de desktop para que el panel de servidores use una rejilla más amplia y cómoda, evitando que las tarjetas sigan viéndose pequeñas o demasiado encerradas. + +## Context +Una vez ampliado el shell principal y eliminadas restricciones internas, el panel de servidores debe beneficiarse directamente de ese ancho. Si la rejilla sigue siendo demasiado conservadora, la página seguirá dando sensación de estrechez aunque el contenedor ya sea ancho. + +## Steps +1. Revisar la rejilla actual del panel de servidores. +2. Ajustar el grid para que aproveche mejor desktop ancho. +3. Usar una estrategia robusta tipo: + - `repeat(auto-fit, minmax(360px, 1fr))` + o equivalente mejor adaptada al diseño actual. +4. Revisar gaps, paddings y anchuras mínimas para evitar tarjetas pequeñas o apretadas. +5. Mantener buen comportamiento en tablet y móvil. +6. No rediseñar la tarjeta; centrarse en cómo la rejilla usa el ancho disponible. + +## Files to Read First +- AGENTS.md +- ai/repo-context.md +- frontend/assets/css/styles.css +- frontend/assets/js/main.js +- frontend/index.html + +## Expected Files to Modify +- frontend/assets/css/styles.css +- frontend/index.html si hiciera falta alinear clases o wrappers + +## Constraints +- No tocar backend. +- No añadir librerías nuevas. +- No hacer cambios destructivos. +- Mantener el trabajo centrado en grid y aprovechamiento del ancho. + +## Validation +- El panel de servidores se ve más amplio y mejor repartido en desktop. +- Las tarjetas dejan de sentirse pequeñas o encajadas. +- La UI mantiene buena lectura en tablet y móvil. + +## Change Budget +- Preferir menos de 2 archivos modificados. +- Preferir menos de 120 líneas cambiadas. diff --git a/ai/tasks/done/TASK-049-remove-nonessential-copy-and-technical-badges.md b/ai/tasks/done/TASK-049-remove-nonessential-copy-and-technical-badges.md new file mode 100644 index 0000000..99444fb --- /dev/null +++ b/ai/tasks/done/TASK-049-remove-nonessential-copy-and-technical-badges.md @@ -0,0 +1,62 @@ +# TASK-049-remove-nonessential-copy-and-technical-badges + +## Goal +Eliminar de la landing los textos descriptivos y etiquetas tecnicas que no aportan valor al usuario final, dejando una interfaz mas limpia, directa y orientada a producto final. + +## Context +La UI ya esta en una fase suficientemente madura como para prescindir de copy de presentacion y de labels internos o tecnicos. Actualmente siguen apareciendo textos de relleno o de tono demasiado explicativo, asi como etiquetas como "Snapshot real A2S", que no deberian estar visibles en la vista principal publica. + +## Steps +1. Revisar el hero, bloque de trailer y bloque de servidores. +2. Eliminar o simplificar textos descriptivos no esenciales, incluyendo ejemplos como: + - descripciones explicativas del trailer + - copy redundante del bloque de servidores + - textos internos de validacion o sistema +3. Eliminar de las tarjetas reales etiquetas tecnicas como: + - "Snapshot real A2S" +4. Revisar tambien chips o badges secundarios que aporten poco valor al usuario final. +5. Mantener solo el texto minimo necesario para entender cada seccion. +6. Preservar claridad y estetica sin dejar la pagina vacia ni fria. +7. No tocar backend en esta task. + +## Files to Read First +- AGENTS.md +- ai/repo-context.md +- frontend/index.html +- frontend/assets/js/main.js +- frontend/assets/css/styles.css + +## Expected Files to Modify +- frontend/index.html +- frontend/assets/js/main.js +- frontend/assets/css/styles.css + +## Constraints +- No tocar backend. +- No eliminar informacion util de servidor como mapa, jugadores, region, estado, ultima actualizacion y CTA. +- No anadir librerias nuevas. +- No hacer cambios destructivos. +- Mantener el ajuste centrado en copy/UI. + +## Validation +- Desaparecen textos descriptivos innecesarios. +- Desaparecen etiquetas tecnicas como "Snapshot real A2S". +- La pagina se percibe mas limpia y mas finalista. +- La informacion util del servidor sigue intacta. + +## Change Budget +- Preferir menos de 3 archivos modificados. +- Preferir menos de 120 lineas cambiadas. + +## Outcome +- `frontend/index.html` elimina copy descriptivo del hero y del bloque de trailer, simplifica el encabezado de servidores y quita etiquetas secundarias en las tarjetas estaticas. +- `frontend/assets/js/main.js` sustituye textos tecnicos por textos de producto mas directos y elimina labels internos visibles en tarjetas y estados del bloque de servidores. +- `frontend/assets/css/styles.css` elimina el estilo del texto descriptivo del hero ya retirado del markup. + +## Validation Result +- Validado con `node --check frontend/assets/js/main.js`. +- Verificado con `rg` que ya no quedan labels visibles como `Snapshot real A2S`, `Fallback estatico`, `Servidor de referencia` o equivalentes en `frontend/`. +- Revisado en diff: la task queda limitada a `frontend/index.html`, `frontend/assets/js/main.js`, `frontend/assets/css/styles.css` y este archivo de task. El cambio no toca backend. + +## Decision Notes +- Se mantuvo una nota breve en el bloque de servidores para no dejar la seccion fria, pero se reescribio a una forma claramente orientada a usuario final y sin lenguaje interno del sistema. diff --git a/ai/tasks/done/TASK-050-live-snapshot-refresh-and-frontend-polling-alignment.md b/ai/tasks/done/TASK-050-live-snapshot-refresh-and-frontend-polling-alignment.md new file mode 100644 index 0000000..02ca86e --- /dev/null +++ b/ai/tasks/done/TASK-050-live-snapshot-refresh-and-frontend-polling-alignment.md @@ -0,0 +1,67 @@ +# TASK-050-live-snapshot-refresh-and-frontend-polling-alignment + +## Goal +Alinear la captura periodica de snapshots y el consumo del frontend para que la web muestre datos mucho mas cercanos al estado real actual de los servidores, evitando que se queden estancados durante horas. + +## Context +La pagina ya consume datos reales A2S, pero en la practica estaba mostrando snapshots demasiado antiguos, con mapas y poblacion desactualizados. Esto degrada la percepcion de fiabilidad del producto. El sistema necesitaba una politica mas util de refresco local y una lectura periodica razonable desde frontend. + +## Steps +1. Revisar como se ejecuta actualmente el scheduler local de snapshots. +2. Revisar la frecuencia de captura actual o la ausencia de ejecucion continua. +3. Definir una frecuencia de refresco razonable para entorno local de desarrollo y demo, por ejemplo: + - captura backend cada 60 segundos + - refresco frontend cada 60-90 segundos +4. Ajustar el scheduler o su configuracion para facilitar un refresco frecuente y controlado. +5. Revisar el frontend para que vuelva a consultar el backend periodicamente sin necesidad de recargar la pagina. +6. Asegurar que el refresco no rompa la UI ni genere comportamiento agresivo. +7. Mantener el sistema simple y apropiado para la fase actual. +8. Documentar claramente como arrancar el backend y el refresco para ver datos vivos. + +## Files to Read First +- AGENTS.md +- ai/repo-context.md +- backend/README.md +- backend/app/config.py +- backend/app/scheduler.py +- backend/app/collector.py +- frontend/assets/js/main.js +- frontend/index.html + +## Expected Files to Modify +- backend/README.md +- backend/app/config.py +- backend/app/scheduler.py +- frontend/assets/js/main.js +- opcionalmente frontend/index.html si requiere una alineacion minima + +## Constraints +- No introducir infraestructura pesada. +- No anadir librerias nuevas. +- No hacer cambios destructivos. +- No abrir nuevas fuentes de datos. +- Mantener la solucion simple, local y util para producto actual. + +## Validation +- El sistema puede refrescar snapshots reales con una frecuencia razonable. +- El frontend actualiza la vista sin depender de recarga manual. +- Los mapas y jugadores cambian cuando cambian realmente en los servidores. +- La documentacion deja claro como usar este flujo en local. + +## Change Budget +- Preferir menos de 5 archivos modificados. +- Preferir menos de 180 lineas cambiadas. + +## Outcome +- `backend/app/config.py` baja el intervalo por defecto del scheduler local a `60` segundos para acercar la persistencia a una demo viva. +- `backend/app/scheduler.py` alinea la ayuda del comando con ese nuevo comportamiento orientado a desarrollo y demo. +- `frontend/index.html` expone `data-server-refresh-ms=\"60000\"` y `frontend/assets/js/main.js` reutiliza ese valor para relanzar `hydrateServers()` cada `60` segundos sin recargar la pagina y sin solapar peticiones. +- `backend/README.md` documenta el flujo local recomendado con backend, scheduler y polling del frontend. + +## Validation Result +- Validado con `node --check frontend/assets/js/main.js`. +- Validado con `python -m py_compile backend/app/config.py backend/app/scheduler.py backend/app/collector.py`. +- Revisado en diff: la task queda limitada a `backend/README.md`, `backend/app/config.py`, `backend/app/scheduler.py`, `frontend/assets/js/main.js`, `frontend/index.html` y este archivo de task. + +## Decision Notes +- Se mantuvo el polling solo sobre el bloque de servidores para no convertir toda la landing en una pagina con refresco agresivo. Salud del backend y trailer siguen siendo una hidratacion inicial simple. diff --git a/ai/tasks/done/TASK-051-real-server-card-minimal-product-pass.md b/ai/tasks/done/TASK-051-real-server-card-minimal-product-pass.md new file mode 100644 index 0000000..8290fb7 --- /dev/null +++ b/ai/tasks/done/TASK-051-real-server-card-minimal-product-pass.md @@ -0,0 +1,66 @@ +# TASK-051-real-server-card-minimal-product-pass + +## Goal +Dejar las tarjetas de servidores reales en una forma minima y orientada a producto final, mostrando solo la informacion esencial para un visitante que quiere ver estado actual y conectarse. + +## Context +Las tarjetas ya se simplificaron parcialmente, pero todavia quedaban restos de estructura y bloques secundarios que no aportaban suficiente valor. El objetivo era dejar una tarjeta muy clara y minima, centrada en estado actual del servidor. + +## Steps +1. Revisar la composicion actual de las tarjetas reales. +2. Asegurar que el contenido visible principal quede limitado a: + - nombre + - estado + - jugadores + - mapa + - region + - ultima actualizacion + - boton Conectar +3. Evaluar si promedio y pico deben quedar ocultos o pasar a una capa secundaria muy discreta. +4. Eliminar cualquier bloque residual que complique la lectura rapida. +5. Mejorar la jerarquia para que el usuario vea en pocos segundos si el servidor le interesa. +6. Mantener una composicion limpia y coherente con el ancho actual de la pagina. +7. No tocar backend salvo una alineacion menor si fuera imprescindible. + +## 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 romper el boton Conectar. +- No eliminar la informacion util esencial. +- No anadir librerias nuevas. +- No hacer cambios destructivos. +- Mantener el ajuste centrado en UX de producto final. + +## Validation +- Las tarjetas muestran solo informacion util y clara. +- La lectura es rapida y limpia. +- No quedan bloques residuales de informacion interna o excesiva. +- La UI se siente mas final y menos experimental. + +## Change Budget +- Preferir menos de 3 archivos modificados. +- Preferir menos de 140 lineas cambiadas. + +## Outcome +- `frontend/assets/js/main.js` deja las tarjetas dinamicas en su forma minima: identidad, estado, jugadores, mapa, region, ultima actualizacion y CTA cuando existe destino conectable. +- `frontend/index.html` alinea las tarjetas estaticas de fallback con esa misma estructura minima. +- `frontend/assets/css/styles.css` elimina estilos de bloques residuales ya innecesarios, como la barra de carga y los encabezados intermedios de seccion, para reforzar una lectura mas rapida. + +## Validation Result +- Validado con `node --check frontend/assets/js/main.js`. +- Verificado con `rg` que ya no quedan restos de `server-card__load`, `server-panel-section`, ni texto asociado a metricas o bloques historicos secundarios dentro de `frontend/`. +- Revisado en diff: la task queda limitada a `frontend/index.html`, `frontend/assets/js/main.js`, `frontend/assets/css/styles.css` y este archivo de task. + +## Decision Notes +- Se elimino la barra visual de ocupacion porque duplicaba la informacion ya expresada por `Jugadores` y cargaba la tarjeta sin mejorar la decision principal del visitante. diff --git a/ai/worker.lock b/ai/worker.lock index 0eb4ed5..6917c23 100644 --- a/ai/worker.lock +++ b/ai/worker.lock @@ -1 +1 @@ -39568 \ No newline at end of file +32960 \ No newline at end of file diff --git a/backend/README.md b/backend/README.md index 8e890ef..35c6a33 100644 --- a/backend/README.md +++ b/backend/README.md @@ -61,6 +61,11 @@ Variables opcionales: - `HLL_BACKEND_ALLOWED_ORIGINS` - `HLL_BACKEND_REFRESH_INTERVAL_SECONDS` +El `frontend/index.html` viene preparado para volver a consultar el bloque de +servidores cada `60000` ms (`60s`) sin recargar la pagina completa. La landing +lee ese valor desde `data-server-refresh-ms`, por lo que puede ajustarse en el +HTML si una demo local necesita un intervalo distinto. + Valor por defecto de `HLL_BACKEND_ALLOWED_ORIGINS`: - `null` @@ -239,7 +244,7 @@ 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 +- espera `60` segundos entre ejecuciones - permite fallback controlado si A2S no responde - sigue en ejecucion hasta que se detiene manualmente @@ -259,6 +264,25 @@ python -m app.scheduler --interval 120 python -m app.scheduler --source a2s --no-fallback --max-runs 2 ``` +Flujo local recomendado para ver datos vivos en la landing: + +1. Desde `backend/`, arrancar la API: + + ```powershell + python -m app.main + ``` + +2. En otra terminal, dejar el scheduler corriendo: + + ```powershell + python -m app.scheduler + ``` + +3. Servir `frontend/` con un servidor local sencillo y abrir la landing. El + frontend volvera a pedir `/api/servers` y `/api/servers/latest` cada `60` + segundos, por lo que los cambios de mapa o poblacion apareceran sin recarga + manual cuando existan snapshots nuevos. + Este mecanismo deja el refresco desacoplado del servidor HTTP y es facil de reemplazar mas adelante por un scheduler mas serio sin rehacer el colector. diff --git a/backend/app/config.py b/backend/app/config.py index 6fff5cf..e27ff2b 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -9,7 +9,7 @@ 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_REFRESH_INTERVAL_SECONDS = 60 DEFAULT_ALLOWED_ORIGINS = ( "null", "http://127.0.0.1:5500", diff --git a/backend/app/scheduler.py b/backend/app/scheduler.py index e4e4e26..249aa28 100644 --- a/backend/app/scheduler.py +++ b/backend/app/scheduler.py @@ -49,13 +49,13 @@ def run_local_refresh_loop( def main() -> None: """Allow local scheduled refresh execution without adding external infrastructure.""" parser = argparse.ArgumentParser( - description="Run periodic local snapshot refreshes for development.", + description="Run periodic local snapshot refreshes for development and landing demos.", ) parser.add_argument( "--interval", type=int, default=get_refresh_interval_seconds(), - help="Seconds to wait between persisted refresh runs.", + help="Seconds to wait between persisted refresh runs. Defaults to env value or 60.", ) parser.add_argument( "--source", diff --git a/frontend/assets/css/styles.css b/frontend/assets/css/styles.css index a46d65f..9924e07 100644 --- a/frontend/assets/css/styles.css +++ b/frontend/assets/css/styles.css @@ -14,9 +14,9 @@ --accent-warm: #d2b676; --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; + --page-shell-width: 1600px; + --page-shell-gutter: 32px; + --panel-content-width: 1120px; --font-main: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; } @@ -35,10 +35,12 @@ body { color: var(--text); background-color: var(--bg); background: - linear-gradient(rgba(9, 11, 8, 0.62), rgba(9, 11, 8, 0.96)), - radial-gradient(circle at top, rgba(84, 96, 59, 0.2), transparent 34%), - 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(rgba(9, 11, 8, 0.56), rgba(9, 11, 8, 0.97)), + radial-gradient(circle at 18% 12%, rgba(108, 124, 75, 0.2), transparent 28%), + radial-gradient(circle at top, rgba(84, 96, 59, 0.22), transparent 34%), + radial-gradient(circle at 85% 10%, rgba(210, 182, 118, 0.1), transparent 24%), + radial-gradient(circle at 78% 65%, rgba(54, 65, 42, 0.2), transparent 30%), + radial-gradient(circle at bottom, rgba(210, 182, 118, 0.08), transparent 28%), linear-gradient(145deg, #171d15 0%, var(--bg-deep) 100%); } @@ -53,20 +55,47 @@ a { } .page-shell { + position: relative; + isolation: isolate; width: 100%; - padding: 34px 20px 72px; + padding: 30px 20px 72px; +} + +.page-shell::before { + content: ""; + position: absolute; + inset: 0; + z-index: -2; + background: + radial-gradient(circle at 12% 18%, rgba(210, 182, 118, 0.07), transparent 18%), + radial-gradient(circle at 82% 9%, rgba(142, 160, 98, 0.14), transparent 24%), + linear-gradient(180deg, rgba(255, 255, 255, 0.012), transparent 24%); + pointer-events: none; +} + +.page-shell::after { + content: ""; + position: absolute; + inset: 18px 4% auto; + z-index: -1; + height: 220px; + border-radius: 999px; + background: radial-gradient(circle, rgba(0, 0, 0, 0.36), transparent 72%); + filter: blur(18px); + pointer-events: none; } .hero { position: relative; overflow: hidden; - width: min(100%, var(--hero-shell-width)); + width: min(var(--page-shell-width), calc(100vw - (var(--page-shell-gutter) * 2))); margin: 0 auto; border: 1px solid var(--border); border-radius: 24px; background: - 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%), + linear-gradient(180deg, rgba(26, 33, 21, 0.86), rgba(10, 13, 9, 0.98)), + radial-gradient(circle at top center, rgba(183, 201, 125, 0.08), transparent 32%), + radial-gradient(circle at 82% 22%, rgba(210, 182, 118, 0.08), transparent 24%), repeating-linear-gradient( 90deg, rgba(255, 255, 255, 0.015) 0, @@ -104,23 +133,37 @@ a { position: absolute; inset: 0; background: - radial-gradient(circle at top right, rgba(142, 160, 98, 0.16), transparent 28%), - radial-gradient(circle at left center, rgba(210, 182, 118, 0.08), transparent 26%), + radial-gradient(circle at top right, rgba(142, 160, 98, 0.18), transparent 28%), + radial-gradient(circle at left center, rgba(210, 182, 118, 0.1), transparent 26%), + radial-gradient(circle at 50% 115%, rgba(0, 0, 0, 0.42), transparent 34%), 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%); + linear-gradient(180deg, rgba(0, 0, 0, 0) 48%, rgba(0, 0, 0, 0.34) 100%); pointer-events: none; } .hero__content { position: relative; z-index: 1; - max-width: 860px; + width: 100%; + max-width: none; margin: 0 auto; - padding: 68px 32px 78px; + padding: 58px 52px 64px; +} + +.hero__brand { + position: relative; display: grid; - justify-items: center; - text-align: center; + grid-template-columns: minmax(240px, 340px) minmax(0, 1fr); + align-items: center; + gap: 36px; +} + +.hero__copy { + display: grid; + justify-items: start; + text-align: left; gap: 18px; + max-width: 660px; } .hero__content::before { @@ -140,9 +183,9 @@ a { } .logo-frame { - width: min(336px, 78vw); - min-height: 220px; - padding: 20px 24px; + width: min(340px, 100%); + min-height: 248px; + padding: 24px 28px; display: grid; place-items: center; border: 1px dashed rgba(183, 201, 125, 0.45); @@ -180,7 +223,7 @@ a { width: auto; height: auto; max-width: 100%; - max-height: 220px; + max-height: 246px; object-fit: contain; filter: drop-shadow(0 10px 20px rgba(0, 0, 0, 0.28)) @@ -210,8 +253,8 @@ h2 { } h1 { - max-width: 12ch; - font-size: clamp(2.2rem, 5vw, 4.6rem); + max-width: 10ch; + font-size: clamp(2.6rem, 5vw, 4.9rem); letter-spacing: 0.02em; text-shadow: 0 10px 28px rgba(0, 0, 0, 0.32); } @@ -226,26 +269,30 @@ h2 { } .hero__text { - max-width: 640px; margin: 0; + max-width: 56ch; color: var(--text-soft); - font-size: 1.06rem; - line-height: 1.85; + font-size: 1.03rem; + line-height: 1.75; } .hero__actions { - display: grid; - justify-items: center; - gap: 14px; - margin-top: 4px; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 14px 16px; + margin-top: 2px; } .status-chip { margin: 0; - padding: 0.45rem 0.85rem; + padding: 0.5rem 0.9rem; border: 1px solid rgba(159, 168, 141, 0.22); border-radius: 999px; - background: rgba(21, 26, 19, 0.72); + background: linear-gradient(180deg, rgba(28, 34, 26, 0.82), rgba(14, 18, 13, 0.86)); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.04), + 0 10px 24px rgba(0, 0, 0, 0.18); font-size: 0.76rem; letter-spacing: 0.12em; text-transform: uppercase; @@ -294,22 +341,25 @@ h2 { .content { width: 100%; - margin-top: -34px; + margin-top: -28px; position: relative; z-index: 2; display: grid; - gap: 24px; + gap: 30px; } .panel { position: relative; - width: 100%; - padding: 32px; + width: min(var(--page-shell-width), calc(100vw - (var(--page-shell-gutter) * 2))); + margin: 0 auto; + padding: 34px 40px 38px; border: 1px solid var(--border); border-radius: 24px; background: - linear-gradient(180deg, rgba(23, 28, 21, 0.96), rgba(13, 16, 12, 0.99)); - box-shadow: var(--shadow); + linear-gradient(180deg, rgba(25, 31, 23, 0.96), rgba(12, 15, 11, 0.995)); + box-shadow: + 0 34px 82px rgba(0, 0, 0, 0.34), + inset 0 1px 0 rgba(255, 255, 255, 0.03); } .panel::before { @@ -321,10 +371,26 @@ h2 { background: linear-gradient(90deg, rgba(210, 182, 118, 0.7), transparent); } +.panel::after { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + background: + radial-gradient(circle at top center, rgba(183, 201, 125, 0.07), transparent 30%), + linear-gradient(180deg, rgba(255, 255, 255, 0.015), transparent 18%); + pointer-events: none; +} + .panel__header { margin-bottom: 22px; } +.panel__shell { + width: 100%; + margin: 0 auto; +} + .panel__header--servers { display: flex; align-items: flex-start; @@ -344,99 +410,11 @@ h2 { 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 { position: relative; width: 100%; @@ -482,55 +460,22 @@ h2 { .servers-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); - gap: 20px; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 360px), 1fr)); + gap: 24px; } .servers-grid--section { - grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr)); + grid-template-columns: repeat(auto-fit, minmax(min(100%, 360px), 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 { + position: relative; + overflow: hidden; min-width: 0; - padding: 20px; + padding: 22px; border: 1px solid rgba(159, 168, 141, 0.18); - border-radius: 20px; + border-radius: 22px; background: linear-gradient(180deg, rgba(28, 34, 25, 0.94), rgba(15, 18, 13, 0.98)); box-shadow: @@ -538,6 +483,24 @@ h2 { var(--shadow-soft); } +.server-card::before { + content: ""; + position: absolute; + inset: 0 0 auto; + height: 1px; + background: linear-gradient(90deg, rgba(210, 182, 118, 0.4), transparent 68%); + pointer-events: none; +} + +.server-card::after { + content: ""; + position: absolute; + inset: 0; + background: + radial-gradient(circle at top right, rgba(183, 201, 125, 0.06), transparent 28%); + pointer-events: none; +} + .server-card--real { border-color: rgba(183, 201, 125, 0.24); background: @@ -572,7 +535,7 @@ h2 { .server-card__eyebrow { margin: 0; color: var(--accent-warm); - font-size: 0.72rem; + font-size: 0.68rem; letter-spacing: 0.14em; text-transform: uppercase; } @@ -585,14 +548,6 @@ h2 { 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; @@ -606,14 +561,36 @@ h2 { padding: 10px 12px; max-width: 100%; min-width: 120px; - border: 1px solid rgba(159, 168, 141, 0.16); + border: 1px solid rgba(159, 168, 141, 0.18); border-radius: 14px; - background: rgba(10, 12, 9, 0.3); + background: linear-gradient(180deg, rgba(12, 15, 10, 0.54), rgba(9, 11, 8, 0.34)); color: var(--accent-strong); font-size: 1rem; font-weight: 700; letter-spacing: 0.04em; text-align: center; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03); +} + +.server-card__load { + width: 100%; + height: 9px; + margin-bottom: 18px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.05); + overflow: hidden; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.32); +} + +.server-card__load span { + display: block; + height: 100%; + border-radius: inherit; + background: + linear-gradient(90deg, rgba(210, 182, 118, 0.86), rgba(142, 160, 98, 0.96)); + box-shadow: + 0 0 18px rgba(142, 160, 98, 0.22), + inset 0 1px 0 rgba(255, 255, 255, 0.18); } .server-card__actions { @@ -658,13 +635,14 @@ h2 { align-items: center; justify-content: center; min-width: 88px; - padding: 0.35rem 0.65rem; + padding: 0.38rem 0.72rem; 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; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03); } .server-state--online { @@ -679,34 +657,18 @@ h2 { 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; + gap: 12px; } .server-stat { - padding-top: 14px; - border-top: 1px solid rgba(159, 168, 141, 0.12); + padding: 14px 14px 15px; + border: 1px solid rgba(159, 168, 141, 0.14); + border-radius: 16px; + background: linear-gradient(180deg, rgba(12, 15, 11, 0.38), rgba(8, 10, 7, 0.18)); } .server-stat--players dd { @@ -727,20 +689,6 @@ h2 { 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; @@ -751,9 +699,9 @@ h2 { .server-card__quickfact { min-width: 0; padding: 12px 14px; - border: 1px solid rgba(159, 168, 141, 0.12); + border: 1px solid rgba(159, 168, 141, 0.14); border-radius: 14px; - background: rgba(10, 12, 9, 0.22); + background: linear-gradient(180deg, rgba(12, 15, 11, 0.34), rgba(8, 10, 7, 0.2)); } .server-card__quickfact p, @@ -777,96 +725,6 @@ h2 { 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; @@ -877,12 +735,23 @@ h2 { } @media (max-width: 1120px) { - .servers-grid--section { - grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr)); + .hero__brand { + grid-template-columns: 1fr; + justify-items: center; + gap: 24px; } - .server-card__body { - grid-template-columns: 1fr; + .hero__copy { + justify-items: center; + text-align: center; + } + + .hero__actions { + justify-content: center; + } + + .servers-grid--section { + grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); } } @@ -893,25 +762,17 @@ h2 { } } -@media (max-width: 960px) { - .server-card__body { - grid-template-columns: 1fr; - } -} - @media (max-width: 640px) { .page-shell { padding: 18px 14px 40px; } - .hero__content, - .panel { - padding: 24px 16px; + .hero__content { + padding: 42px 18px 48px; } - .hero__content { - padding-top: 44px; - padding-bottom: 52px; + .panel { + padding: 24px 16px; } .hero__content::before { @@ -932,6 +793,16 @@ h2 { max-height: 180px; } + h1 { + max-width: 11ch; + font-size: clamp(2.2rem, 10vw, 3.2rem); + } + + .hero__actions { + width: 100%; + justify-content: center; + } + .discord-button { width: 100%; } @@ -973,14 +844,6 @@ h2 { 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; } diff --git a/frontend/assets/js/main.js b/frontend/assets/js/main.js index 3be382d..bbe581f 100644 --- a/frontend/assets/js/main.js +++ b/frontend/assets/js/main.js @@ -1,11 +1,15 @@ // Progressive enhancement for local frontend-backend checks. const RECENT_SNAPSHOT_WINDOW_MS = 30 * 60 * 1000; +const DEFAULT_SERVER_POLL_INTERVAL_MS = 60 * 1000; document.addEventListener("DOMContentLoaded", () => { console.info("HLL Vietnam frontend ready"); const backendBaseUrl = document.body.dataset.backendBaseUrl || "http://127.0.0.1:8000"; + const serverPollIntervalMs = getServerPollIntervalMs( + document.body.dataset.serverRefreshMs, + ); const statusNode = document.getElementById("backend-status"); const trailerFrame = document.getElementById("trailer-frame"); const trailerTitle = document.getElementById("trailer-title"); @@ -13,42 +17,44 @@ document.addEventListener("DOMContentLoaded", () => { 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" }, - ); + setServersDataState(serversBadge, { kind: "fallback" }); + + let serverRefreshInFlight = false; + const refreshServers = async () => { + if (serverRefreshInFlight) { + return; + } + + serverRefreshInFlight = true; + try { + await hydrateServers( + backendBaseUrl, + serversTitle, + serversNote, + serversList, + serversBadge, + ); + } finally { + serverRefreshInFlight = false; + } + }; Promise.allSettled([ fetchHealth(backendBaseUrl, statusNode), hydrateTrailer(backendBaseUrl, trailerFrame, trailerTitle), - hydrateServers( - backendBaseUrl, - serversTitle, - serversNote, - serversList, - serversBadge, - serversSource, - serversSourceLabel, - serversSourceMeta, - statsPreview, - statsPreviewItems, - ), + refreshServers(), ]).catch((error) => { console.warn("Progressive enhancement failed", error); }); + + if (serverPollIntervalMs > 0) { + window.setInterval(() => { + void refreshServers(); + }, serverPollIntervalMs); + } }); async function fetchHealth(backendBaseUrl, statusNode) { @@ -102,11 +108,6 @@ async function hydrateServers( serversNote, serversList, serversBadge, - serversSource, - serversSourceLabel, - serversSourceMeta, - statsPreview, - statsPreviewItems, ) { if (!serversTitle || !serversNote || !serversList || !serversBadge) { return; @@ -125,20 +126,12 @@ async function hydrateServers( } serversTitle.textContent = - serversData.title || "Servidores actuales de Hell Let Loose"; - setServersDataState( - { - badgeNode: serversBadge, - sourceNode: serversSource, - labelNode: serversSourceLabel, - metaNode: serversSourceMeta, - }, - { kind: "fallback" }, - ); + serversData.title || "Estado actual de servidores"; + setServersDataState(serversBadge, { kind: "fallback" }); if (serversData.context === "current-hll-reference") { serversNote.textContent = - "Referencia provisional del HLL actual mientras no existan datos reales de HLL Vietnam."; + "Referencia actual de servidores de Hell Let Loose."; } if (serversData.items.length === 0) { @@ -154,11 +147,6 @@ async function hydrateServers( serversNote, serversList, serversBadge, - serversSource, - serversSourceLabel, - serversSourceMeta, - statsPreview, - statsPreviewItems, ); } catch (error) { console.warn("Servers panel remains on static fallback", error); @@ -171,16 +159,7 @@ async function hydrateServerStats( 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; @@ -188,48 +167,20 @@ async function hydrateServerStats( 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" + ? "Servidores disponibles" : 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); + ? "Estado actual y acceso directo a los servidores disponibles." + : "Ultimo estado disponible de servidores."; + setServersDataState(serversBadge, latestState); + serversList.innerHTML = renderServerSections(visibleItems); } catch (error) { - console.warn("Historical stats preview unavailable", error); + console.warn("Historical server enrichment unavailable", error); } } @@ -245,45 +196,28 @@ function updateBackendStatus(statusNode, label, stateClass) { } } -function setServersDataState(nodes, state) { - const { badgeNode, sourceNode, labelNode, metaNode } = nodes; - if (!badgeNode || !sourceNode || !labelNode || !metaNode) { +function setServersDataState(badgeNode, state) { + if (!badgeNode) { 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.textContent = "Datos en vivo"; 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.textContent = "Actividad 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.textContent = "Referencia actual"; badgeNode.classList.remove("status-chip--ok"); badgeNode.classList.add("status-chip--fallback"); - sourceNode.dataset.state = "fallback"; } function renderServerCard(server) { @@ -296,20 +230,23 @@ function renderServerCard(server) { 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 occupancy = getPopulationPercent(players, maxPlayers); + const sourceLabel = isRealA2SSnapshot(server) ? "Snapshot activo" : "Referencia actual"; return `
-

Servidor de referencia

+

${escapeHtml(sourceLabel)}

${escapeHtml(serverName)}

- ${escapeHtml(serverStatus)} +
+ ${escapeHtml(serverStatus)} +

${players} / ${maxPlayers}

+
@@ -329,7 +266,7 @@ function renderServerCard(server) { `; } -function renderServerStatsCard(server, historyItems) { +function renderServerStatsCard(server) { const serverName = server.server_name || "Servidor sin nombre"; const statusLabel = formatServerStatus(server.status); const stateClass = @@ -339,17 +276,13 @@ function renderServerStatsCard(server, historyItems) { 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 occupancy = getPopulationPercent(players, maxPlayers); 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 }, @@ -360,9 +293,8 @@ function renderServerStatsCard(server, historyItems) {
-

${escapeHtml(eyebrowLabel)}

+

${isRealSnapshot ? "Snapshot activo" : "Referencia actual"}

${escapeHtml(serverName)}

-

Mapa actual, region visible y ultima captura persistida del servidor.

${escapeHtml(statusLabel)} @@ -370,60 +302,19 @@ function renderServerStatsCard(server, historyItems) { ${connectAction}
-
-
- ${quickFacts} - ${summaryMarkup} -
-
-
-

Tendencia reciente

- ${historyItems.length} capturas -
- -
-
+ ${quickFacts}
`; } -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(""); - +function renderServerSections(latestItems) { return ` -
-
-

${escapeHtml(title)}

-

${escapeHtml(intro)}

-
-
- ${cards} -
-
+
+ ${latestItems.map((server) => renderServerStatsCard(server)).join("")} +
`; } @@ -446,56 +337,6 @@ function renderConnectAction(server) { `; } -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 ` -
-
-
Visto online
-
${escapeHtml(summary.last_seen_online_at ? formatTimestamp(summary.last_seen_online_at) : "Sin registro")}
-
-
-
Capturas
-
${escapeHtml(`${summary.recent_capture_count || 0}/${summary.window_size || 0}`)}
-
-
-
Promedio
-
${escapeHtml(formatPlayerMetric(summary.recent_average_players))}
-
-
-
Pico
-
${escapeHtml(formatPlayerMetric(summary.recent_peak_players))}
-
-
- `; -} - function renderQuickFacts(items) { return `
@@ -547,32 +388,6 @@ function deriveSnapshotState(items) { return { kind: "historical", timestampLabel }; } -function renderStatsPreviewItem(label, value) { - return ` -
-

${escapeHtml(label)}

- ${escapeHtml(value)} -
- `; -} - -function renderTrend(historyItems) { - if (!Array.isArray(historyItems) || historyItems.length === 0) { - return ''; - } - - 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 ``; - }) - .join(""); -} - function formatServerStatus(status) { if (status === "online") { return "Online"; @@ -619,8 +434,21 @@ function formatElapsedMinutes(minutes) { return `hace ${days} d`; } -function formatPlayerMetric(value) { - return Number.isFinite(value) ? String(value) : "Sin dato"; +function getPopulationPercent(players, maxPlayers) { + if (!Number.isFinite(players) || !Number.isFinite(maxPlayers) || maxPlayers <= 0) { + return 0; + } + + return Math.max(0, Math.min(100, Math.round((players / maxPlayers) * 100))); +} + +function getServerPollIntervalMs(rawValue) { + const parsedValue = Number(rawValue); + if (!Number.isFinite(parsedValue) || parsedValue <= 0) { + return DEFAULT_SERVER_POLL_INTERVAL_MS; + } + + return parsedValue; } async function fetchJson(url) { diff --git a/frontend/index.html b/frontend/index.html index 6fe705f..23d4ec3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -10,154 +10,152 @@ Comunidad Hispana - HLL Vietnam - +
-
- Logo oficial de la comunidad HLL Vietnam -
-

Comunidad táctica hispana

-

- Comunidad Hispana - HLL Vietnam -

-

- Punto de encuentro para jugadores, escuadras y comunidad alrededor - del futuro universo HLL Vietnam. -

-
-

- Backend no verificado -

- - Unirse al Discord - +
+
+ Logo oficial de la comunidad HLL Vietnam +
+
+

Comunidad táctica hispana

+

+ Comunidad Hispana + HLL Vietnam +

+

+ Centro de reunión para escuadras, eventos y acceso directo al + Discord oficial de la comunidad. +

+
+

+ Backend no verificado +

+ + Unirse al Discord + +
+
-
-

Trailer

-

Primer vistazo a HLL Vietnam

-

- El bloque principal se apoya en una presentacion limpia del - trailer para reforzar el tono tactico y cinematografico del - proyecto. -

-
-
- +
+
+

Trailer

+

Primer vistazo a HLL Vietnam

+
+
+ +
-
-
-

Actividad de servidores

-

Servidores actuales de Hell Let Loose

+
+
+
+

Servidores

+

Estado actual de servidores

+
+

+ Referencia actual +

-

- Fallback estatico +

+ Estado actual y acceso directo a los servidores disponibles.

-
-

- El panel prioriza snapshots reales capturados por el backend y - conserva un fallback provisional solo cuando no hay datos utilizables. -

-
-

- Fallback estatico activo -

-

- La landing conserva la referencia provisional cuando el backend o - el historico aun no aportan snapshots utilizables. -

-
- -
-
-
-
-

Servidor de referencia

-

HLL ESP Tactical Rotation

+
+
+
+
+

Referencia actual

+

HLL ESP Tactical Rotation

+
+
+ Online +

74 / 100

+
- Online -
- -
-
-
Jugadores
-
74 / 100
+ -
-
Mapa
-
Sainte-Marie-du-Mont
+
+
+
Jugadores
+
74 / 100
+
+
+
Mapa
+
Sainte-Marie-du-Mont
+
+
+
Region
+
EU
+
+
+
+
+
+
+

Referencia actual

+

HLL LATAM Night Offensive

+
+
+ Online +

51 / 100

+
-
-
Region
-
EU
+ -
-
-
-
-
-

Servidor de referencia

-

HLL LATAM Night Offensive

-
- Online -
- -
-
-
Jugadores
-
51 / 100
-
-
-
Mapa
-
Carentan
-
-
-
Region
-
LATAM
-
-
-
+
+
+
Jugadores
+
51 / 100
+
+
+
Mapa
+
Carentan
+
+
+
Region
+
LATAM
+
+
+ +