// Progressive enhancement for local frontend-backend checks. const DEFAULT_SERVER_POLL_INTERVAL_MS = 120 * 1000; const SERVER_HISTORY_URLS = Object.freeze({ "comunidad-hispana-01": "https://scoreboard.comunidadhll.es/games", "comunidad-hispana-02": "https://scoreboard.comunidadhll.es:5443/games", }); const COMMUNITY_CLANS = Object.freeze([ { name: "Comunidad Hispana HLL Vietnam", badge: "Comunidad anfitriona", description: "Punto principal de reunion para escuadras, anuncios y acceso directo al Discord del proyecto.", logoSrc: "./assets/img/logo.png", logoAlt: "Logo de Comunidad Hispana HLL Vietnam", discordUrl: "https://discord.com/invite/PedEqZ2Xsa", discordLabel: "Abrir Discord", }, ]); 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"); const serversTitle = document.getElementById("servers-title"); const serversList = document.getElementById("servers-list"); const serversBadge = document.getElementById("servers-badge"); const communityClansList = document.getElementById("community-clans-list"); updateBackendStatus(statusNode, "Backend comprobando", "status-chip--idle"); setServersDataState(serversBadge, { timestampLabel: "" }); hydrateCommunityClans(communityClansList); let serverRefreshInFlight = false; const refreshServers = async () => { if (serverRefreshInFlight) { return; } serverRefreshInFlight = true; try { await hydrateServers( backendBaseUrl, serversTitle, serversList, serversBadge, ); } finally { serverRefreshInFlight = false; } }; Promise.allSettled([ fetchHealth(backendBaseUrl, statusNode), hydrateTrailer(backendBaseUrl, trailerFrame, trailerTitle), refreshServers(), ]).catch((error) => { console.warn("Progressive enhancement failed", error); }); if (serverPollIntervalMs > 0) { window.setInterval(() => { void refreshServers(); }, serverPollIntervalMs); } }); async function fetchHealth(backendBaseUrl, statusNode) { try { const response = await fetch(`${backendBaseUrl}/health`); if (!response.ok) { throw new Error(`Health request failed with ${response.status}`); } const payload = await response.json(); if (payload.status === "ok") { updateBackendStatus(statusNode, "Backend operativo", "status-chip--ok"); return; } throw new Error("Unexpected health payload"); } catch (error) { console.warn("Backend health check unavailable", error); updateBackendStatus( statusNode, "Modo estatico activo", "status-chip--fallback", ); } } async function hydrateTrailer(backendBaseUrl, trailerFrame, trailerTitle) { if (!trailerFrame || !trailerTitle) { return; } try { const response = await fetch(`${backendBaseUrl}/api/trailer`); if (!response.ok) { throw new Error(`Trailer request failed with ${response.status}`); } const payload = await response.json(); const trailer = payload.data; if (!trailer || !trailer.video_url || !trailer.title) { throw new Error("Trailer payload incomplete"); } trailerFrame.src = trailer.video_url; trailerFrame.title = trailer.title; trailerTitle.textContent = trailer.title; } catch (error) { console.warn("Trailer placeholder remains static", error); } } async function hydrateServers( backendBaseUrl, serversTitle, serversList, serversBadge, ) { if (!serversTitle || !serversList || !serversBadge) { return; } try { const payload = await fetchJson(`${backendBaseUrl}/api/servers`); const serversData = payload.data; if (!serversData || !Array.isArray(serversData.items)) { throw new Error("Servers payload incomplete"); } serversTitle.textContent = serversData.title || "Estado actual de servidores"; setServersDataState(serversBadge, deriveSnapshotState(serversData)); if (serversData.items.length === 0) { serversList.innerHTML = '

Informacion de servidores disponible mas adelante.

