Puntuacion MVP V2
+ ${escapeHtml( + Number.isFinite(scoreValue) ? scoreValue.toFixed(1) : "0.0", + )} +${escapeHtml( + buildMonthlyMvpV2SignalSummary(item), + )}
+diff --git a/ai/tasks/pending/TASK-085-monthly-mvp-v2-ui.md b/ai/tasks/done/TASK-085-monthly-mvp-v2-ui.md
similarity index 100%
rename from ai/tasks/pending/TASK-085-monthly-mvp-v2-ui.md
rename to ai/tasks/done/TASK-085-monthly-mvp-v2-ui.md
diff --git a/ai/tasks/pending/TASK-086-mvp-v1-v2-comparison-panel.md b/ai/tasks/done/TASK-086-mvp-v1-v2-comparison-panel.md
similarity index 100%
rename from ai/tasks/pending/TASK-086-mvp-v1-v2-comparison-panel.md
rename to ai/tasks/done/TASK-086-mvp-v1-v2-comparison-panel.md
diff --git a/frontend/assets/css/historico.css b/frontend/assets/css/historico.css
index 28fa988..75bd8ee 100644
--- a/frontend/assets/css/historico.css
+++ b/frontend/assets/css/historico.css
@@ -200,6 +200,13 @@
gap: 16px;
}
+.historical-mvp-card--v2 {
+ border-color: rgba(187, 143, 72, 0.26);
+ background:
+ radial-gradient(circle at top right, rgba(187, 143, 72, 0.12), transparent 40%),
+ linear-gradient(180deg, rgba(31, 27, 18, 0.96), rgba(12, 15, 11, 0.98));
+}
+
.historical-mvp-card--rank-1 {
border-color: rgba(210, 182, 118, 0.34);
background:
@@ -235,6 +242,20 @@
line-height: 1.3;
}
+.historical-mvp-card__version {
+ display: inline-flex;
+ align-items: center;
+ min-height: 28px;
+ padding: 0 10px;
+ border: 1px solid rgba(187, 143, 72, 0.28);
+ border-radius: 999px;
+ color: var(--accent-warm);
+ font-size: 0.68rem;
+ font-weight: 800;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
.historical-mvp-card__score-label {
margin-bottom: 6px;
color: var(--muted);
@@ -272,6 +293,39 @@
line-height: 1.5;
}
+.historical-mvp-card__signals {
+ display: grid;
+ gap: 10px;
+}
+
+.historical-mvp-card__signal-summary {
+ color: var(--text-soft);
+ font-size: 0.9rem;
+ line-height: 1.5;
+}
+
+.historical-mvp-card__signal-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 10px;
+}
+
+.historical-mvp-card__signal-grid article {
+ padding: 10px 12px;
+ border: 1px solid rgba(187, 143, 72, 0.14);
+ border-radius: 14px;
+ background: rgba(13, 17, 12, 0.42);
+}
+
+.historical-mvp-card__signal-grid span {
+ display: block;
+ margin-bottom: 6px;
+ color: var(--muted);
+ font-size: 0.68rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
.historical-state {
margin: 0 0 16px;
padding: 14px 16px;
@@ -333,6 +387,81 @@
gap: 14px;
}
+.historical-comparison-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
+ gap: 14px;
+}
+
+.historical-comparison-card {
+ display: grid;
+ gap: 14px;
+ padding: 18px;
+ border: 1px solid rgba(159, 168, 141, 0.16);
+ border-radius: 18px;
+ background:
+ radial-gradient(circle at top right, rgba(183, 201, 125, 0.1), transparent 42%),
+ linear-gradient(180deg, rgba(28, 34, 25, 0.94), rgba(12, 15, 11, 0.98));
+ box-shadow: var(--shadow-soft);
+}
+
+.historical-comparison-card__top,
+.historical-comparison-card__scores {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.historical-comparison-card__eyebrow,
+.historical-comparison-card__delta-label,
+.historical-comparison-card__meta span {
+ margin: 0 0 6px;
+ color: var(--muted);
+ font-size: 0.7rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.historical-comparison-card__title {
+ margin: 0;
+ font-size: 1.08rem;
+ line-height: 1.35;
+}
+
+.historical-comparison-card__delta-value {
+ color: var(--accent-strong);
+ font-size: 1.35rem;
+ line-height: 1;
+}
+
+.historical-comparison-card__score-block strong {
+ display: block;
+ font-size: 1rem;
+}
+
+.historical-comparison-card__meta {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 10px;
+}
+
+.historical-comparison-card__meta article {
+ padding: 10px 12px;
+ border: 1px solid rgba(159, 168, 141, 0.14);
+ border-radius: 14px;
+ background: rgba(13, 17, 12, 0.42);
+}
+
+.historical-comparison-card__summary {
+ margin: 0;
+ padding-top: 12px;
+ border-top: 1px solid rgba(159, 168, 141, 0.12);
+ color: var(--text-soft);
+ font-size: 0.9rem;
+ line-height: 1.5;
+}
+
.historical-match-card {
display: grid;
gap: 14px;
@@ -401,6 +530,19 @@
grid-template-columns: 1fr;
}
+ .historical-mvp-card__signal-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .historical-comparison-card__top,
+ .historical-comparison-card__scores {
+ flex-direction: column;
+ }
+
+ .historical-comparison-card__meta {
+ grid-template-columns: 1fr;
+ }
+
.historical-tab {
width: 100%;
}
diff --git a/frontend/assets/js/historico.js b/frontend/assets/js/historico.js
index e94dc54..a516b76 100644
--- a/frontend/assets/js/historico.js
+++ b/frontend/assets/js/historico.js
@@ -100,6 +100,16 @@ document.addEventListener("DOMContentLoaded", () => {
const monthlyMvpSnapshotMetaNode = document.getElementById(
"monthly-mvp-snapshot-meta",
);
+ const monthlyMvpV2TitleNode = document.getElementById("monthly-mvp-v2-title");
+ const monthlyMvpV2StateNode = document.getElementById("monthly-mvp-v2-state");
+ const monthlyMvpV2ListNode = document.getElementById("monthly-mvp-v2-list");
+ const monthlyMvpV2NoteNode = document.getElementById("monthly-mvp-v2-note");
+ const monthlyMvpV2SnapshotMetaNode = document.getElementById(
+ "monthly-mvp-v2-snapshot-meta",
+ );
+ const comparisonStateNode = document.getElementById("mvp-comparison-state");
+ const comparisonListNode = document.getElementById("mvp-comparison-list");
+ const comparisonNoteNode = document.getElementById("mvp-comparison-note");
const weeklyValueHeadingNode = document.getElementById("weekly-leaderboard-value-heading");
const weeklyWindowNoteNode = document.getElementById("weekly-window-note");
const weeklySnapshotMetaNode = document.getElementById(
@@ -123,7 +133,10 @@ document.addEventListener("DOMContentLoaded", () => {
const recentMatchesCache = new Map();
const leaderboardCache = new Map();
const monthlyMvpCache = new Map();
+ const monthlyMvpV2Cache = new Map();
const pendingRequestCache = new Map();
+ let latestMonthlyMvpResult = null;
+ let latestMonthlyMvpV2Result = null;
const getSummarySnapshot = (serverSlug) =>
getCachedJson(
@@ -157,6 +170,14 @@ document.addEventListener("DOMContentLoaded", () => {
`${backendBaseUrl}/api/historical/snapshots/monthly-mvp?server=${encodeURIComponent(serverSlug)}&limit=3`,
);
+ const getMonthlyMvpV2Snapshot = (serverSlug) =>
+ getCachedJson(
+ monthlyMvpV2Cache,
+ pendingRequestCache,
+ buildMonthlyMvpV2SnapshotKey(serverSlug),
+ `${backendBaseUrl}/api/historical/snapshots/monthly-mvp-v2?server=${encodeURIComponent(serverSlug)}&limit=3`,
+ );
+
const refreshServerContent = async () => {
const requestId = activeServerRequestId + 1;
const leaderboardRequestId = activeLeaderboardRequestId + 1;
@@ -184,14 +205,28 @@ document.addEventListener("DOMContentLoaded", () => {
summaryNoteNode.textContent = `La vista esta leyendo snapshots precalculados del historico local para ${activeServerLabel}.`;
setSnapshotMeta(summarySnapshotMetaNode, "Cargando snapshot de resumen...");
renderSummaryLoading(summaryNode);
- monthlyMvpTitleNode.textContent = `Top 3 MVP mensual - ${activeServerLabel}`;
- monthlyMvpNoteNode.textContent = "Cargando periodo del MVP mensual...";
- setState(monthlyMvpStateNode, "Cargando Top 3 MVP mensual...");
+ monthlyMvpTitleNode.textContent = `Top 3 MVP mensual V1 - ${activeServerLabel}`;
+ monthlyMvpNoteNode.textContent = "Cargando periodo del MVP mensual V1...";
+ setState(monthlyMvpStateNode, "Cargando Top 3 MVP mensual V1...");
monthlyMvpListNode.innerHTML = "";
setSnapshotMeta(
monthlyMvpSnapshotMetaNode,
- "Cargando snapshot del MVP mensual...",
+ "Cargando snapshot del MVP mensual V1...",
);
+ monthlyMvpV2TitleNode.textContent = `Top 3 MVP mensual V2 - ${activeServerLabel}`;
+ monthlyMvpV2NoteNode.textContent = "Cargando lectura avanzada del MVP mensual V2...";
+ setState(monthlyMvpV2StateNode, "Cargando Top 3 MVP mensual V2...");
+ monthlyMvpV2ListNode.innerHTML = "";
+ setSnapshotMeta(
+ monthlyMvpV2SnapshotMetaNode,
+ "Cargando snapshot del MVP mensual V2...",
+ );
+ latestMonthlyMvpResult = null;
+ latestMonthlyMvpV2Result = null;
+ comparisonListNode.innerHTML = "";
+ comparisonNoteNode.textContent =
+ "Cargando comparativa entre los rankings mensuales V1 y V2...";
+ setState(comparisonStateNode, "Preparando comparativa V1 vs V2...");
weeklyWindowNoteNode.textContent = "Cargando snapshot del ranking activo...";
setSnapshotMeta(
weeklySnapshotMetaNode,
@@ -221,6 +256,7 @@ document.addEventListener("DOMContentLoaded", () => {
buildMonthlyMvpSnapshotKey(activeServerSlug),
);
if (cachedMonthlyMvpPayload) {
+ latestMonthlyMvpResult = { status: "fulfilled", value: cachedMonthlyMvpPayload };
hydrateMonthlyMvp(
{ status: "fulfilled", value: cachedMonthlyMvpPayload },
monthlyMvpStateNode,
@@ -229,6 +265,36 @@ document.addEventListener("DOMContentLoaded", () => {
monthlyMvpNoteNode,
monthlyMvpSnapshotMetaNode,
);
+ hydrateMvpComparison(
+ latestMonthlyMvpResult,
+ latestMonthlyMvpV2Result,
+ comparisonStateNode,
+ comparisonListNode,
+ comparisonNoteNode,
+ );
+ }
+
+ const cachedMonthlyMvpV2Payload = readCachedPayload(
+ monthlyMvpV2Cache,
+ buildMonthlyMvpV2SnapshotKey(activeServerSlug),
+ );
+ if (cachedMonthlyMvpV2Payload) {
+ latestMonthlyMvpV2Result = { status: "fulfilled", value: cachedMonthlyMvpV2Payload };
+ hydrateMonthlyMvpV2(
+ { status: "fulfilled", value: cachedMonthlyMvpV2Payload },
+ monthlyMvpV2StateNode,
+ monthlyMvpV2ListNode,
+ monthlyMvpV2TitleNode,
+ monthlyMvpV2NoteNode,
+ monthlyMvpV2SnapshotMetaNode,
+ );
+ hydrateMvpComparison(
+ latestMonthlyMvpResult,
+ latestMonthlyMvpV2Result,
+ comparisonStateNode,
+ comparisonListNode,
+ comparisonNoteNode,
+ );
}
const cachedLeaderboardPayload = readCachedPayload(
@@ -358,6 +424,7 @@ document.addEventListener("DOMContentLoaded", () => {
return;
}
+ latestMonthlyMvpResult = monthlyMvpResult;
hydrateMonthlyMvp(
monthlyMvpResult,
monthlyMvpStateNode,
@@ -366,6 +433,43 @@ document.addEventListener("DOMContentLoaded", () => {
monthlyMvpNoteNode,
monthlyMvpSnapshotMetaNode,
);
+ hydrateMvpComparison(
+ latestMonthlyMvpResult,
+ latestMonthlyMvpV2Result,
+ comparisonStateNode,
+ comparisonListNode,
+ comparisonNoteNode,
+ );
+ });
+
+ void settlePromise(getMonthlyMvpV2Snapshot(targetServerSlug)).then((monthlyMvpV2Result) => {
+ if (
+ !isActiveServerRequest(
+ requestId,
+ targetServerSlug,
+ targetTimeframe,
+ targetMetric,
+ )
+ ) {
+ return;
+ }
+
+ latestMonthlyMvpV2Result = monthlyMvpV2Result;
+ hydrateMonthlyMvpV2(
+ monthlyMvpV2Result,
+ monthlyMvpV2StateNode,
+ monthlyMvpV2ListNode,
+ monthlyMvpV2TitleNode,
+ monthlyMvpV2NoteNode,
+ monthlyMvpV2SnapshotMetaNode,
+ );
+ hydrateMvpComparison(
+ latestMonthlyMvpResult,
+ latestMonthlyMvpV2Result,
+ comparisonStateNode,
+ comparisonListNode,
+ comparisonNoteNode,
+ );
});
};
@@ -722,28 +826,28 @@ function hydrateMonthlyMvp(
snapshotMetaNode,
) {
if (result.status !== "fulfilled") {
- titleNode.textContent = `Top 3 MVP mensual - ${getHistoricalServerLabel(activeServerSlug)}`;
- noteNode.textContent = "No se pudo leer el snapshot mensual del MVP.";
- setSnapshotMeta(snapshotMetaNode, "Error al leer el snapshot del MVP mensual.");
- setState(stateNode, "No se pudo cargar el Top 3 MVP mensual.", true);
+ titleNode.textContent = `Top 3 MVP mensual V1 - ${getHistoricalServerLabel(activeServerSlug)}`;
+ noteNode.textContent = "No se pudo leer el snapshot mensual del MVP V1.";
+ setSnapshotMeta(snapshotMetaNode, "Error al leer el snapshot del MVP mensual V1.");
+ setState(stateNode, "No se pudo cargar el Top 3 MVP mensual V1.", true);
listNode.innerHTML = "";
return;
}
const payload = result.value?.data;
- titleNode.textContent = `Top 3 MVP mensual - ${getHistoricalServerLabel(
+ titleNode.textContent = `Top 3 MVP mensual V1 - ${getHistoricalServerLabel(
payload?.server_slug || activeServerSlug,
)}`;
noteNode.textContent = buildMonthlyMvpNote(payload);
setSnapshotMeta(
snapshotMetaNode,
- buildSnapshotMetaText(payload, "Snapshot del MVP mensual pendiente de generacion."),
+ buildSnapshotMetaText(payload, "Snapshot del MVP mensual V1 pendiente de generacion."),
);
if (!payload?.found) {
setState(
stateNode,
- "Todavia no hay un Top 3 MVP mensual listo para el alcance activo.",
+ "Todavia no hay un Top 3 MVP mensual V1 listo para el alcance activo.",
);
listNode.innerHTML = "";
return;
@@ -763,6 +867,107 @@ function hydrateMonthlyMvp(
stateNode.hidden = true;
}
+function hydrateMonthlyMvpV2(
+ result,
+ stateNode,
+ listNode,
+ titleNode,
+ noteNode,
+ snapshotMetaNode,
+) {
+ if (result.status !== "fulfilled") {
+ titleNode.textContent = `Top 3 MVP mensual V2 - ${getHistoricalServerLabel(activeServerSlug)}`;
+ noteNode.textContent = "No se pudo leer el snapshot mensual del MVP V2.";
+ setSnapshotMeta(snapshotMetaNode, "Error al leer el snapshot del MVP mensual V2.");
+ setState(stateNode, "No se pudo cargar el Top 3 MVP mensual V2.", true);
+ listNode.innerHTML = "";
+ return;
+ }
+
+ const payload = result.value?.data;
+ titleNode.textContent = `Top 3 MVP mensual V2 - ${getHistoricalServerLabel(
+ payload?.server_slug || activeServerSlug,
+ )}`;
+ noteNode.textContent = buildMonthlyMvpV2Note(payload);
+ setSnapshotMeta(
+ snapshotMetaNode,
+ buildSnapshotMetaText(payload, "Snapshot del MVP mensual V2 pendiente de generacion."),
+ );
+
+ if (!payload?.found) {
+ setState(
+ stateNode,
+ "Todavia no hay un Top 3 MVP mensual V2 listo para el alcance activo.",
+ );
+ listNode.innerHTML = "";
+ return;
+ }
+
+ const items = payload?.items;
+ if (!Array.isArray(items) || items.length === 0) {
+ setState(
+ stateNode,
+ "No hay jugadores elegibles para el MVP mensual V2 en el periodo activo.",
+ );
+ listNode.innerHTML = "";
+ return;
+ }
+
+ listNode.innerHTML = items.map((item) => renderMonthlyMvpV2Card(item, payload)).join("");
+ stateNode.hidden = true;
+}
+
+function hydrateMvpComparison(
+ monthlyMvpResult,
+ monthlyMvpV2Result,
+ stateNode,
+ listNode,
+ noteNode,
+) {
+ if (!monthlyMvpResult || !monthlyMvpV2Result) {
+ setState(stateNode, "Preparando comparativa V1 vs V2...");
+ return;
+ }
+
+ if (
+ monthlyMvpResult.status !== "fulfilled" ||
+ monthlyMvpV2Result.status !== "fulfilled"
+ ) {
+ noteNode.textContent = "No se pudo completar la lectura conjunta de V1 y V2.";
+ setState(stateNode, "No se pudo cargar la comparativa V1 vs V2.", true);
+ listNode.innerHTML = "";
+ return;
+ }
+
+ const v1Payload = monthlyMvpResult.value?.data;
+ const v2Payload = monthlyMvpV2Result.value?.data;
+ const v1Items = Array.isArray(v1Payload?.items) ? v1Payload.items : [];
+ const v2Items = Array.isArray(v2Payload?.items) ? v2Payload.items : [];
+
+ if (!v1Payload?.found || !v2Payload?.found || !v1Items.length || !v2Items.length) {
+ noteNode.textContent =
+ "La comparativa se activara cuando existan rankings V1 y V2 listos para el mismo alcance.";
+ setState(stateNode, "Todavia no hay una comparativa V1 vs V2 lista para este alcance.");
+ listNode.innerHTML = "";
+ return;
+ }
+
+ const comparisonItems = buildMvpComparisonItems(v1Items, v2Items);
+ if (!comparisonItems.length) {
+ noteNode.textContent =
+ "No se encontraron jugadores coincidentes o relevantes para comparar entre V1 y V2.";
+ setState(stateNode, "Sin diferencias comparables entre V1 y V2 para el alcance activo.");
+ listNode.innerHTML = "";
+ return;
+ }
+
+ noteNode.textContent = buildMvpComparisonNote(v1Payload, v2Payload, comparisonItems.length);
+ listNode.innerHTML = comparisonItems
+ .map((item) => renderMvpComparisonCard(item))
+ .join("");
+ stateNode.hidden = true;
+}
+
function renderRecentMatchCard(item) {
const mapName = item.map?.pretty_name || item.map?.name || "Mapa no disponible";
return `
@@ -862,6 +1067,105 @@ function renderMonthlyMvpCard(item, payload) {
`;
}
+function renderMonthlyMvpV2Card(item, payload) {
+ const scoreValue = Number(item?.mvp_v2_score);
+ return `
+ Puntuacion MVP V2 ${escapeHtml(
+ buildMonthlyMvpV2SignalSummary(item),
+ )} Jugador comparado Delta puesto Score V1 Score V2 ${escapeHtml(item.summary)}${escapeHtml(item.playerName)}
+
MVP mensual
-MVP mensual V1
+- Cargando periodo del MVP mensual... + Cargando baseline del MVP mensual V1...
- Cargando Top 3 MVP mensual... + Cargando Top 3 MVP mensual V1...
MVP mensual V2
++ Cargando lectura avanzada del MVP mensual V2... +
+ ++ Cargando Top 3 MVP mensual V2... +
+ +Comparativa V1 vs V2
++ Cargando comparativa entre los rankings mensuales V1 y V2... +
++ Preparando comparativa V1 vs V2... +
+ +