fix server panel ux and hero cleanup

This commit is contained in:
devRaGonSa
2026-03-20 19:54:45 +01:00
parent 94fb6d19d0
commit 32baca96af
4 changed files with 267 additions and 298 deletions

View File

@@ -0,0 +1,101 @@
# TASK-023-server-panel-ux-fixes-and-hero-cleanup
## Goal
Corregir la UX del bloque de servidores y limpiar elementos visuales del hero de la landing, eliminando mensajes innecesarios, arreglando el CTA roto de conexion y dejando visible una referencia honesta de actualizacion del snapshot.
## Context
La landing ya muestra datos actuales de los 2 servidores reales de la comunidad, pero todavia hay varios problemas de UX y acabado:
- aparece un texto largo e innecesario bajo el titulo del bloque de servidores indicando que el backend no pudo refrescar y que muestra el ultimo snapshot valido
- el boton `Conectar` no funciona correctamente y muestra errores de conexion como los observados en Steam
- el badge superior derecho del bloque de servidores debe pasar a mostrar una referencia limpia tipo `Actualizado ...` con el momento real correspondiente
- en el hero deben eliminarse los pills `BACKEND OPERATIVO` y `COMUNIDAD TACTICA HISPANA`
La tarea debe dejar la UI mas limpia, mas honesta y sin CTAs rotos.
## Steps
1. Revisar la implementacion actual del bloque de servidores en frontend y el payload real de `/api/servers`.
2. Eliminar del bloque de servidores el texto:
- `El backend no pudo refrescar ahora mismo y muestra el ultimo snapshot valido.`
- `Captura: ...`
o cualquier variante equivalente que recargue innecesariamente la UI.
3. Mantener la referencia temporal del snapshot unicamente en el badge superior derecho del bloque, sustituyendo el texto actual por un formato limpio tipo:
- `Actualizado 20/3/26, 19:37`
o equivalente coherente con el estilo visual actual.
4. Asegurar que el momento mostrado en ese badge viene de datos reales del snapshot/backend y no de texto ficticio.
5. Revisar la implementacion del boton `Conectar` de cada servidor.
6. Investigar por que el CTA actual produce el error observado y corregirlo si existe una forma fiable de conexion directa compatible con el flujo actual del proyecto.
7. Si la conexion directa no puede garantizarse de forma robusta con el comportamiento actual del juego/cliente, no dejar un boton roto:
- sustituirlo por una accion segura y util
- priorizar una UX que siempre funcione, por ejemplo `Copiar IP`, `Copiar direccion` o una variante equivalente basada en los datos reales del servidor
- si se conserva una accion de conexion, debe quedar realmente funcional
8. Mantener el comportamiento de los 2 servidores reales de la comunidad y no reintroducir servidores ficticios.
9. En el hero, eliminar visualmente:
- `BACKEND OPERATIVO`
- `COMUNIDAD TACTICA HISPANA`
10. Mantener intactos:
- logo
- CTA principal de Discord
- trailer
- estructura general de la landing
11. Asegurar que los cambios funcionan tanto en la UI estatica como en la UI hidratada por JS.
12. Al completar la implementacion:
- dejar el repositorio consistente
- hacer commit
- hacer push al remoto si el entorno lo permite
## Files to Read First
- AGENTS.md
- ai/repo-context.md
- ai/architecture-index.md
- docs/frontend-backend-contract.md
- frontend/index.html
- frontend/assets/js/main.js
- frontend/assets/css/styles.css
- backend/app/payloads.py
- backend/app/routes.py
- backend/README.md
## Expected Files to Modify
- frontend/index.html
- frontend/assets/js/main.js
- frontend/assets/css/styles.css
- opcionalmente backend/app/payloads.py si hace falta exponer mejor datos utiles para la UX del boton de servidor
- opcionalmente backend/README.md si cambia el comportamiento esperado del CTA de servidor
## Constraints
- No volver a introducir mensajes largos y tecnicos en la UI principal del bloque de servidores.
- No dejar CTAs rotos o engañosos.
- No presentar datos no reales.
- No añadir librerias nuevas.
- No romper el polling ni la hidratacion actual del bloque.
- No romper el fallback actual.
- No hacer cambios destructivos.
- Mantener la solucion clara, visualmente limpia y coherente con la landing.
## Validation
- Ya no aparece el texto largo bajo el titulo del bloque de servidores sobre fallo de refresh.
- El badge superior derecho muestra `Actualizado ...` con el momento real correspondiente al snapshot.
- El CTA de servidor deja de fallar:
- o conecta correctamente
- o se sustituye por una accion segura que si funciona
- Los 2 servidores reales de la comunidad siguen siendo los unicos mostrados.
- En el hero ya no aparecen `BACKEND OPERATIVO` ni `COMUNIDAD TACTICA HISPANA`.
- La landing mantiene coherencia visual y funcional.
- Los cambios quedan committeados y se hace push si el entorno lo permite.
## Change Budget
- Preferir menos de 5 archivos modificados.
- Preferir menos de 220 lineas cambiadas.
## Outcome
- `frontend/index.html` elimina los pills del hero, retira el texto secundario bajo el bloque de servidores y alinea el fallback estatico con los 2 servidores reales de Comunidad Hispana.
- `frontend/assets/js/main.js` deja la referencia temporal del snapshot solo en el badge superior derecho usando `last_snapshot_at` real del backend y sustituye la CTA rota `Conectar` por una accion segura de `Copiar IP`.
- `frontend/assets/css/styles.css` adapta las tarjetas al nuevo CTA, elimina estilos ya innecesarios del layout anterior y mantiene la presentacion limpia en desktop y movil.
## Validation Result
- Validado con `node --check frontend/assets/js/main.js`.
- Verificado con busqueda en `frontend/` que ya no queda el texto largo del snapshot fallido ni `steam://connect`.
- Revisado con `git diff --name-only`: los cambios de esta task quedan limitados a `frontend/index.html`, `frontend/assets/js/main.js`, `frontend/assets/css/styles.css` y este archivo de task; permanecen sin tocar otros cambios locales ajenos.
## Decision Notes
- La conexion directa por `steam://connect/<host>:<game_port>` se retira de la UI porque no podia garantizarse como CTA robusta en el flujo actual; se prioriza una accion segura y util basada en los datos reales del backend.

