Add rcon-first elo mmr foundation
This commit is contained in:
@@ -158,9 +158,16 @@
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.historical-elo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.historical-stat-card,
|
||||
.historical-match-card,
|
||||
.historical-mvp-card {
|
||||
.historical-mvp-card,
|
||||
.historical-elo-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 18px;
|
||||
@@ -177,7 +184,10 @@
|
||||
.historical-match-card strong,
|
||||
.historical-mvp-card p,
|
||||
.historical-mvp-card strong,
|
||||
.historical-mvp-card span {
|
||||
.historical-mvp-card span,
|
||||
.historical-elo-card p,
|
||||
.historical-elo-card strong,
|
||||
.historical-elo-card span {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -298,6 +308,91 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.historical-elo-card {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
border-color: rgba(96, 150, 124, 0.24);
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(96, 150, 124, 0.12), transparent 40%),
|
||||
linear-gradient(180deg, rgba(21, 32, 27, 0.96), rgba(12, 15, 11, 0.98));
|
||||
}
|
||||
|
||||
.historical-elo-card__top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.historical-elo-card__rank {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 42px;
|
||||
min-height: 42px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid rgba(96, 150, 124, 0.28);
|
||||
border-radius: 999px;
|
||||
color: #a8d4bf;
|
||||
font-size: 0.76rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.historical-elo-card__accuracy {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid rgba(96, 150, 124, 0.24);
|
||||
border-radius: 999px;
|
||||
color: #a8d4bf;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.historical-elo-card__meta,
|
||||
.historical-elo-card__scores {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.historical-elo-card__meta article,
|
||||
.historical-elo-card__scores article {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(96, 150, 124, 0.14);
|
||||
border-radius: 14px;
|
||||
background: rgba(13, 17, 12, 0.42);
|
||||
}
|
||||
|
||||
.historical-elo-card__meta span,
|
||||
.historical-elo-card__scores span {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
color: var(--muted);
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.historical-elo-card__summary {
|
||||
color: var(--text-soft);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.historical-elo-card__footer {
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid rgba(159, 168, 141, 0.12);
|
||||
color: var(--text-soft);
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.historical-mvp-card__signal-summary {
|
||||
color: var(--text-soft);
|
||||
font-size: 0.9rem;
|
||||
|
||||
@@ -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 "";
|
||||
|
||||
@@ -167,6 +167,27 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel historical-panel" aria-labelledby="elo-mmr-title">
|
||||
<div class="panel__shell">
|
||||
<div class="panel__header historical-panel__header">
|
||||
<div>
|
||||
<p class="eyebrow eyebrow--section">Elo/MMR mensual</p>
|
||||
<h2 id="elo-mmr-title">Rating persistente y ranking mensual del alcance activo</h2>
|
||||
<p class="historical-panel__note" id="elo-mmr-note">
|
||||
Cargando sistema mensual de rating...
|
||||
</p>
|
||||
<p class="historical-snapshot-meta" id="elo-mmr-meta">
|
||||
Cargando metadata de Elo/MMR...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="historical-state" id="elo-mmr-state" aria-live="polite">
|
||||
Cargando leaderboard Elo/MMR...
|
||||
</p>
|
||||
<div class="historical-elo-grid" id="elo-mmr-list"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel historical-panel" aria-labelledby="weekly-ranking-title">
|
||||
<div class="panel__shell">
|
||||
<div class="panel__header historical-panel__header">
|
||||
|
||||
Reference in New Issue
Block a user