'; return; } const visibleItems = selectPrimaryServerItems(serversData.items); serversList.innerHTML = renderServerSections(visibleItems); } catch (error) { console.warn("Servers panel remains on static fallback", error); } } function updateBackendStatus(statusNode, label, stateClass) { if (!statusNode) { return; } statusNode.textContent = label; statusNode.classList.remove("status-chip--ok", "status-chip--fallback"); if (stateClass) { statusNode.classList.add(stateClass); } } function setServersDataState(badgeNode, state) { if (!badgeNode) { return; } const hasLabel = typeof state.label === "string" && state.label; badgeNode.textContent = hasLabel ? state.label : "Actualizado no disponible"; badgeNode.classList.toggle("status-chip--ok", Boolean(hasLabel && state.isFresh)); badgeNode.classList.toggle( "status-chip--fallback", !hasLabel || !state.isFresh, ); } function renderServerStatsCard(server) { const serverName = server.server_name || "Servidor sin nombre"; const statusLabel = formatServerStatus(server.status); const stateClass = server.status === "online" ? "server-state--online" : "server-state--offline"; const isRealSnapshot = server.snapshot_origin === "real-a2s"; const currentMap = server.current_map || "Sin mapa disponible"; const region = server.region || "Region pendiente"; const players = Number.isFinite(server.players) ? server.players : 0; const maxPlayers = Number.isFinite(server.max_players) ? server.max_players : 0; const actionMarkup = renderServerAction(server); const cardVariantClass = isRealSnapshot ? "server-card--real" : "server-card--reference"; const quickFacts = renderQuickFacts([ { label: "Mapa", value: currentMap, valueClassName: "server-card__quickfact-value--map" }, { label: "Region", value: region }, ]); return `

${isRealSnapshot ? "Servidor de comunidad" : "Referencia actual"}

${escapeHtml(serverName)}

${escapeHtml(statusLabel)}

${escapeHtml(`${players} / ${maxPlayers}`)}

${actionMarkup}
${quickFacts}
`; } function renderServerSections(latestItems) { return latestItems.map((server) => renderServerStatsCard(server)).join(""); } function renderServerAction(server) { const historyUrl = getServerHistoryUrl(server); if (!historyUrl) { return ""; } return `
Historico
`; } function hydrateCommunityClans(listNode) { if (!listNode) { return; } listNode.innerHTML = COMMUNITY_CLANS.map((clan) => renderCommunityClanCard(clan)).join(""); } function renderCommunityClanCard(clan) { return `

${escapeHtml(clan.badge)}

${escapeHtml(clan.name)}

${escapeHtml(clan.description)}

${escapeHtml(clan.discordLabel)}
`; } function renderQuickFacts(items) { return `
${items .map( (item) => `

${escapeHtml(item.label)}

${escapeHtml(item.value)}
`, ) .join("")}
`; } function getServerHistoryUrl(server) { const externalServerId = typeof server?.external_server_id === "string" ? server.external_server_id.trim() : ""; if (!externalServerId) { return ""; } return SERVER_HISTORY_URLS[externalServerId] || ""; } function selectPrimaryServerItems(items) { if (!Array.isArray(items)) { return []; } const realItems = items.filter(isRealA2SSnapshot); return realItems.length > 0 ? realItems : items; } function isRealA2SSnapshot(item) { return item?.snapshot_origin === "real-a2s"; } function deriveSnapshotState(serversData) { const timestampLabel = serversData?.last_snapshot_at ? formatTimestamp(serversData.last_snapshot_at) : ""; if (!timestampLabel) { return { label: "", isFresh: false, }; } const isFresh = serversData?.is_stale !== true; return { label: isFresh ? `Actualizado ${timestampLabel}` : `Ultimo snapshot ${timestampLabel}`, isFresh, }; } function formatServerStatus(status) { if (status === "online") { return "Online"; } if (status === "offline") { return "Offline"; } return "Estado pendiente"; } function formatTimestamp(timestamp) { const value = new Date(timestamp); if (Number.isNaN(value.getTime())) { return "Fecha no disponible"; } return new Intl.DateTimeFormat("es-ES", { dateStyle: "short", timeStyle: "short", }).format(value); } function getServerPollIntervalMs(rawValue) { const parsedValue = Number(rawValue); if (!Number.isFinite(parsedValue) || parsedValue <= 0) { return DEFAULT_SERVER_POLL_INTERVAL_MS; } return parsedValue; } async function fetchJson(url) { const response = await fetch(url); if (!response.ok) { throw new Error(`Request failed with ${response.status}`); } return response.json(); } function escapeHtml(value) { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); }