Add rcon-first elo mmr foundation

This commit is contained in:
devRaGonSa
2026-03-25 17:29:29 +01:00
parent 787e753f77
commit 6ed416f79a
18 changed files with 2282 additions and 33 deletions

View File

@@ -110,6 +110,10 @@ document.addEventListener("DOMContentLoaded", () => {
const comparisonStateNode = document.getElementById("mvp-comparison-state");
const comparisonListNode = document.getElementById("mvp-comparison-list");
const comparisonNoteNode = document.getElementById("mvp-comparison-note");
const eloMmrStateNode = document.getElementById("elo-mmr-state");
const eloMmrListNode = document.getElementById("elo-mmr-list");
const eloMmrNoteNode = document.getElementById("elo-mmr-note");
const eloMmrMetaNode = document.getElementById("elo-mmr-meta");
const weeklyValueHeadingNode = document.getElementById("weekly-leaderboard-value-heading");
const weeklyWindowNoteNode = document.getElementById("weekly-window-note");
const weeklySnapshotMetaNode = document.getElementById(
@@ -134,6 +138,7 @@ document.addEventListener("DOMContentLoaded", () => {
const leaderboardCache = new Map();
const monthlyMvpCache = new Map();
const monthlyMvpV2Cache = new Map();
const eloMmrCache = new Map();
const pendingRequestCache = new Map();
let latestMonthlyMvpResult = null;
let latestMonthlyMvpV2Result = null;
@@ -178,6 +183,14 @@ document.addEventListener("DOMContentLoaded", () => {
`${backendBaseUrl}/api/historical/snapshots/monthly-mvp-v2?server=${encodeURIComponent(serverSlug)}&limit=3`,
);
const getEloMmrLeaderboard = (serverSlug) =>
getCachedJson(
eloMmrCache,
pendingRequestCache,
buildEloMmrSnapshotKey(serverSlug),
`${backendBaseUrl}/api/historical/elo-mmr/leaderboard?server=${encodeURIComponent(serverSlug)}&limit=5`,
);
const refreshServerContent = async () => {
const requestId = activeServerRequestId + 1;
const leaderboardRequestId = activeLeaderboardRequestId + 1;
@@ -227,6 +240,10 @@ document.addEventListener("DOMContentLoaded", () => {
comparisonNoteNode.textContent =
"Cargando comparativa entre los rankings mensuales V1 y V2...";
setState(comparisonStateNode, "Preparando comparativa V1 vs V2...");
eloMmrListNode.innerHTML = "";
eloMmrNoteNode.textContent = "Cargando lectura del rating persistente y score mensual...";
setState(eloMmrStateNode, "Cargando leaderboard Elo/MMR...");
setSnapshotMeta(eloMmrMetaNode, "Cargando metadata de Elo/MMR...");
weeklyWindowNoteNode.textContent = "Cargando snapshot del ranking activo...";
setSnapshotMeta(
weeklySnapshotMetaNode,
@@ -297,6 +314,20 @@ document.addEventListener("DOMContentLoaded", () => {
);
}
const cachedEloMmrPayload = readCachedPayload(
eloMmrCache,
buildEloMmrSnapshotKey(activeServerSlug),
);
if (cachedEloMmrPayload) {
hydrateEloMmr(
{ status: "fulfilled", value: cachedEloMmrPayload },
eloMmrStateNode,
eloMmrListNode,
eloMmrNoteNode,
eloMmrMetaNode,
);
}
const cachedLeaderboardPayload = readCachedPayload(
leaderboardCache,
buildLeaderboardSnapshotKey(
@@ -471,6 +502,27 @@ document.addEventListener("DOMContentLoaded", () => {
comparisonNoteNode,
);
});
void settlePromise(getEloMmrLeaderboard(targetServerSlug)).then((eloMmrResult) => {
if (
!isActiveServerRequest(
requestId,
targetServerSlug,
targetTimeframe,
targetMetric,
)
) {
return;
}
hydrateEloMmr(
eloMmrResult,
eloMmrStateNode,
eloMmrListNode,
eloMmrNoteNode,
eloMmrMetaNode,
);
});
};
const refreshLeaderboardContent = async () => {
@@ -1166,6 +1218,89 @@ function renderMvpComparisonCard(item) {
`;
}
function hydrateEloMmr(result, stateNode, listNode, noteNode, metaNode) {
if (result?.status !== "fulfilled") {
setState(stateNode, "No se pudo cargar el leaderboard Elo/MMR.", true);
listNode.innerHTML = "";
noteNode.textContent =
"El sistema Elo/MMR sigue disponible solo cuando existe rebuild persistido.";
setSnapshotMeta(metaNode, "Error al cargar metadata de Elo/MMR.");
return;
}
const payload = result.value?.data;
const items = Array.isArray(payload?.items) ? payload.items : [];
if (!payload?.found || !items.length) {
setState(
stateNode,
"Todavia no hay leaderboard Elo/MMR mensual listo para este alcance.",
);
listNode.innerHTML = "";
noteNode.textContent =
"El bloque aparece cuando existe un rebuild persistido con jugadores elegibles.";
setSnapshotMeta(
metaNode,
buildEloMmrMeta(payload),
);
return;
}
stateNode.hidden = true;
noteNode.textContent = buildEloMmrNote(payload);
setSnapshotMeta(metaNode, buildEloMmrMeta(payload));
listNode.innerHTML = items.map((item) => renderEloMmrCard(item, payload)).join("");
}
function renderEloMmrCard(item, payload) {
return `
<article class="historical-elo-card">
<div class="historical-elo-card__top">
<div>
<span class="historical-elo-card__rank">#${escapeHtml(item?.ranking_position || "-")}</span>
</div>
<div>
<span class="historical-elo-card__accuracy">${escapeHtml(formatAccuracyMode(item?.accuracy_mode))}</span>
</div>
</div>
<div>
<strong class="historical-mvp-card__player">${escapeHtml(
item?.player?.name || "Jugador no identificado",
)}</strong>
</div>
<div class="historical-elo-card__scores">
<article>
<span>Score mensual</span>
<strong>${escapeHtml(formatDecimal(item?.monthly_rank_score, 2))}</strong>
</article>
<article>
<span>MMR persistente</span>
<strong>${escapeHtml(formatDecimal(item?.persistent_rating?.mmr, 1))}</strong>
</article>
</div>
<div class="historical-elo-card__meta">
<article>
<span>MMR gain</span>
<strong>${escapeHtml(formatSignedDecimal(item?.persistent_rating?.mmr_gain, 1))}</strong>
</article>
<article>
<span>Elegibilidad</span>
<strong>${escapeHtml(item?.eligible ? "Elegible" : "Parcial")}</strong>
</article>
<article>
<span>Partidas validas</span>
<strong>${escapeHtml(formatNumber(item?.valid_matches))}</strong>
</article>
<article>
<span>Confidence</span>
<strong>${escapeHtml(formatDecimal(item?.components?.confidence, 1))}</strong>
</article>
</div>
<p class="historical-elo-card__summary">${escapeHtml(buildEloMmrSummary(item))}</p>
<p class="historical-elo-card__footer">${escapeHtml(buildEloMmrFooter(item, payload))}</p>
</article>
`;
}
function setState(node, message, isError = false) {
node.textContent = message;
node.hidden = false;
@@ -1275,6 +1410,10 @@ function buildMonthlyMvpV2SnapshotKey(serverSlug) {
return `monthly-mvp-v2:${serverSlug}`;
}
function buildEloMmrSnapshotKey(serverSlug) {
return `elo-mmr:${serverSlug}`;
}
function buildRangeLabel(start, end) {
if (!start && !end) {
return "";
@@ -1414,6 +1553,31 @@ function buildMonthlyMvpV2Footer(item, payload) {
)} partidas · ${formatDecimal(hoursPlayed, 1)} h jugadas`;
}
function buildEloMmrNote(payload) {
const monthLabel = formatMonthKey(payload?.month_key);
const exactRatio = Number(payload?.capabilities_summary?.exact_ratio || 0);
const approximateRatio = Number(payload?.capabilities_summary?.approximate_ratio || 0);
return `${monthLabel || "Mes activo"}. Rating persistente + score mensual con ${formatPercent(exactRatio)} de senal exacta y ${formatPercent(approximateRatio)} de senal aproximada en este corte.`;
}
function buildEloMmrMeta(payload) {
const sourceLabel = payload?.selected_source || payload?.source || "origen no disponible";
const fallbackLabel = payload?.fallback_used
? `fallback ${payload?.fallback_reason || "activo"}`
: "sin fallback";
return `Generado ${formatTimestamp(payload?.generated_at)} · fuente ${sourceLabel} · ${fallbackLabel}`;
}
function buildEloMmrSummary(item) {
return `AvgMatchScore ${formatDecimal(item?.components?.avg_match_score, 1)}, actividad ${formatDecimal(item?.components?.activity, 1)} y strength-of-schedule ${formatDecimal(item?.components?.strength_of_schedule, 1)}.`;
}
function buildEloMmrFooter(item, payload) {
const monthLabel = formatMonthKey(payload?.month_key);
const hoursPlayed = Number(item?.total_time_seconds) / 3600;
return `${monthLabel || "Mes activo"} · ${formatNumber(item?.valid_matches)} validas / ${formatNumber(item?.total_matches)} totales · ${formatDecimal(hoursPlayed, 1)} h`;
}
function buildMvpComparisonItems(v1Items, v2Items) {
const v1TopItems = v1Items.slice(0, 3);
const v2TopItems = v2Items.slice(0, 3);
@@ -1612,6 +1776,28 @@ function formatPercent(value) {
}).format(parsedValue * 100)} %`;
}
function formatSignedDecimal(value, fractionDigits = 1) {
const parsedValue = Number(value);
if (!Number.isFinite(parsedValue)) {
return "0";
}
const prefix = parsedValue > 0 ? "+" : "";
return `${prefix}${formatDecimal(parsedValue, fractionDigits)}`;
}
function formatAccuracyMode(mode) {
if (mode === "exact") {
return "Exacto";
}
if (mode === "approximate") {
return "Aproximado";
}
if (mode === "partial") {
return "Parcial";
}
return "Mixto";
}
function formatMonthKey(monthKey) {
if (!monthKey) {
return "";