View File

@@ -572,27 +572,6 @@ h2 {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03); 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 { .server-card__actions {
margin: 0; margin: 0;
width: 100%; width: 100%;
@@ -600,7 +579,7 @@ h2 {
justify-content: flex-end; justify-content: flex-end;
} }
.server-connect-button { .server-copy-button {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -617,19 +596,26 @@ h2 {
font-weight: 700; font-weight: 700;
letter-spacing: 0.12em; letter-spacing: 0.12em;
text-transform: uppercase; text-transform: uppercase;
cursor: pointer;
transition: transition:
transform 160ms ease, transform 160ms ease,
border-color 160ms ease, border-color 160ms ease,
background 160ms ease; background 160ms ease,
opacity 160ms ease;
} }
.server-connect-button:hover, .server-copy-button:hover,
.server-connect-button:focus-visible { .server-copy-button:focus-visible {
transform: translateY(-1px); transform: translateY(-1px);
border-color: rgba(210, 182, 118, 0.52); border-color: rgba(210, 182, 118, 0.52);
background: linear-gradient(180deg, rgba(210, 182, 118, 0.2), rgba(142, 160, 98, 0.26)); background: linear-gradient(180deg, rgba(210, 182, 118, 0.2), rgba(142, 160, 98, 0.26));
} }
.server-copy-button:disabled {
opacity: 0.78;
cursor: default;
}
.server-state { .server-state {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -657,38 +643,6 @@ h2 {
color: var(--accent-warm); color: var(--accent-warm);
} }
.server-card__stats {
margin: 0;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.server-stat {
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 {
color: var(--accent-strong);
}
.server-card__stats dt {
margin: 0 0 6px;
color: var(--muted);
font-size: 0.75rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.server-card__stats dd {
margin: 0;
font-size: 0.95rem;
line-height: 1.4;
}
.server-card__quickfacts { .server-card__quickfacts {
margin-bottom: 0; margin-bottom: 0;
display: grid; display: grid;
@@ -836,10 +790,6 @@ h2 {
justify-content: start; justify-content: start;
} }
.server-card__stats {
grid-template-columns: 1fr;
}
.server-card__quickfacts { .server-card__quickfacts {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }

View File

@@ -13,13 +13,12 @@ document.addEventListener("DOMContentLoaded", () => {
const trailerFrame = document.getElementById("trailer-frame"); const trailerFrame = document.getElementById("trailer-frame");
const trailerTitle = document.getElementById("trailer-title"); const trailerTitle = document.getElementById("trailer-title");
const serversTitle = document.getElementById("servers-title"); const serversTitle = document.getElementById("servers-title");
const serversNote = document.getElementById("servers-note");
const serversList = document.getElementById("servers-list"); const serversList = document.getElementById("servers-list");
const serversBadge = document.getElementById("servers-badge"); const serversBadge = document.getElementById("servers-badge");
updateBackendStatus(statusNode, "Backend comprobando", "status-chip--idle"); updateBackendStatus(statusNode, "Backend comprobando", "status-chip--idle");
setServersDataState(serversBadge, { timestampLabel: "" });
setServersDataState(serversBadge, { kind: "fallback" }); bindCopyAddressActions();
let serverRefreshInFlight = false; let serverRefreshInFlight = false;
const refreshServers = async () => { const refreshServers = async () => {
@@ -32,7 +31,6 @@ document.addEventListener("DOMContentLoaded", () => {
await hydrateServers( await hydrateServers(
backendBaseUrl, backendBaseUrl,
serversTitle, serversTitle,
serversNote,
serversList, serversList,
serversBadge, serversBadge,
); );
@@ -72,7 +70,11 @@ async function fetchHealth(backendBaseUrl, statusNode) {
throw new Error("Unexpected health payload"); throw new Error("Unexpected health payload");
} catch (error) { } catch (error) {
console.warn("Backend health check unavailable", error); console.warn("Backend health check unavailable", error);
updateBackendStatus(statusNode, "Modo estatico activo", "status-chip--fallback"); updateBackendStatus(
statusNode,
"Modo estatico activo",
"status-chip--fallback",
);
} }
} }
@@ -104,11 +106,10 @@ async function hydrateTrailer(backendBaseUrl, trailerFrame, trailerTitle) {
async function hydrateServers( async function hydrateServers(
backendBaseUrl, backendBaseUrl,
serversTitle, serversTitle,
serversNote,
serversList, serversList,
serversBadge, serversBadge,
) { ) {
if (!serversTitle || !serversNote || !serversList || !serversBadge) { if (!serversTitle || !serversList || !serversBadge) {
return; return;
} }
@@ -121,11 +122,7 @@ async function hydrateServers(
serversTitle.textContent = serversTitle.textContent =
serversData.title || "Estado actual de servidores"; serversData.title || "Estado actual de servidores";
setServersDataState( setServersDataState(serversBadge, deriveSnapshotState(serversData));
serversBadge,
deriveSnapshotState(serversData),
);
serversNote.textContent = buildServersNote(serversData);
if (serversData.items.length === 0) { if (serversData.items.length === 0) {
serversList.innerHTML = serversList.innerHTML =
@@ -157,73 +154,12 @@ function setServersDataState(badgeNode, state) {
return; return;
} }
if (state.kind === "live") { const hasTimestamp = typeof state.timestampLabel === "string" && state.timestampLabel;
badgeNode.textContent = state.timestampLabel badgeNode.textContent = hasTimestamp
? `Actualizado ${state.timestampLabel}` ? `Actualizado ${state.timestampLabel}`
: "Datos en vivo"; : "Actualizado no disponible";
badgeNode.classList.remove("status-chip--fallback"); badgeNode.classList.toggle("status-chip--ok", Boolean(hasTimestamp));
badgeNode.classList.add("status-chip--ok"); badgeNode.classList.toggle("status-chip--fallback", !hasTimestamp);
return;
}
if (state.kind === "historical") {
badgeNode.textContent = state.timestampLabel
? `Snapshot ${state.timestampLabel}`
: "Actividad reciente";
badgeNode.classList.remove("status-chip--fallback");
badgeNode.classList.add("status-chip--ok");
return;
}
badgeNode.textContent = "Respaldo controlado";
badgeNode.classList.remove("status-chip--ok");
badgeNode.classList.add("status-chip--fallback");
}
function renderServerCard(server) {
const serverName = server.server_name || "Servidor sin nombre";
const serverStatus =
server.status === "online" ? "Online" : server.status === "offline" ? "Offline" : "Estado pendiente";
const stateClass =
server.status === "online" ? "server-state--online" : "server-state--offline";
const currentMap = server.current_map || "Sin mapa disponible";
const region = server.region || "Region pendiente";
const players = Number.isFinite(server.players) ? server.players : 0;
const maxPlayers = Number.isFinite(server.max_players) ? server.max_players : 0;
const occupancy = getPopulationPercent(players, maxPlayers);
const sourceLabel = isRealA2SSnapshot(server) ? "Snapshot activo" : "Referencia actual";
return `
<article class="server-card">
<div class="server-card__top">
<div class="server-card__identity">
<p class="server-card__eyebrow">${escapeHtml(sourceLabel)}</p>
<h3>${escapeHtml(serverName)}</h3>
</div>
<div class="server-card__status-column">
<span class="server-state ${stateClass}">${escapeHtml(serverStatus)}</span>
<p class="server-card__population">${players} / ${maxPlayers}</p>
</div>
</div>
<div class="server-card__load" aria-hidden="true">
<span style="width: ${occupancy}%"></span>
</div>
<dl class="server-card__stats">
<div class="server-stat server-stat--players">
<dt>Jugadores</dt>
<dd>${players} / ${maxPlayers}</dd>
</div>
<div class="server-stat">
<dt>Mapa</dt>
<dd>${escapeHtml(currentMap)}</dd>
</div>
<div class="server-stat">
<dt>Region</dt>
<dd>${escapeHtml(region)}</dd>
</div>
</dl>
</article>
`;
} }
function renderServerStatsCard(server) { function renderServerStatsCard(server) {
@@ -236,35 +172,28 @@ function renderServerStatsCard(server) {
const region = server.region || "Region pendiente"; const region = server.region || "Region pendiente";
const players = Number.isFinite(server.players) ? server.players : 0; const players = Number.isFinite(server.players) ? server.players : 0;
const maxPlayers = Number.isFinite(server.max_players) ? server.max_players : 0; const maxPlayers = Number.isFinite(server.max_players) ? server.max_players : 0;
const occupancy = getPopulationPercent(players, maxPlayers); const address = getServerAddress(server);
const updatedAt = server.captured_at const actionMarkup = renderServerAction(server);
? formatTimestamp(server.captured_at)
: "Sin captura reciente";
const updatedAgo = formatElapsedMinutes(server.history_summary?.minutes_since_last_capture);
const connectAction = renderConnectAction(server);
const cardVariantClass = isRealSnapshot ? "server-card--real" : "server-card--reference"; const cardVariantClass = isRealSnapshot ? "server-card--real" : "server-card--reference";
const quickFacts = renderQuickFacts([ const quickFacts = renderQuickFacts([
{ label: "Mapa", value: currentMap }, { label: "Mapa", value: currentMap },
{ label: "Region", value: region }, { label: "Region", value: region },
{ label: "Ultima captura", value: updatedAgo || updatedAt }, { label: "Direccion", value: address || "No disponible" },
]); ]);
return ` return `
<article class="server-card server-card--stats ${cardVariantClass}"> <article class="server-card server-card--stats ${cardVariantClass}">
<div class="server-card__top server-card__top--stats"> <div class="server-card__top server-card__top--stats">
<div class="server-card__identity"> <div class="server-card__identity">
<p class="server-card__eyebrow">${isRealSnapshot ? "Snapshot activo" : "Referencia actual"}</p> <p class="server-card__eyebrow">${isRealSnapshot ? "Servidor de comunidad" : "Referencia actual"}</p>
<h3>${escapeHtml(serverName)}</h3> <h3>${escapeHtml(serverName)}</h3>
</div> </div>
<div class="server-card__status-column"> <div class="server-card__status-column">
<span class="server-state ${stateClass}">${escapeHtml(statusLabel)}</span> <span class="server-state ${stateClass}">${escapeHtml(statusLabel)}</span>
<p class="server-card__population">${escapeHtml(`${players} / ${maxPlayers}`)}</p> <p class="server-card__population">${escapeHtml(`${players} / ${maxPlayers}`)}</p>
${connectAction} ${actionMarkup}
</div> </div>
</div> </div>
<div class="server-card__load" aria-hidden="true">
<span style="width: ${occupancy}%"></span>
</div>
${quickFacts} ${quickFacts}
</article> </article>
`; `;
@@ -278,21 +207,22 @@ function renderServerSections(latestItems) {
`; `;
} }
function renderConnectAction(server) { function renderServerAction(server) {
if (!server || !isRealA2SSnapshot(server)) { const address = getServerAddress(server);
if (!address) {
return ""; return "";
} }
const host = typeof server.host === "string" ? server.host.trim() : "";
const gamePort = Number.isInteger(server.game_port) ? server.game_port : Number(server.game_port);
if (!host || !Number.isInteger(gamePort) || gamePort <= 0) {
return "";
}
const connectUrl = `steam://connect/${host}:${gamePort}`;
return ` return `
<div class="server-card__actions"> <div class="server-card__actions">
<a class="server-connect-button" href="${escapeHtml(connectUrl)}">Conectar</a> <button
class="server-copy-button"
type="button"
data-copy-address="${escapeHtml(address)}"
data-default-label="Copiar IP"
>
Copiar IP
</button>
</div> </div>
`; `;
} }
@@ -314,6 +244,68 @@ function renderQuickFacts(items) {
`; `;
} }
function bindCopyAddressActions() {
document.addEventListener("click", async (event) => {
const button = event.target.closest("[data-copy-address]");
if (!(button instanceof HTMLButtonElement)) {
return;
}
const address = button.dataset.copyAddress || "";
if (!address) {
return;
}
const defaultLabel = button.dataset.defaultLabel || "Copiar IP";
button.disabled = true;
try {
await copyText(address);
button.textContent = "IP copiada";
} catch (error) {
console.warn("Could not copy server address", error);
button.textContent = "Copia manual";
} finally {
window.setTimeout(() => {
button.textContent = defaultLabel;
button.disabled = false;
}, 1800);
}
});
}
async function copyText(value) {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(value);
return;
}
const textarea = document.createElement("textarea");
textarea.value = value;
textarea.setAttribute("readonly", "");
textarea.style.position = "absolute";
textarea.style.left = "-9999px";
document.body.append(textarea);
textarea.select();
const copied = document.execCommand("copy");
textarea.remove();
if (!copied) {
throw new Error("Clipboard copy command failed");
}
}
function getServerAddress(server) {
const host = typeof server?.host === "string" ? server.host.trim() : "";
const gamePort = Number(server?.game_port);
if (!host || !Number.isInteger(gamePort) || gamePort <= 0) {
return "";
}
return `${host}:${gamePort}`;
}
function selectPrimaryServerItems(items) { function selectPrimaryServerItems(items) {
if (!Array.isArray(items)) { if (!Array.isArray(items)) {
return []; return [];
@@ -328,40 +320,11 @@ function isRealA2SSnapshot(item) {
} }
function deriveSnapshotState(serversData) { function deriveSnapshotState(serversData) {
const timestampLabel = serversData?.last_snapshot_at return {
? formatTimestamp(serversData.last_snapshot_at) timestampLabel: serversData?.last_snapshot_at
: ""; ? formatTimestamp(serversData.last_snapshot_at)
: "",
if (!serversData) { };
return { kind: "fallback", timestampLabel };
}
if (serversData.freshness === "fresh" && serversData.source === "real-time-a2s-refresh") {
return { kind: "live", timestampLabel };
}
if (Array.isArray(serversData.items) && serversData.items.length > 0) {
return { kind: "historical", timestampLabel };
}
return { kind: "fallback", timestampLabel };
}
function buildServersNote(serversData) {
const lastSnapshotLabel = serversData.last_snapshot_at
? formatTimestamp(serversData.last_snapshot_at)
: "sin timestamp disponible";
const snapshotAgeLabel = formatSnapshotAge(serversData.snapshot_age_seconds);
if (serversData.freshness === "fresh") {
return `Estado real consultado desde backend. Ultimo snapshot: ${lastSnapshotLabel}${snapshotAgeLabel ? `, ${snapshotAgeLabel}.` : "."}`;
}
if (Array.isArray(serversData.items) && serversData.items.length > 0) {
return `El backend no pudo refrescar ahora mismo y muestra el ultimo snapshot valido. Captura: ${lastSnapshotLabel}${snapshotAgeLabel ? `, ${snapshotAgeLabel}.` : "."}`;
}
return "El backend no pudo obtener un snapshot valido de los 2 servidores en este momento.";
} }
function formatServerStatus(status) { function formatServerStatus(status) {
@@ -388,54 +351,6 @@ function formatTimestamp(timestamp) {
}).format(value); }).format(value);
} }
function formatElapsedMinutes(minutes) {
if (!Number.isFinite(minutes)) {
return "";
}
if (minutes < 1) {
return "hace menos de 1 min";
}
if (minutes < 60) {
return `hace ${minutes} min`;
}
const hours = Math.floor(minutes / 60);
if (hours < 24) {
return `hace ${hours} h`;
}
const days = Math.floor(hours / 24);
return `hace ${days} d`;
}
function formatSnapshotAge(snapshotAgeSeconds) {
if (!Number.isFinite(snapshotAgeSeconds)) {
return "";
}
if (snapshotAgeSeconds < 60) {
return `hace ${snapshotAgeSeconds} s`;
}
const wholeMinutes = Math.floor(snapshotAgeSeconds / 60);
if (wholeMinutes < 60) {
return `hace ${wholeMinutes} min`;
}
const wholeHours = Math.floor(wholeMinutes / 60);
return `hace ${wholeHours} h`;
}
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) { function getServerPollIntervalMs(rawValue) {
const parsedValue = Number(rawValue); const parsedValue = Number(rawValue);
if (!Number.isFinite(parsedValue) || parsedValue <= 0) { if (!Number.isFinite(parsedValue) || parsedValue <= 0) {

View File

@@ -30,7 +30,6 @@
/> />
</div> </div>
<div class="hero__copy"> <div class="hero__copy">
<p class="eyebrow">Comunidad táctica hispana</p>
<h1 class="hero__title"> <h1 class="hero__title">
Comunidad Hispana Comunidad Hispana
<span class="hero__title-accent">HLL Vietnam</span> <span class="hero__title-accent">HLL Vietnam</span>
@@ -40,13 +39,6 @@
Discord oficial de la comunidad. Discord oficial de la comunidad.
</p> </p>
<div class="hero__actions"> <div class="hero__actions">
<p
class="status-chip status-chip--idle"
id="backend-status"
aria-live="polite"
>
Backend no verificado
</p>
<a <a
class="discord-button" class="discord-button"
href="https://discord.com/invite/PedEqZ2Xsa" href="https://discord.com/invite/PedEqZ2Xsa"
@@ -90,70 +82,81 @@
<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">
Referencia actual Actualizado no disponible
</p> </p>
</div> </div>
<p class="panel__intro" id="servers-note">
Estado actual y acceso directo a los servidores disponibles.
</p>
<div class="servers-grid" id="servers-list"> <div class="servers-grid" id="servers-list">
<article class="server-card"> <article class="server-card server-card--stats server-card--real">
<div class="server-card__top"> <div class="server-card__top server-card__top--stats">
<div class="server-card__identity"> <div class="server-card__identity">
<p class="server-card__eyebrow">Referencia actual</p> <p class="server-card__eyebrow">Servidor de comunidad</p>
<h3>HLL ESP Tactical Rotation</h3> <h3>Comunidad Hispana #01</h3>
</div> </div>
<div class="server-card__status-column"> <div class="server-card__status-column">
<span class="server-state server-state--online">Online</span> <span class="server-state server-state--online">Online</span>
<p class="server-card__population">74 / 100</p> <p class="server-card__population">74 / 100</p>
<div class="server-card__actions">
<button
class="server-copy-button"
type="button"
data-copy-address="152.114.195.174:7777"
data-default-label="Copiar IP"
>
Copiar IP
</button>
</div>
</div> </div>
</div> </div>
<div class="server-card__load" aria-hidden="true"> <div class="server-card__quickfacts">
<span style="width: 74%"></span> <article class="server-card__quickfact">
<p>Mapa</p>
<strong>Sainte-Marie-du-Mont</strong>
</article>
<article class="server-card__quickfact">
<p>Region</p>
<strong>ES</strong>
</article>
<article class="server-card__quickfact">
<p>Direccion</p>
<strong>152.114.195.174:7777</strong>
</article>
</div> </div>
<dl class="server-card__stats">
<div class="server-stat server-stat--players">
<dt>Jugadores</dt>
<dd>74 / 100</dd>
</div>
<div class="server-stat">
<dt>Mapa</dt>
<dd>Sainte-Marie-du-Mont</dd>
</div>
<div class="server-stat">
<dt>Region</dt>
<dd>EU</dd>
</div>
</dl>
</article> </article>
<article class="server-card"> <article class="server-card server-card--stats server-card--real">
<div class="server-card__top"> <div class="server-card__top server-card__top--stats">
<div class="server-card__identity"> <div class="server-card__identity">
<p class="server-card__eyebrow">Referencia actual</p> <p class="server-card__eyebrow">Servidor de comunidad</p>
<h3>HLL LATAM Night Offensive</h3> <h3>Comunidad Hispana #02</h3>
</div> </div>
<div class="server-card__status-column"> <div class="server-card__status-column">
<span class="server-state server-state--online">Online</span> <span class="server-state server-state--offline">Offline</span>
<p class="server-card__population">51 / 100</p> <p class="server-card__population">0 / 100</p>
<div class="server-card__actions">
<button
class="server-copy-button"
type="button"
data-copy-address="152.114.195.150:7877"
data-default-label="Copiar IP"
>
Copiar IP
</button>
</div>
</div> </div>
</div> </div>
<div class="server-card__load" aria-hidden="true"> <div class="server-card__quickfacts">
<span style="width: 51%"></span> <article class="server-card__quickfact">
<p>Mapa</p>
<strong>Sin mapa disponible</strong>
</article>
<article class="server-card__quickfact">
<p>Region</p>
<strong>ES</strong>
</article>
<article class="server-card__quickfact">
<p>Direccion</p>
<strong>152.114.195.150:7877</strong>
</article>
</div> </div>
<dl class="server-card__stats">
<div class="server-stat server-stat--players">
<dt>Jugadores</dt>
<dd>51 / 100</dd>
</div>
<div class="server-stat">
<dt>Mapa</dt>
<dd>Carentan</dd>
</div>
<div class="server-stat">
<dt>Region</dt>
<dd>LATAM</dd>
</div>
</dl>
</article> </article>
</div> </div>
</div> </div>