Polish public page navigation and copy

This commit is contained in:
devRaGonSa
2026-06-09 13:05:33 +02:00
parent d9371d6912
commit b64d4e180f
8 changed files with 381 additions and 131 deletions

View File

@@ -0,0 +1,139 @@
---
id: TASK-203
title: Public pages navigation and copy polish
status: done
type: frontend
team: Frontend Senior
supporting_teams:
- Experto en interfaz
roadmap_item: foundation
priority: medium
---
# TASK-203 - Public pages navigation and copy polish
## Goal
Reorganizar la navegacion superior y normalizar el layout visual de `index`, `historico`, `stats` y `ranking` para que compartan una navegacion publica coherente, una cabecera visual alineada y textos visibles corregidos en UTF-8.
## Context
Las paginas publicas principales ya exponen el producto, pero hoy mezclan botones sueltos dentro del hero, encabezados desalineados y varios textos mal codificados o demasiado tecnicos para usuarios normales. Esta tarea corrige solo la capa visual y de copy en `frontend/`, sin tocar backend, endpoints, logica de datos, Elo/MMR pausado, Comunidad Hispana #03 ni assets/SVGs de armas.
Preserve the current product identity: Spanish-speaking HLL Vietnam community, military/Vietnam/tactical/sober visual direction and controlled repository evolution.
## Steps
1. Inspeccionar el HTML, CSS y JS compartidos de las paginas publicas afectadas.
2. Crear o reutilizar una navegacion superior global para `index`, `historico`, `stats` y `ranking`.
3. Alinear el hero de `stats` y `ranking` con la composicion visual usada por `index` y `historico`.
4. Limpiar botones duplicados dentro del hero y simplificar el copy visible sin cambiar logica de datos.
5. Corregir textos visibles con problemas de codificacion en las paginas afectadas y validar el resultado por inspeccion.
## Files to Read First
- `AGENTS.md`
- `ai/repo-context.md`
- `ai/architecture-index.md`
- `frontend/index.html`
- `frontend/historico.html`
- `frontend/assets/css/styles.css`
## Expected Files to Modify
- `ai/tasks/done/TASK-203-public-pages-navigation-and-copy-polish.md`
- `frontend/index.html`
- `frontend/historico.html`
- `frontend/stats.html`
- `frontend/ranking.html`
- `frontend/assets/css/styles.css`
- `frontend/assets/js/stats.js`
- `frontend/assets/js/ranking.js`
## Constraints
- Keep the change minimal.
- Preserve HLL Vietnam project identity.
- Do not introduce unnecessary frameworks or dependencies.
- Do not implement backend functionality unless the task explicitly requires it.
- Do not expand Elo/MMR, historical workers or RCON server #03 handling unless the task explicitly requires it.
- Do not overwrite repository-specific context with generic platform template text.
- No ejecutar `ai-platform run`.
- No modificar `backend/`, scripts de validacion backend, endpoints, logica de datos, assets/SVGs de armas ni siluetas.
- No reactivar Elo/MMR ni reintroducir Comunidad Hispana #03.
## Validation
Before completing the task ensure:
- `index.html`, `historico.html`, `stats.html` y `ranking.html` muestran navegacion superior con acceso a Inicio, Historico, Stats y Ranking
- `index.html` mantiene `Unirse al Discord` dentro del hero
- `stats.html` y `ranking.html` muestran imagen/icono de comunidad en hero
- `stats.html` y `ranking.html` ya no muestran `Backend operativo`
- no aparecen textos mal codificados visibles como `Público`, `Públicos`, `Año`, `AÃo`, `Histórico`
- `ranking.html` ya no muestra `Fuente` en el panel visible de tabla activa
- `git diff --name-only` matches the expected scope
- no unrelated files were modified
- integration tests are run when relevant and configured
## Outcome
Archivos modificados:
- `frontend/index.html`
- `frontend/historico.html`
- `frontend/stats.html`
- `frontend/ranking.html`
- `frontend/assets/css/styles.css`
- `frontend/assets/js/stats.js`
- `frontend/assets/js/ranking.js`
- `ai/tasks/done/TASK-203-public-pages-navigation-and-copy-polish.md`
Navegacion nueva:
- Se anadio una navegacion superior comun en las cuatro paginas publicas con acceso directo a Inicio, Historico, Stats y Ranking.
- El estado activo se marca por pagina y la navegacion queda separada del hero como banda superior de tabs tacticos.
Textos corregidos:
- Se corrigieron textos visibles relacionados con Historico, publico/publicos, estadisticas, metrica, limite y ano/anual.
- En HTML se usaron entidades para asegurar render correcto en entorno Windows y lectura segura en navegador.
- En JS visible se usaron escapes Unicode donde hacia falta para evitar mojibake.
Botones movidos:
- En `index`, `Stats`, `Ranking` y `Ver historico propio` salieron del hero y pasaron a la navegacion global.
- En `historico`, `Volver inicio` y `Stats` se sustituyeron por la navegacion global, y se anadio acceso directo a Ranking.
- En `stats` y `ranking`, los enlaces de retorno cruzado del hero se sustituyeron por la navegacion global.
Elementos retirados:
- Se elimino el badge visible de backend en `stats` y `ranking`.
- Se elimino el campo visual `Fuente` del panel visible de metadata en `ranking`.
Validaciones ejecutadas:
- Revision por inspeccion de `frontend/index.html`, `frontend/historico.html`, `frontend/stats.html` y `frontend/ranking.html`.
- Verificacion de navegacion superior comun en las cuatro paginas.
- Verificacion de que `Unirse al Discord` permanece en el hero de `index`.
- Verificacion de que `stats` y `ranking` incluyen `logo.png` en el hero.
- Verificacion de que ya no existen `id="stats-backend-state"` ni `id="ranking-backend-state"` en HTML.
- Verificacion de que `ranking.js` ya no genera la tarjeta visible `Fuente`.
- Revision de `git status --short --untracked-files=all` y `git diff --name-only`.
- Confirmacion de que no hay cambios en `backend/` ni en `scripts/`.
Tests:
- No se ejecutaron scripts de validacion frontend existentes porque los scripts disponibles no son ligeros para este alcance y uno de ellos (`scripts/run-stats-validation.ps1`) exige el chip `stats-backend-state`, eliminado intencionadamente por esta task. No se modifico ese script por estar fuera de alcance.
Confirmaciones de alcance:
- No se tocaron `backend/`, endpoints, logica de datos, scripts backend, Elo/MMR ni Comunidad Hispana #03.
- No se tocaron assets/SVGs de armas. Los cambios ya presentes en `frontend/assets/img/weapons/` y `ai/system-metrics.md` eran previos y se dejaron intactos.
## Change Budget
- Prefer fewer than 5 modified files.
- Prefer changes under 200 lines when feasible.
- Split the work into follow-up tasks if limits are exceeded.

