feat: refine current match live feed

This commit is contained in:
devRaGonSa
2026-05-21 19:47:12 +02:00
parent 370e014b59
commit d558ac81ed
14 changed files with 330 additions and 152 deletions

View File

@@ -297,10 +297,12 @@ function renderServerStatsCard(server) {
<div class="server-card__status-column">
<span class="server-state ${stateClass}">${escapeHtml(statusLabel)}</span>
<p class="server-card__population">${escapeHtml(`${players} / ${maxPlayers}`)}</p>
${actionMarkup}
</div>
</div>
${quickFacts}
<div class="server-card__bottom">
${quickFacts}
${actionMarkup}
</div>
</article>
`;
}
@@ -342,16 +344,8 @@ function renderServerAction(server) {
return `
<div class="server-card__actions">
<a
class="server-action-link"
href="${escapeHtml(actions.publicScoreboardUrl)}"
target="_blank"
rel="noreferrer"
>
Scoreboard publico
</a>
<a class="server-action-link" href="${escapeHtml(actions.historicalUrl)}">
Nuestro historico
Historico
</a>
<a class="server-action-link" href="${escapeHtml(actions.currentMatchUrl)}">
Partida actual

View File

@@ -1,4 +1,5 @@
const CURRENT_MATCH_POLL_INTERVAL_MS = 30 * 1000;
const CURRENT_MATCH_KILL_FEED_POLL_INTERVAL_MS = 1500;
const CURRENT_MATCH_SERVERS = Object.freeze({
"comunidad-hispana-01": "Comunidad Hispana #01",
"comunidad-hispana-02": "Comunidad Hispana #02",
@@ -9,10 +10,14 @@ const CURRENT_MATCH_SCOREBOARDS = Object.freeze({
});
const CURRENT_MATCH_KILL_FEED_LIMIT = 15;
const CURRENT_MATCH_WEAPONS = Object.freeze({
"m1 garand": { label: "M1 Garand", glyph: "M1" },
mp40: { label: "MP40", glyph: "MP" },
"m1a1 thompson": { label: "M1A1 Thompson", glyph: "TH" },
unknown: { label: "Arma desconocida", glyph: "?" },
"m1 garand": { label: "M1 Garand", icon: "m1_garand.PNG" },
mp40: { label: "MP40", icon: "mp40.PNG" },
"m1a1 thompson": { label: "M1A1 Thompson", icon: "thompson.PNG" },
thompson: { label: "M1A1 Thompson", icon: "thompson.PNG" },
"gewehr 43": { label: "Gewehr 43", icon: "gewehr.PNG" },
g43: { label: "Gewehr 43", icon: "gewehr.PNG" },
mg42: { label: "MG42", icon: "mg42.PNG" },
unknown: { label: "Arma desconocida", icon: "" },
});
document.addEventListener("DOMContentLoaded", () => {
@@ -43,27 +48,43 @@ document.addEventListener("DOMContentLoaded", () => {
nodes.history.href = `./historico.html?server=${encodeURIComponent(serverSlug)}`;
const killFeedState = initializeKillFeed(nodes);
const playerStatsState = initializePlayerStats(nodes);
let refreshInFlight = false;
const refresh = async () => {
if (refreshInFlight) {
let matchRefreshInFlight = false;
const refreshMatchPanels = async () => {
if (matchRefreshInFlight) {
return;
}
refreshInFlight = true;
matchRefreshInFlight = true;
try {
await Promise.allSettled([
loadCurrentMatch({ backendBaseUrl, serverSlug, nodes }),
loadKillFeed({ backendBaseUrl, serverSlug, nodes, killFeedState }),
loadPlayerStats({ backendBaseUrl, serverSlug, nodes, playerStatsState }),
]);
} finally {
refreshInFlight = false;
matchRefreshInFlight = false;
}
};
void refresh();
let killFeedRefreshInFlight = false;
const refreshKillFeed = async () => {
if (killFeedRefreshInFlight) {
return;
}
killFeedRefreshInFlight = true;
try {
await loadKillFeed({ backendBaseUrl, serverSlug, nodes, killFeedState });
} finally {
killFeedRefreshInFlight = false;
}
};
void refreshMatchPanels();
void refreshKillFeed();
window.setInterval(() => {
void refresh();
void refreshMatchPanels();
}, CURRENT_MATCH_POLL_INTERVAL_MS);
window.setInterval(() => {
void refreshKillFeed();
}, CURRENT_MATCH_KILL_FEED_POLL_INTERVAL_MS);
});
async function loadCurrentMatch({ backendBaseUrl, serverSlug, nodes }) {
@@ -80,8 +101,11 @@ async function loadCurrentMatch({ backendBaseUrl, serverSlug, nodes }) {
async function loadKillFeed({ backendBaseUrl, serverSlug, nodes, killFeedState }) {
try {
const cursor = killFeedState.latestEventId
? `&since_event_id=${encodeURIComponent(killFeedState.latestEventId)}`
: "";
const payload = await fetchJson(
`${backendBaseUrl}/api/current-match/kills?server=${encodeURIComponent(serverSlug)}&limit=${CURRENT_MATCH_KILL_FEED_LIMIT}`,
`${backendBaseUrl}/api/current-match/kills?server=${encodeURIComponent(serverSlug)}&limit=${CURRENT_MATCH_KILL_FEED_LIMIT}${cursor}`,
);
renderKillFeed(payload?.data || {}, nodes, killFeedState);
} catch (error) {
@@ -108,7 +132,7 @@ function renderCurrentMatch(data, nodes) {
nodes.title.textContent = mapName;
nodes.summary.textContent = serverName;
nodes.note.textContent = data.found
? "Lectura en vivo recibida. La pagina se actualiza cada 30 segundos."
? "Lectura en vivo recibida. El feed de bajas se actualiza en tiempo casi real."
: "Todavia no hay snapshot live disponible para este servidor.";
nodes.scoreboard.href = scoreboardUrl || "./index.html";
nodes.scoreboard.hidden = !scoreboardUrl;
@@ -150,6 +174,7 @@ function initializeKillFeed(nodes) {
nodes.feedList = document.getElementById("current-match-feed-list");
return {
byId: new Map(),
latestEventId: "",
visibleSignature: "",
};
}
@@ -178,6 +203,7 @@ function renderKillFeed(data, nodes, state) {
const incoming = Array.isArray(data.items) ? data.items : [];
if (data.scope === "no-current-match-events") {
state.byId.clear();
state.latestEventId = "";
}
incoming.forEach((event) => {
if (event?.event_id) {
@@ -187,6 +213,8 @@ function renderKillFeed(data, nodes, state) {
const events = [...state.byId.values()]
.sort(compareKillFeedEvents)
.slice(0, CURRENT_MATCH_KILL_FEED_LIMIT);
state.byId = new Map(events.map((event) => [event.event_id, event]));
state.latestEventId = events[0]?.event_id || state.latestEventId;
if (events.length === 0) {
nodes.feedList.innerHTML = "";
state.visibleSignature = "";
@@ -227,7 +255,7 @@ function renderKillFeedRow(event) {
title="${escapeHtml(weapon.label)}"
aria-label="${escapeHtml(weapon.label)}"
>
<span aria-hidden="true">${escapeHtml(weapon.glyph)}</span>
${renderKillFeedWeaponIcon(weapon)}
<em>${escapeHtml(weapon.label)}</em>
</span>
<span class="current-match-killfeed__player current-match-killfeed__player--victim">
@@ -242,10 +270,29 @@ function resolveKillFeedWeapon(value) {
const key = normalizeLookupText(value);
return CURRENT_MATCH_WEAPONS[key] || {
label: String(value || CURRENT_MATCH_WEAPONS.unknown.label),
glyph: CURRENT_MATCH_WEAPONS.unknown.glyph,
icon: CURRENT_MATCH_WEAPONS.unknown.icon,
};
}
function renderKillFeedWeaponIcon(weapon) {
if (!weapon.icon) {
return '<span class="current-match-killfeed__weapon-fallback" aria-hidden="true">?</span>';
}
return `
<img
class="current-match-killfeed__weapon-icon"
src="./assets/img/weapons/${escapeHtml(weapon.icon)}"
alt=""
width="88"
height="32"
loading="lazy"
decoding="async"
onerror="this.hidden = true; this.nextElementSibling.hidden = false;"
/>
<span class="current-match-killfeed__weapon-fallback" aria-hidden="true" hidden>?</span>
`;
}
function renderPlayerStats(data, nodes, state) {
const items = Array.isArray(data.items) ? sortPlayerStats(data.items) : [];
if (items.length === 0) {
@@ -262,6 +309,7 @@ function renderPlayerStats(data, nodes, state) {
.map((item) =>
[
item.player_name,
item.team,
item.kills,
item.deaths,
item.teamkills,
@@ -312,10 +360,15 @@ function renderPlayerStatsTable(items) {
}
function renderPlayerStatsRow(item) {
const team = getPlayerTeamDisplay(item.team);
return `
<tr>
<tr class="historical-player-row historical-player-row--${team.key}">
<td>${escapeHtml(item.player_name || "Jugador no disponible")}</td>
<td>${escapeHtml(item.team || "No disponible")}</td>
<td class="historical-player-team-cell">
<span class="historical-player-team-badge historical-player-team-badge--${team.key}">
${escapeHtml(team.label)}
</span>
</td>
<td>${escapeHtml(formatStatNumber(item.kills))}</td>
<td>${escapeHtml(formatStatNumber(item.deaths))}</td>
<td>${escapeHtml(formatStatNumber(item.teamkills))}</td>
@@ -325,6 +378,17 @@ function renderPlayerStatsRow(item) {
`;
}
function getPlayerTeamDisplay(value) {
const normalized = String(value || "").trim().toLowerCase();
if (normalized === "allies" || normalized === "allied" || normalized === "aliados") {
return { key: "allies", label: "Aliados" };
}
if (normalized === "axis" || normalized === "eje") {
return { key: "axis", label: "Eje" };
}
return { key: "unknown", label: "No disponible" };
}
function toStatNumber(value) {
return Number.isFinite(Number(value)) ? Number(value) : 0;
}