View File

@@ -1,4 +1,4 @@
:root { :root {
--bg: #0f120d; --bg: #0f120d;
--bg-deep: #090b08; --bg-deep: #090b08;
--bg-elevated: rgba(27, 33, 24, 0.92); --bg-elevated: rgba(27, 33, 24, 0.92);
@@ -85,6 +85,58 @@ a {
pointer-events: none; pointer-events: none;
} }
.public-nav {
width: min(var(--page-shell-width), calc(100vw - (var(--page-shell-gutter) * 2)));
margin: 0 auto 16px;
padding: 10px;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
border: 1px solid var(--border);
border-radius: 22px;
background:
linear-gradient(180deg, rgba(24, 30, 22, 0.9), rgba(10, 13, 9, 0.96)),
radial-gradient(circle at top center, rgba(183, 201, 125, 0.08), transparent 40%);
box-shadow: var(--shadow-soft);
}
.public-nav__link {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 50px;
padding: 0 18px;
border: 1px solid rgba(159, 168, 141, 0.22);
border-radius: 999px;
background: linear-gradient(180deg, rgba(18, 23, 16, 0.88), rgba(10, 12, 9, 0.96));
color: var(--text-soft);
font-size: 0.82rem;
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
transition:
transform 160ms ease,
border-color 160ms ease,
background 160ms ease,
color 160ms ease,
box-shadow 160ms ease;
}
.public-nav__link:hover,
.public-nav__link:focus-visible {
transform: translateY(-1px);
border-color: rgba(210, 182, 118, 0.48);
background: linear-gradient(180deg, rgba(43, 50, 36, 0.94), rgba(16, 20, 14, 0.98));
color: var(--text);
}
.public-nav__link.is-active {
border-color: rgba(183, 201, 125, 0.42);
background: linear-gradient(180deg, rgba(183, 201, 125, 0.22), rgba(89, 101, 58, 0.28));
color: var(--text);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.hero { .hero {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
@@ -920,6 +972,11 @@ h2 {
margin-top: 8px; margin-top: 8px;
} }
.stats-hero .hero__brand,
.ranking-hero .hero__brand {
align-items: center;
}
.stats-search-form { .stats-search-form {
margin: 0 0 14px; margin: 0 0 14px;
display: grid; display: grid;
@@ -1401,6 +1458,10 @@ h2 {
} }
@media (max-width: 760px) { @media (max-width: 760px) {
.public-nav {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.servers-grid, .servers-grid,
.servers-grid--section, .servers-grid--section,
.clans-grid { .clans-grid {
@@ -1430,6 +1491,18 @@ h2 {
font-size: 0.98rem; font-size: 0.98rem;
} }
.public-nav {
padding: 8px;
gap: 8px;
}
.public-nav__link {
min-height: 46px;
padding-inline: 12px;
font-size: 0.74rem;
letter-spacing: 0.08em;
}
.stats-search-form__fields { .stats-search-form__fields {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -1475,7 +1548,8 @@ h2 {
width: 100%; width: 100%;
} }
.secondary-button { .secondary-button,
.public-nav__link {
width: 100%; width: 100%;
} }
@@ -1541,3 +1615,4 @@ h2 {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }

View File

@@ -1,4 +1,4 @@
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const backendBaseUrl = document.body.dataset.backendBaseUrl || "http://127.0.0.1:8000"; const backendBaseUrl = document.body.dataset.backendBaseUrl || "http://127.0.0.1:8000";
const form = document.getElementById("ranking-form"); const form = document.getElementById("ranking-form");
const timeframeSelect = document.getElementById("ranking-timeframe"); const timeframeSelect = document.getElementById("ranking-timeframe");
@@ -185,7 +185,7 @@ document.addEventListener("DOMContentLoaded", () => {
if (isAnnual) { if (isAnnual) {
setFilterNote( setFilterNote(
"El ranking anual sigue limitado a kills porque solo esa metrica tiene lectura snapshot segura.", "El ranking anual sigue limitado a kills porque solo esa m\u00e9trica tiene lectura snapshot segura.",
"warning", "warning",
); );
return; return;
@@ -272,7 +272,7 @@ document.addEventListener("DOMContentLoaded", () => {
if (!isBackendOnline) { if (!isBackendOnline) {
setRankingState("error", "Backend no disponible. El ranking queda en estado offline."); setRankingState("error", "Backend no disponible. El ranking queda en estado offline.");
renderEmptyState( renderEmptyState(
"No fue posible contactar el backend. Reintenta cuando el servicio vuelva a estar operativo.", "No fue posible contactar el backend. Reintenta cuando el servicio vuelva a estar disponible.",
); );
return; return;
} }
@@ -281,8 +281,8 @@ document.addEventListener("DOMContentLoaded", () => {
if (timeframe === "annual") { if (timeframe === "annual") {
year = Number.parseInt(String(yearInput?.value || "").trim(), 10); year = Number.parseInt(String(yearInput?.value || "").trim(), 10);
if (!Number.isFinite(year) || year <= 0) { if (!Number.isFinite(year) || year <= 0) {
setRankingState("error", "El ano solicitado no es valido."); setRankingState("error", "El a\u00f1o solicitado no es v\u00e1lido.");
renderEmptyState("Corrige el ano y vuelve a consultar el ranking anual."); renderEmptyState("Corrige el a\u00f1o y vuelve a consultar el ranking anual.");
return; return;
} }
} }
@@ -322,7 +322,7 @@ document.addEventListener("DOMContentLoaded", () => {
setBackendState("Backend no disponible", false); setBackendState("Backend no disponible", false);
setRankingState("error", "Error controlado al cargar el ranking."); setRankingState("error", "Error controlado al cargar el ranking.");
renderEmptyState( renderEmptyState(
"La lectura del ranking fallo en este intento. Revisa el backend o actualiza la pagina.", "La lectura del ranking fall\u00f3 en este intento. Revisa el backend o actualiza la p\u00e1gina.",
); );
} }
} }
@@ -343,20 +343,20 @@ document.addEventListener("DOMContentLoaded", () => {
) { ) {
setRankingState("warning", "El ranking anual solo admite kills por ahora."); setRankingState("warning", "El ranking anual solo admite kills por ahora.");
renderEmptyState( renderEmptyState(
"Las metricas extra estan disponibles en semanal y mensual. El ranking anual sigue limitado a kills mientras no existan snapshots seguros adicionales.", "Las m\u00e9tricas extra est\u00e1n disponibles en semanal y mensual. El ranking anual sigue limitado a kills mientras no existan snapshots seguros adicionales.",
); );
return; return;
} }
if (statusCode === 400 && normalizedMessage.includes("metric")) { if (statusCode === 400 && normalizedMessage.includes("metric")) {
setRankingState("warning", "La metrica solicitada no esta soportada."); setRankingState("warning", "La m\u00e9trica solicitada no est\u00e1 soportada.");
renderEmptyState( renderEmptyState(
"Usa kills, deaths, teamkills, partidas jugadas, K/D o kills por partida.", "Usa kills, deaths, teamkills, partidas jugadas, K/D o kills por partida.",
); );
return; return;
} }
if (statusCode === 400 && normalizedMessage.includes("year")) { if (statusCode === 400 && normalizedMessage.includes("year")) {
setRankingState("warning", "El ranking anual requiere un ano valido."); setRankingState("warning", "El ranking anual requiere un a\u00f1o v\u00e1lido.");
renderEmptyState("Define un ano valido para consultar la lectura anual."); renderEmptyState("Define un a\u00f1o v\u00e1lido para consultar la lectura anual.");
return; return;
} }
if (statusCode === 400 && normalizedMessage.includes("timeframe")) { if (statusCode === 400 && normalizedMessage.includes("timeframe")) {
@@ -398,7 +398,7 @@ document.addEventListener("DOMContentLoaded", () => {
if (timeframe === "annual" && snapshotStatus === "missing") { if (timeframe === "annual" && snapshotStatus === "missing") {
setRankingState("warning", "El snapshot anual solicitado aun no fue generado."); setRankingState("warning", "El snapshot anual solicitado aun no fue generado.");
renderEmptyState( renderEmptyState(
"No existe snapshot anual para el ano y servidor elegidos. Este estado es informativo y no implica caida del backend.", "No existe snapshot anual para el a\u00f1o y servidor elegidos. Este estado es informativo y no implica ca\u00edda del backend.",
); );
return; return;
} }
@@ -441,10 +441,9 @@ document.addEventListener("DOMContentLoaded", () => {
const cards = [ const cards = [
{ label: "Periodo activo", value: labelForTimeframe(timeframe), active: true }, { label: "Periodo activo", value: labelForTimeframe(timeframe), active: true },
{ label: "Servidor activo", value: labelForServer(data.server_id), active: true }, { label: "Servidor activo", value: labelForServer(data.server_id), active: true },
{ label: "Metrica activa", value: labelForMetric(metric), active: true }, { label: "M\u00e9trica activa", value: labelForMetric(metric), active: true },
{ label: "Limite", value: `Top ${safeInt(data.limit, safeInt(defaultLimit, 20))}` }, { label: "L\u00edmite", value: `Top ${safeInt(data.limit, safeInt(defaultLimit, 20))}` },
{ label: "Ventana", value: labelForWindow(data) }, { label: "Ventana", value: labelForWindow(data) },
{ label: "Fuente", value: String(source.read_model || "No disponible") },
{ label: "Actualizado", value: formatDateTime(source.generated_at) }, { label: "Actualizado", value: formatDateTime(source.generated_at) },
]; ];
@@ -490,7 +489,7 @@ document.addEventListener("DOMContentLoaded", () => {
function labelForWindow(data) { function labelForWindow(data) {
if (String(data.timeframe || "") === "annual") { if (String(data.timeframe || "") === "annual") {
return `Ano ${safeInt(data.year, currentYear)}`; return `A\u00f1o ${safeInt(data.year, currentYear)}`;
} }
return String(data.window_label || data.window_kind || "Ventana activa"); return String(data.window_label || data.window_kind || "Ventana activa");
} }
@@ -586,3 +585,4 @@ document.addEventListener("DOMContentLoaded", () => {
.replaceAll("'", "&#39;"); .replaceAll("'", "&#39;");
} }
}); });

View File

@@ -1,4 +1,4 @@
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const backendBaseUrl = document.body.dataset.backendBaseUrl || "http://127.0.0.1:8000"; const backendBaseUrl = document.body.dataset.backendBaseUrl || "http://127.0.0.1:8000";
const searchForm = document.getElementById("stats-search-form"); const searchForm = document.getElementById("stats-search-form");
const searchInput = document.getElementById("stats-search-input"); const searchInput = document.getElementById("stats-search-input");
@@ -39,22 +39,22 @@ document.addEventListener("DOMContentLoaded", () => {
"Error al buscar jugadores. Verifica backend y reintenta.", "Error al buscar jugadores. Verifica backend y reintenta.",
searchReadyPrefix: "Se encontraron ", searchReadyPrefix: "Se encontraron ",
searchReadySuffix: searchReadySuffix:
" jugador(es). Selecciona uno para ver sus estadisticas.", " jugador(es). Selecciona uno para ver sus estad\u00edsticas.",
searchShortQueryHelp: searchShortQueryHelp:
"Usa al menos 1 caracter para iniciar la busqueda.", "Usa al menos 1 car\u00e1cter para iniciar la b\u00fasqueda.",
profileLoading: "Cargando estadisticas personales...", profileLoading: "Cargando estad\u00edsticas personales...",
profileNoStats: profileNoStats:
"Jugador sin estadisticas suficientes para mostrar datos personales.", "Jugador sin estad\u00edsticas suficientes para mostrar datos personales.",
profileError: "No fue posible cargar las estadisticas del jugador.", profileError: "No fue posible cargar las estad\u00edsticas del jugador.",
annualLoading: "Cargando ranking anual...", annualLoading: "Cargando ranking anual...",
annualMissing: annualMissing:
"No hay snapshot generado para el ano solicitado. Mostrara estado missing hasta que exista.", "No hay snapshot generado para el a\u00f1o solicitado. Mostrar\u00e1 estado pending hasta que exista.",
annualReadyEmpty: annualReadyEmpty:
"Snapshot anual listo pero sin datos para el ano y servidor seleccionado.", "Ranking anual listo, pero sin datos para el a\u00f1o y servidor seleccionados.",
annualUnsupportedMetric: annualUnsupportedMetric:
"La metrica anual solicitada no esta soportada en V1.", "La m\u00e9trica anual solicitada no est\u00e1 soportada en V1.",
annualMetricInvalid: annualMetricInvalid:
"Parametro de ranking anual invalido. Usa metric=kills en V1.", "Par\u00e1metro de ranking anual inv\u00e1lido. Usa metric=kills en V1.",
annualReadyPrefix: "Ranking anual listo para", annualReadyPrefix: "Ranking anual listo para",
weeklyPlaceholder: weeklyPlaceholder:
"Sin datos semanales. El ranking semanal se actualiza al cargar un jugador.", "Sin datos semanales. El ranking semanal se actualiza al cargar un jugador.",
@@ -69,7 +69,7 @@ document.addEventListener("DOMContentLoaded", () => {
monthlyWindowUnavailableSummary: monthlyWindowUnavailableSummary:
"Resumen mensual no disponible temporalmente.", "Resumen mensual no disponible temporalmente.",
profileReadyTitle: "Perfil personal", profileReadyTitle: "Perfil personal",
profileEmptyTitle: "Selecciona un jugador para ver sus estadisticas.", profileEmptyTitle: "Selecciona un jugador para ver sus estad\u00edsticas.",
partialProfileLoadWarning: partialProfileLoadWarning:
"Algunos bloques de ranking no se cargaron; se mantienen los disponibles.", "Algunos bloques de ranking no se cargaron; se mantienen los disponibles.",
}; };
@@ -78,8 +78,8 @@ document.addEventListener("DOMContentLoaded", () => {
if (searchHelpNode) { if (searchHelpNode) {
searchHelpNode.textContent = searchHelpNode.textContent =
"Usa el buscador para encontrar un jugador. Al seleccionar uno veras resumen semanal y mensual. " + "Usa el buscador para encontrar un jugador. Al seleccionar uno ver\u00e1s resumen semanal y mensual. " +
"El ranking anual se consulta por separado con el ano indicado."; "El ranking anual se consulta por separado con el a\u00f1o indicado.";
} }
setBackendState(messages.backendChecking, false); setBackendState(messages.backendChecking, false);
@@ -176,7 +176,7 @@ document.addEventListener("DOMContentLoaded", () => {
setBackendState(messages.backendOnline, true); setBackendState(messages.backendOnline, true);
setAnnualState( setAnnualState(
"neutral", "neutral",
"Backend disponible. Selecciona un ano para cargar el ranking anual (kills).", "Backend disponible. Selecciona un a\u00f1o para cargar el ranking anual (kills).",
); );
void loadAnnualRanking(); void loadAnnualRanking();
} catch (error) { } catch (error) {
@@ -252,7 +252,7 @@ document.addEventListener("DOMContentLoaded", () => {
const year = resolveAnnualYear(); const year = resolveAnnualYear();
if (year === null) { if (year === null) {
setAnnualState("error", "El ano ingresado no es valido."); setAnnualState("error", "El a\u00f1o ingresado no es v\u00e1lido.");
return; return;
} }
@@ -345,7 +345,7 @@ document.addEventListener("DOMContentLoaded", () => {
setAnnualState( setAnnualState(
"neutral", "neutral",
`${messages.annualReadyPrefix} ${serverId}, ano ${year}, metrica ${metric}`, `${messages.annualReadyPrefix} ${serverId}, a\u00f1o ${year}, m\u00e9trica ${metric}`,
); );
annualContentNode.innerHTML = ` annualContentNode.innerHTML = `
@@ -353,7 +353,7 @@ document.addEventListener("DOMContentLoaded", () => {
<p class="stats-summary-title">Top ${limit} anual</p> <p class="stats-summary-title">Top ${limit} anual</p>
<div class="stats-annual-meta"> <div class="stats-annual-meta">
<p><strong>Servidor:</strong> ${escapeHtml(serverId)}</p> <p><strong>Servidor:</strong> ${escapeHtml(serverId)}</p>
<p><strong>Metrica:</strong> ${escapeHtml(metric)}</p> <p><strong>M\u00e9trica:</strong> ${escapeHtml(metric)}</p>
<p><strong>Partidas fuente:</strong> ${safeInt(sourceMatches, 0)}</p> <p><strong>Partidas fuente:</strong> ${safeInt(sourceMatches, 0)}</p>
<p><strong>Actualizado:</strong> ${escapeHtml(generatedAt || "No disponible")}</p> <p><strong>Actualizado:</strong> ${escapeHtml(generatedAt || "No disponible")}</p>
</div> </div>
@@ -755,8 +755,8 @@ document.addEventListener("DOMContentLoaded", () => {
const metric = escapeHtml(String(ranking.metric || annualMetric)); const metric = escapeHtml(String(ranking.metric || annualMetric));
return ` return `
<p><strong>Posicion:</strong> ${rank}</p> <p><strong>Posici\u00f3n:</strong> ${rank}</p>
<p><strong>Metric:</strong> ${metric}</p> <p><strong>M\u00e9trica:</strong> ${metric}</p>
<p>${escapeHtml(formatWindowRange(ranking))}</p> <p>${escapeHtml(formatWindowRange(ranking))}</p>
`; `;
} }
@@ -783,7 +783,7 @@ document.addEventListener("DOMContentLoaded", () => {
if (Number.isFinite(safeParseNumber(ranking.ranking_position))) { if (Number.isFinite(safeParseNumber(ranking.ranking_position))) {
return { return {
tone: "ok", tone: "ok",
title: `Posicion #${safeInt(ranking.ranking_position, 0)}`, title: `Posici\u00f3n #${safeInt(ranking.ranking_position, 0)}`,
detail: `Jugador posicionado en el ranking ${label} activo por kills.`, detail: `Jugador posicionado en el ranking ${label} activo por kills.`,
}; };
} }
@@ -902,3 +902,4 @@ document.addEventListener("DOMContentLoaded", () => {
.replaceAll("'", "&#39;"); .replaceAll("'", "&#39;");
} }
}); });

View File

@@ -1,30 +1,35 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="es"> <html lang="es">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta <meta
name="description" name="description"
content="Historico propio de HLL Vietnam con ranking semanal y partidas recientes por servidor." content="Hist&oacute;rico propio de HLL Vietnam con ranking semanal y partidas recientes por servidor."
/> />
<title>Historico - HLL Vietnam</title> <title>Hist&oacute;rico - HLL Vietnam</title>
<link rel="stylesheet" href="./assets/css/styles.css" /> <link rel="stylesheet" href="./assets/css/styles.css" />
<link rel="stylesheet" href="./assets/css/historico.css" /> <link rel="stylesheet" href="./assets/css/historico.css" />
</head> </head>
<body data-backend-base-url="http://127.0.0.1:8000"> <body data-backend-base-url="http://127.0.0.1:8000">
<div class="page-shell historical-shell"> <div class="page-shell historical-shell">
<nav class="public-nav" aria-label="Navegaci&oacute;n p&uacute;blica principal">
<a class="public-nav__link" href="./index.html">
Inicio
</a>
<a class="public-nav__link is-active" href="./historico.html" aria-current="page">
Hist&oacute;rico
</a>
<a class="public-nav__link" href="./stats.html">
Stats
</a>
<a class="public-nav__link" href="./ranking.html">
Ranking
</a>
</nav>
<header class="hero historical-hero"> <header class="hero historical-hero">
<div class="hero__overlay"></div> <div class="hero__overlay"></div>
<div class="hero__content historical-hero__content"> <div class="hero__content historical-hero__content">
<div class="historical-hero__topline">
<a class="secondary-button secondary-button--ghost" href="./index.html">
VOLVER INICIO
</a>
<a class="secondary-button secondary-button--ghost" href="./stats.html">
STATS
</a>
<p class="eyebrow">Historico propio</p>
</div>
<div class="historical-hero__layout historical-hero__layout--registry"> <div class="historical-hero__layout historical-hero__layout--registry">
<div class="logo-frame historical-logo-frame"> <div class="logo-frame historical-logo-frame">
<img <img
@@ -38,6 +43,7 @@
</div> </div>
<div class="historical-hero__copy"> <div class="historical-hero__copy">
<div> <div>
<p class="eyebrow eyebrow--section">Hist&oacute;rico propio</p>
<h1 class="historical-hero__title"> <h1 class="historical-hero__title">
Registro de la Registro de la
<span class="hero__title-accent">Comunidad Hispana</span> <span class="hero__title-accent">Comunidad Hispana</span>
@@ -47,8 +53,8 @@
de nuestros servidores. de nuestros servidores.
</p> </p>
</div> </div>
<div class="historical-selector" aria-label="Seleccion de servidor"> <div class="historical-selector" aria-label="Selecci&oacute;n de servidor">
<button <button
class="historical-selector__button is-active" class="historical-selector__button is-active"
type="button" type="button"
data-server-slug="all-servers" data-server-slug="all-servers"
@@ -96,7 +102,7 @@
<div class="historical-summary-grid" id="historical-summary"> <div class="historical-summary-grid" id="historical-summary">
<article class="historical-stat-card"> <article class="historical-stat-card">
<p>Estado</p> <p>Estado</p>
<strong>Cargando datos historicos</strong> <strong>Cargando datos hist&oacute;ricos</strong>
</article> </article>
</div> </div>
</div> </div>
@@ -106,7 +112,7 @@
<div class="panel__shell"> <div class="panel__shell">
<div class="panel__header historical-panel__header"> <div class="panel__header historical-panel__header">
<div> <div>
<p class="eyebrow eyebrow--section">Rankings historicos</p> <p class="eyebrow eyebrow--section">Rankings hist&oacute;ricos</p>
<h2 id="weekly-ranking-title">Ranking del alcance activo</h2> <h2 id="weekly-ranking-title">Ranking del alcance activo</h2>
<p class="historical-panel__note" id="weekly-window-note"> <p class="historical-panel__note" id="weekly-window-note">
Cargando rango del ranking... Cargando rango del ranking...
@@ -144,7 +150,7 @@
class="historical-tabs" class="historical-tabs"
id="weekly-leaderboard-tabs" id="weekly-leaderboard-tabs"
role="tablist" role="tablist"
aria-label="Metricas del ranking" aria-label="M&eacute;tricas del ranking"
> >
<button <button
class="historical-tab is-active" class="historical-tab is-active"
@@ -207,7 +213,7 @@
<div class="panel__header historical-panel__header"> <div class="panel__header historical-panel__header">
<div> <div>
<p class="eyebrow eyebrow--section">Partidas recientes</p> <p class="eyebrow eyebrow--section">Partidas recientes</p>
<h2 id="recent-matches-title">Ultimas partidas registradas</h2> <h2 id="recent-matches-title">&Uacute;ltimas partidas registradas</h2>
<p class="historical-panel__note" id="recent-matches-note"> <p class="historical-panel__note" id="recent-matches-note">
Lista de partidas ya registradas. Lista de partidas ya registradas.
</p> </p>
@@ -230,3 +236,4 @@
<script src="./assets/js/historico-recent-live.js"></script> <script src="./assets/js/historico-recent-live.js"></script>
</body> </body>
</html> </html>

View File

@@ -16,6 +16,20 @@
data-server-refresh-ms="300000" data-server-refresh-ms="300000"
> >
<div class="page-shell"> <div class="page-shell">
<nav class="public-nav" aria-label="Navegación pública principal">
<a class="public-nav__link is-active" href="./index.html" aria-current="page">
Inicio
</a>
<a class="public-nav__link" href="./historico.html">
Histórico
</a>
<a class="public-nav__link" href="./stats.html">
Stats
</a>
<a class="public-nav__link" href="./ranking.html">
Ranking
</a>
</nav>
<header class="hero"> <header class="hero">
<div class="hero__overlay"></div> <div class="hero__overlay"></div>
<div class="hero__content"> <div class="hero__content">
@@ -36,7 +50,7 @@
<span class="hero__title-accent">HLL</span> <span class="hero__title-accent">HLL</span>
</h1> </h1>
<p class="hero__text"> <p class="hero__text">
Centro de reunion para escuadras, eventos y acceso directo al Centro de reunión para escuadras, eventos y acceso directo al
Discord oficial de la comunidad. Discord oficial de la comunidad.
</p> </p>
<div class="hero__actions"> <div class="hero__actions">
@@ -48,15 +62,6 @@
> >
Unirse al Discord Unirse al Discord
</a> </a>
<a class="secondary-button" href="./stats.html">
Stats
</a>
<a class="secondary-button" href="./ranking.html">
Ranking
</a>
<a class="secondary-button" href="./historico.html">
Ver historico propio
</a>
</div> </div>
</div> </div>
</div> </div>
@@ -88,7 +93,7 @@
<div class="panel__shell"> <div class="panel__shell">
<div class="panel__header panel__header--servers"> <div class="panel__header panel__header--servers">
<div> <div>
<p class="eyebrow eyebrow--section">Servidores públicos</p> <p class="eyebrow eyebrow--section">Servidores públicos</p>
<h2 id="servers-title">Estado actual de servidores</h2> <h2 id="servers-title">Estado actual de servidores</h2>
</div> </div>
<p class="status-chip status-chip--fallback" id="servers-badge"> <p class="status-chip status-chip--fallback" id="servers-badge">
@@ -96,11 +101,11 @@
</p> </p>
</div> </div>
<p class="panel__intro panel__intro--tight"> <p class="panel__intro panel__intro--tight">
El panel muestra el ultimo estado disponible consultado de los servidores. El panel muestra el último estado disponible consultado de los servidores.
</p> </p>
<div class="panel__actions panel__actions--compact"> <div class="panel__actions panel__actions--compact">
<a class="secondary-button secondary-button--compact" href="./historico.html"> <a class="secondary-button secondary-button--compact" href="./historico.html">
Abrir zona historico Abrir zona histórico
</a> </a>
</div> </div>
<div class="servers-grid" id="servers-list" aria-live="polite"> <div class="servers-grid" id="servers-list" aria-live="polite">
@@ -132,3 +137,4 @@
</body> </body>
</html> </html>

View File

@@ -1,44 +1,54 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="es"> <html lang="es">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta <meta
name="description" name="description"
content="Ranking global de HLL Vietnam con filtros por periodo, servidor y limite." content="Ranking global de HLL Vietnam con filtros por periodo, servidor y l&iacute;mite."
/> />
<title>Ranking - HLL Vietnam</title> <title>Ranking - HLL Vietnam</title>
<link rel="stylesheet" href="./assets/css/styles.css" /> <link rel="stylesheet" href="./assets/css/styles.css" />
</head> </head>
<body data-backend-base-url="http://127.0.0.1:8000"> <body data-backend-base-url="http://127.0.0.1:8000">
<div class="page-shell stats-page-shell"> <div class="page-shell stats-page-shell">
<nav class="public-nav" aria-label="Navegaci&oacute;n p&uacute;blica principal">
<a class="public-nav__link" href="./index.html">
Inicio
</a>
<a class="public-nav__link" href="./historico.html">
Hist&oacute;rico
</a>
<a class="public-nav__link" href="./stats.html">
Stats
</a>
<a class="public-nav__link is-active" href="./ranking.html" aria-current="page">
Ranking
</a>
</nav>
<header class="hero stats-hero ranking-hero"> <header class="hero stats-hero ranking-hero">
<div class="hero__overlay"></div> <div class="hero__overlay"></div>
<div class="hero__content"> <div class="hero__content">
<div class="hero__copy"> <div class="hero__brand">
<p class="eyebrow eyebrow--section">Seccion Ranking</p> <div class="logo-frame">
<h1 class="hero__title"> <img
Ranking src="./assets/img/logo.png"
<span class="hero__title-accent">Global</span> alt="Logo oficial de la comunidad HLL Vietnam"
</h1> class="logo-frame__image"
<p class="hero__text"> width="1024"
Consulta los lideres publicos por metrica con lectura RCON-first, height="1044"
cambia periodo y servidor sin salir de la pagina, y deja Stats para decoding="async"
revisar el rendimiento de un jugador concreto. />
</p> </div>
<p class="status-chip status-chip--fallback" id="ranking-backend-state"> <div class="hero__copy">
Comprobando disponibilidad del backend <p class="eyebrow eyebrow--section">Secci&oacute;n Ranking</p>
</p> <h1 class="hero__title">
<div class="hero__actions stats__hero-actions"> Ranking
<a class="secondary-button secondary-button--ghost" href="./index.html"> <span class="hero__title-accent">Global</span>
Volver al inicio </h1>
</a> <p class="hero__text">
<a class="secondary-button secondary-button--compact" href="./stats.html"> Consulta los l&iacute;deres p&uacute;blicos de la comunidad, cambia de periodo y servidor sin salir de la p&aacute;gina y revisa qui&eacute;n destaca en cada ventana.
Stats </p>
</a>
<a class="secondary-button secondary-button--compact" href="./historico.html">
Historico
</a>
</div> </div>
</div> </div>
</div> </div>
@@ -49,11 +59,10 @@
<div class="panel__shell"> <div class="panel__shell">
<div class="panel__header"> <div class="panel__header">
<p class="eyebrow eyebrow--section">Filtros de lectura</p> <p class="eyebrow eyebrow--section">Filtros de lectura</p>
<h2>Top publico por ventana</h2> <h2>Top p&uacute;blico por ventana</h2>
</div> </div>
<p class="panel__intro panel__intro--tight"> <p class="panel__intro panel__intro--tight">
El ranking expone solo la lectura publica de lideres. Para busqueda El ranking expone la lectura p&uacute;blica de l&iacute;deres. Para b&uacute;squeda individual usa la secci&oacute;n Stats.
individual usa la seccion Stats.
</p> </p>
<form class="ranking-form" id="ranking-form"> <form class="ranking-form" id="ranking-form">
<div class="ranking-form__grid"> <div class="ranking-form__grid">
@@ -74,7 +83,7 @@
</select> </select>
</label> </label>
<label class="stats-search-form__label" for="ranking-metric"> <label class="stats-search-form__label" for="ranking-metric">
Metrica M&eacute;trica
<select class="ranking-select" id="ranking-metric" name="metric"> <select class="ranking-select" id="ranking-metric" name="metric">
<option value="kills">Kills totales</option> <option value="kills">Kills totales</option>
<option value="deaths">Deaths</option> <option value="deaths">Deaths</option>
@@ -85,7 +94,7 @@
</select> </select>
</label> </label>
<label class="stats-search-form__label" for="ranking-limit"> <label class="stats-search-form__label" for="ranking-limit">
Limite L&iacute;mite
<select class="ranking-select" id="ranking-limit" name="limit"> <select class="ranking-select" id="ranking-limit" name="limit">
<option value="5">Top 5</option> <option value="5">Top 5</option>
<option value="10">Top 10</option> <option value="10">Top 10</option>
@@ -95,7 +104,7 @@
</select> </select>
</label> </label>
<label class="stats-search-form__label" for="ranking-year" id="ranking-year-wrap" hidden> <label class="stats-search-form__label" for="ranking-year" id="ranking-year-wrap" hidden>
Ano A&ntilde;o
<input <input
class="stats-search-input" class="stats-search-input"
id="ranking-year" id="ranking-year"
@@ -109,7 +118,7 @@
</label> </label>
</div> </div>
<p class="ranking-form__note" id="ranking-filter-note"> <p class="ranking-form__note" id="ranking-filter-note">
Ranking compara top globales. Para buscar un jugador concreto usa Stats. Consulta l&iacute;deres p&uacute;blicos y cambia de periodo o servidor sin salir de la p&aacute;gina.
</p> </p>
<div class="hero__actions ranking-form__actions"> <div class="hero__actions ranking-form__actions">
<button class="discord-button" type="submit">Actualizar ranking</button> <button class="discord-button" type="submit">Actualizar ranking</button>
@@ -159,3 +168,4 @@
<script src="./assets/js/ranking.js"></script> <script src="./assets/js/ranking.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,43 +1,54 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="es"> <html lang="es">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta <meta
name="description" name="description"
content="Consulta estadisticas personales de jugadores en el panel Stats de HLL Vietnam." content="Consulta estad&iacute;sticas personales de jugadores en el panel Stats de HLL Vietnam."
/> />
<title>Stats - HLL Vietnam</title> <title>Stats - HLL Vietnam</title>
<link rel="stylesheet" href="./assets/css/styles.css" /> <link rel="stylesheet" href="./assets/css/styles.css" />
</head> </head>
<body data-backend-base-url="http://127.0.0.1:8000"> <body data-backend-base-url="http://127.0.0.1:8000">
<div class="page-shell stats-page-shell"> <div class="page-shell stats-page-shell">
<nav class="public-nav" aria-label="Navegaci&oacute;n p&uacute;blica principal">
<a class="public-nav__link" href="./index.html">
Inicio
</a>
<a class="public-nav__link" href="./historico.html">
Hist&oacute;rico
</a>
<a class="public-nav__link is-active" href="./stats.html" aria-current="page">
Stats
</a>
<a class="public-nav__link" href="./ranking.html">
Ranking
</a>
</nav>
<header class="hero stats-hero"> <header class="hero stats-hero">
<div class="hero__overlay"></div> <div class="hero__overlay"></div>
<div class="hero__content"> <div class="hero__content">
<div class="hero__copy"> <div class="hero__brand">
<p class="eyebrow eyebrow--section">Seccion Stats</p> <div class="logo-frame">
<h1 class="hero__title"> <img
Estadisticas src="./assets/img/logo.png"
<span class="hero__title-accent">Personales</span> alt="Logo oficial de la comunidad HLL Vietnam"
</h1> class="logo-frame__image"
<p class="hero__text"> width="1024"
Busca un jugador por nombre o ID y revisa sus resultados semanales, mensuales height="1044"
y ranking anual con base en datos materializados. decoding="async"
</p> />
<p class="status-chip status-chip--fallback" id="stats-backend-state"> </div>
Comprobando disponibilidad del backend <div class="hero__copy">
</p> <p class="eyebrow eyebrow--section">Secci&oacute;n Stats</p>
<div class="hero__actions stats__hero-actions"> <h1 class="hero__title">
<a class="secondary-button secondary-button--ghost" href="./index.html"> Estad&iacute;sticas
Volver al inicio <span class="hero__title-accent">Personales</span>
</a> </h1>
<a class="secondary-button secondary-button--compact" href="./ranking.html"> <p class="hero__text">
Ranking Busca un jugador por nombre o ID y revisa sus resultados semanales, mensuales y ranking anual.
</a> </p>
<a class="secondary-button secondary-button--compact" href="./historico.html">
Historico
</a>
</div> </div>
</div> </div>
</div> </div>
@@ -51,7 +62,7 @@
<h2>Buscar por nombre o ID</h2> <h2>Buscar por nombre o ID</h2>
</div> </div>
<p class="panel__intro panel__intro--tight" id="stats-search-help"> <p class="panel__intro panel__intro--tight" id="stats-search-help">
Usa al menos 1 caracter para buscar. Al seleccionar un jugador se cargan sus Usa al menos 1 car&aacute;cter para buscar. Al seleccionar un jugador se cargar&aacute;n sus
stats y ranking semanal y mensual. El ranking anual se consulta por separado. stats y ranking semanal y mensual. El ranking anual se consulta por separado.
</p> </p>
<form class="stats-search-form" id="stats-search-form"> <form class="stats-search-form" id="stats-search-form">
@@ -66,7 +77,7 @@
name="query" name="query"
placeholder="Ej: Rambo o 76561198000000000" placeholder="Ej: Rambo o 76561198000000000"
autocomplete="off" autocomplete="off"
aria-label="Texto de busqueda de jugadores" aria-label="Texto de b&uacute;squeda de jugadores"
/> />
<button class="discord-button" type="submit" aria-label="Buscar jugador por nombre o ID"> <button class="discord-button" type="submit" aria-label="Buscar jugador por nombre o ID">
Buscar jugador Buscar jugador
@@ -118,7 +129,7 @@
El ranking anual usa snapshots pre-computados para evitar recalcular toda la temporada en cada consulta. El ranking anual usa snapshots pre-computados para evitar recalcular toda la temporada en cada consulta.
</p> </p>
<form class="stats-annual-form" id="stats-annual-form"> <form class="stats-annual-form" id="stats-annual-form">
<label class="stats-search-form__label" for="stats-annual-year">Ano</label> <label class="stats-search-form__label" for="stats-annual-year">A&ntilde;o</label>
<div class="stats-search-form__fields"> <div class="stats-search-form__fields">
<input <input
class="stats-search-input" class="stats-search-input"
@@ -126,7 +137,7 @@
type="number" type="number"
min="2019" min="2019"
value="" value=""
aria-label="Ano del ranking anual" aria-label="A&ntilde;o del ranking anual"
aria-describedby="stats-annual-state" aria-describedby="stats-annual-state"
/> />
<button class="discord-button" type="submit" aria-label="Cargar ranking anual"> <button class="discord-button" type="submit" aria-label="Cargar ranking anual">
@@ -135,7 +146,7 @@
</div> </div>
</form> </form>
<p class="stats-state stats-state--neutral" id="stats-annual-state" role="status" aria-live="polite"> <p class="stats-state stats-state--neutral" id="stats-annual-state" role="status" aria-live="polite">
Esperando ano para cargar el ranking anual (Top 20 por kills). Esperando a&ntilde;o para cargar el ranking anual (Top 20 por kills).
</p> </p>
<div class="stats-annual-table-wrap" id="stats-annual-content" aria-live="polite"></div> <div class="stats-annual-table-wrap" id="stats-annual-content" aria-live="polite"></div>
</div> </div>
@@ -147,3 +158,4 @@
<script src="./assets/js/stats.js"></script> <script src="./assets/js/stats.js"></script>
</body> </body>
</html> </html>