Add monthly MVP top 3 historical UI
This commit is contained in:
66
ai/tasks/done/TASK-069-monthly-mvp-ui-top3.md
Normal file
66
ai/tasks/done/TASK-069-monthly-mvp-ui-top3.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# TASK-069-monthly-mvp-ui-top3
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Añadir a la página histórica una sección clara y visual para mostrar el Top 3 MVP mensual V1, consumiendo los snapshots/API ya preparados y manteniendo coherencia con la navegación existente.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
El monthly MVP V1 ya está definido a nivel de diseño y, tras las tasks previas, debe quedar disponible en snapshots y API. El siguiente paso es exponerlo en la UI de forma clara, sin convertir la pantalla en algo recargado ni confuso.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
1. Revisar la UI histórica actual y el nuevo endpoint/snapshot del monthly MVP.
|
||||||
|
2. Diseñar una sección visual clara para el Top 3 MVP mensual.
|
||||||
|
3. Mantener compatibilidad con el selector actual de ámbito:
|
||||||
|
- servidor individual
|
||||||
|
- all-servers
|
||||||
|
4. Mostrar, como mínimo:
|
||||||
|
- posición 1, 2 y 3
|
||||||
|
- nombre de jugador
|
||||||
|
- puntuación MVP
|
||||||
|
- metadatos mínimos útiles según el diseño aprobado
|
||||||
|
5. Mostrar el periodo mensual real usado de forma natural.
|
||||||
|
6. Mantener estados de:
|
||||||
|
- loading
|
||||||
|
- empty
|
||||||
|
- error
|
||||||
|
7. Integrar la nueva sección sin romper la navegación actual semanal/mensual de leaderboards por métrica.
|
||||||
|
8. Mantener el rendimiento percibido y la coherencia visual con la página actual.
|
||||||
|
9. No crear páginas nuevas.
|
||||||
|
10. Al completar la implementación:
|
||||||
|
- dejar el repositorio consistente
|
||||||
|
- hacer commit
|
||||||
|
- hacer push al remoto si el entorno lo permite
|
||||||
|
|
||||||
|
## Files to Read First
|
||||||
|
- AGENTS.md
|
||||||
|
- frontend/historico.html
|
||||||
|
- frontend/assets/js/historico.js
|
||||||
|
- frontend/assets/css/historico.css
|
||||||
|
- backend/app/routes.py
|
||||||
|
- backend/app/payloads.py
|
||||||
|
- backend/README.md
|
||||||
|
- docs/monthly-mvp-ranking-scoring-design.md
|
||||||
|
|
||||||
|
## Expected Files to Modify
|
||||||
|
- frontend/historico.html
|
||||||
|
- frontend/assets/js/historico.js
|
||||||
|
- frontend/assets/css/historico.css
|
||||||
|
- opcionalmente backend docs mínimas si hay que reflejar el nuevo bloque visible
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
- No romper la UI histórica actual.
|
||||||
|
- No introducir frameworks nuevos.
|
||||||
|
- No crear páginas nuevas.
|
||||||
|
- No hacer cambios destructivos.
|
||||||
|
- Mantener el trabajo centrado en una sección Top 3 mensual clara y útil.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
- La UI histórica muestra el Top 3 MVP mensual.
|
||||||
|
- Funciona para servidor individual y all-servers.
|
||||||
|
- El periodo mensual mostrado es claro.
|
||||||
|
- Los estados de loading/empty/error son coherentes.
|
||||||
|
- La pantalla sigue siendo usable y visualmente consistente.
|
||||||
|
- Los cambios quedan committeados y se hace push si el entorno lo permite.
|
||||||
|
|
||||||
|
## Change Budget
|
||||||
|
- Preferir menos de 5 archivos modificados.
|
||||||
|
- Preferir menos de 220 líneas cambiadas.
|
||||||
@@ -152,8 +152,15 @@
|
|||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.historical-mvp-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.historical-stat-card,
|
.historical-stat-card,
|
||||||
.historical-match-card {
|
.historical-match-card,
|
||||||
|
.historical-mvp-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
@@ -167,7 +174,10 @@
|
|||||||
.historical-stat-card p,
|
.historical-stat-card p,
|
||||||
.historical-stat-card strong,
|
.historical-stat-card strong,
|
||||||
.historical-match-card p,
|
.historical-match-card p,
|
||||||
.historical-match-card strong {
|
.historical-match-card strong,
|
||||||
|
.historical-mvp-card p,
|
||||||
|
.historical-mvp-card strong,
|
||||||
|
.historical-mvp-card span {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,6 +195,83 @@
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.historical-mvp-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-mvp-card--rank-1 {
|
||||||
|
border-color: rgba(210, 182, 118, 0.34);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top right, rgba(210, 182, 118, 0.16), transparent 42%),
|
||||||
|
linear-gradient(180deg, rgba(36, 32, 20, 0.96), rgba(12, 15, 11, 0.98));
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-mvp-card__top {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-mvp-card__rank {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 42px;
|
||||||
|
min-height: 42px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 1px solid rgba(210, 182, 118, 0.26);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--accent-warm);
|
||||||
|
font-size: 0.76rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-mvp-card__player {
|
||||||
|
font-size: 1.18rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-mvp-card__score-label {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-mvp-card__score-value {
|
||||||
|
color: var(--accent-strong);
|
||||||
|
font-size: 1.8rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-mvp-card__meta {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-mvp-card__meta span {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-mvp-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-state {
|
.historical-state {
|
||||||
margin: 0 0 16px;
|
margin: 0 0 16px;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
@@ -306,6 +393,14 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.historical-mvp-card__top {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-mvp-card__meta {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.historical-tab {
|
.historical-tab {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,13 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const weeklyStateNode = document.getElementById("weekly-leaderboard-state");
|
const weeklyStateNode = document.getElementById("weekly-leaderboard-state");
|
||||||
const weeklyTableNode = document.getElementById("weekly-leaderboard-table");
|
const weeklyTableNode = document.getElementById("weekly-leaderboard-table");
|
||||||
const weeklyBodyNode = document.getElementById("weekly-leaderboard-body");
|
const weeklyBodyNode = document.getElementById("weekly-leaderboard-body");
|
||||||
|
const monthlyMvpTitleNode = document.getElementById("monthly-mvp-title");
|
||||||
|
const monthlyMvpStateNode = document.getElementById("monthly-mvp-state");
|
||||||
|
const monthlyMvpListNode = document.getElementById("monthly-mvp-list");
|
||||||
|
const monthlyMvpNoteNode = document.getElementById("monthly-mvp-note");
|
||||||
|
const monthlyMvpSnapshotMetaNode = document.getElementById(
|
||||||
|
"monthly-mvp-snapshot-meta",
|
||||||
|
);
|
||||||
const weeklyValueHeadingNode = document.getElementById("weekly-leaderboard-value-heading");
|
const weeklyValueHeadingNode = document.getElementById("weekly-leaderboard-value-heading");
|
||||||
const weeklyWindowNoteNode = document.getElementById("weekly-window-note");
|
const weeklyWindowNoteNode = document.getElementById("weekly-window-note");
|
||||||
const weeklySnapshotMetaNode = document.getElementById(
|
const weeklySnapshotMetaNode = document.getElementById(
|
||||||
@@ -115,6 +122,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const summaryCache = new Map();
|
const summaryCache = new Map();
|
||||||
const recentMatchesCache = new Map();
|
const recentMatchesCache = new Map();
|
||||||
const leaderboardCache = new Map();
|
const leaderboardCache = new Map();
|
||||||
|
const monthlyMvpCache = new Map();
|
||||||
const pendingRequestCache = new Map();
|
const pendingRequestCache = new Map();
|
||||||
|
|
||||||
const getSummarySnapshot = (serverSlug) =>
|
const getSummarySnapshot = (serverSlug) =>
|
||||||
@@ -141,6 +149,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
`${backendBaseUrl}/api/historical/snapshots/leaderboard?server=${encodeURIComponent(serverSlug)}&timeframe=${encodeURIComponent(timeframeKey)}&metric=${encodeURIComponent(metricKey)}&limit=10`,
|
`${backendBaseUrl}/api/historical/snapshots/leaderboard?server=${encodeURIComponent(serverSlug)}&timeframe=${encodeURIComponent(timeframeKey)}&metric=${encodeURIComponent(metricKey)}&limit=10`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getMonthlyMvpSnapshot = (serverSlug) =>
|
||||||
|
getCachedJson(
|
||||||
|
monthlyMvpCache,
|
||||||
|
pendingRequestCache,
|
||||||
|
buildMonthlyMvpSnapshotKey(serverSlug),
|
||||||
|
`${backendBaseUrl}/api/historical/snapshots/monthly-mvp?server=${encodeURIComponent(serverSlug)}&limit=3`,
|
||||||
|
);
|
||||||
|
|
||||||
const refreshServerContent = async () => {
|
const refreshServerContent = async () => {
|
||||||
const requestId = activeServerRequestId + 1;
|
const requestId = activeServerRequestId + 1;
|
||||||
const leaderboardRequestId = activeLeaderboardRequestId + 1;
|
const leaderboardRequestId = activeLeaderboardRequestId + 1;
|
||||||
@@ -168,6 +184,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
summaryNoteNode.textContent = `La vista esta leyendo snapshots precalculados del historico local para ${activeServerLabel}.`;
|
summaryNoteNode.textContent = `La vista esta leyendo snapshots precalculados del historico local para ${activeServerLabel}.`;
|
||||||
setSnapshotMeta(summarySnapshotMetaNode, "Cargando snapshot de resumen...");
|
setSnapshotMeta(summarySnapshotMetaNode, "Cargando snapshot de resumen...");
|
||||||
renderSummaryLoading(summaryNode);
|
renderSummaryLoading(summaryNode);
|
||||||
|
monthlyMvpTitleNode.textContent = `Top 3 MVP mensual - ${activeServerLabel}`;
|
||||||
|
monthlyMvpNoteNode.textContent = "Cargando periodo del MVP mensual...";
|
||||||
|
setState(monthlyMvpStateNode, "Cargando Top 3 MVP mensual...");
|
||||||
|
monthlyMvpListNode.innerHTML = "";
|
||||||
|
setSnapshotMeta(
|
||||||
|
monthlyMvpSnapshotMetaNode,
|
||||||
|
"Cargando snapshot del MVP mensual...",
|
||||||
|
);
|
||||||
weeklyWindowNoteNode.textContent = "Cargando snapshot del ranking activo...";
|
weeklyWindowNoteNode.textContent = "Cargando snapshot del ranking activo...";
|
||||||
setSnapshotMeta(
|
setSnapshotMeta(
|
||||||
weeklySnapshotMetaNode,
|
weeklySnapshotMetaNode,
|
||||||
@@ -192,6 +216,21 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cachedMonthlyMvpPayload = readCachedPayload(
|
||||||
|
monthlyMvpCache,
|
||||||
|
buildMonthlyMvpSnapshotKey(activeServerSlug),
|
||||||
|
);
|
||||||
|
if (cachedMonthlyMvpPayload) {
|
||||||
|
hydrateMonthlyMvp(
|
||||||
|
{ status: "fulfilled", value: cachedMonthlyMvpPayload },
|
||||||
|
monthlyMvpStateNode,
|
||||||
|
monthlyMvpListNode,
|
||||||
|
monthlyMvpTitleNode,
|
||||||
|
monthlyMvpNoteNode,
|
||||||
|
monthlyMvpSnapshotMetaNode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const cachedLeaderboardPayload = readCachedPayload(
|
const cachedLeaderboardPayload = readCachedPayload(
|
||||||
leaderboardCache,
|
leaderboardCache,
|
||||||
buildLeaderboardSnapshotKey(
|
buildLeaderboardSnapshotKey(
|
||||||
@@ -306,6 +345,28 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
targetTimeframe,
|
targetTimeframe,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void settlePromise(getMonthlyMvpSnapshot(targetServerSlug)).then((monthlyMvpResult) => {
|
||||||
|
if (
|
||||||
|
!isActiveServerRequest(
|
||||||
|
requestId,
|
||||||
|
targetServerSlug,
|
||||||
|
targetTimeframe,
|
||||||
|
targetMetric,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hydrateMonthlyMvp(
|
||||||
|
monthlyMvpResult,
|
||||||
|
monthlyMvpStateNode,
|
||||||
|
monthlyMvpListNode,
|
||||||
|
monthlyMvpTitleNode,
|
||||||
|
monthlyMvpNoteNode,
|
||||||
|
monthlyMvpSnapshotMetaNode,
|
||||||
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshLeaderboardContent = async () => {
|
const refreshLeaderboardContent = async () => {
|
||||||
@@ -559,7 +620,7 @@ function hydrateWeeklyLeaderboard(
|
|||||||
resolvedTimeframeKey,
|
resolvedTimeframeKey,
|
||||||
);
|
);
|
||||||
noteNode.textContent =
|
noteNode.textContent =
|
||||||
"No se pudo leer el snapshot precalculado para esta metrica.";
|
"No se pudo leer los datos precalculados para esta metrica.";
|
||||||
setSnapshotMeta(snapshotMetaNode, "Error al leer el snapshot del ranking.");
|
setSnapshotMeta(snapshotMetaNode, "Error al leer el snapshot del ranking.");
|
||||||
setState(
|
setState(
|
||||||
stateNode,
|
stateNode,
|
||||||
@@ -652,6 +713,56 @@ function hydrateRecentMatches(result, stateNode, listNode, snapshotMetaNode) {
|
|||||||
stateNode.hidden = true;
|
stateNode.hidden = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hydrateMonthlyMvp(
|
||||||
|
result,
|
||||||
|
stateNode,
|
||||||
|
listNode,
|
||||||
|
titleNode,
|
||||||
|
noteNode,
|
||||||
|
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);
|
||||||
|
listNode.innerHTML = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = result.value?.data;
|
||||||
|
titleNode.textContent = `Top 3 MVP mensual - ${getHistoricalServerLabel(
|
||||||
|
payload?.server_slug || activeServerSlug,
|
||||||
|
)}`;
|
||||||
|
noteNode.textContent = buildMonthlyMvpNote(payload);
|
||||||
|
setSnapshotMeta(
|
||||||
|
snapshotMetaNode,
|
||||||
|
buildSnapshotMetaText(payload, "Snapshot del MVP mensual pendiente de generacion."),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!payload?.found) {
|
||||||
|
setState(
|
||||||
|
stateNode,
|
||||||
|
"Todavia no hay un Top 3 MVP mensual 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 en el periodo activo.",
|
||||||
|
);
|
||||||
|
listNode.innerHTML = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
listNode.innerHTML = items.map((item) => renderMonthlyMvpCard(item, payload)).join("");
|
||||||
|
stateNode.hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
function renderRecentMatchCard(item) {
|
function renderRecentMatchCard(item) {
|
||||||
const mapName = item.map?.pretty_name || item.map?.name || "Mapa no disponible";
|
const mapName = item.map?.pretty_name || item.map?.name || "Mapa no disponible";
|
||||||
return `
|
return `
|
||||||
@@ -706,6 +817,51 @@ function renderSummaryCard(label, value) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderMonthlyMvpCard(item, payload) {
|
||||||
|
const scoreValue = Number(item?.mvp_score);
|
||||||
|
return `
|
||||||
|
<article class="historical-mvp-card historical-mvp-card--rank-${escapeHtml(item?.ranking_position || "x")}">
|
||||||
|
<div class="historical-mvp-card__top">
|
||||||
|
<div>
|
||||||
|
<span class="historical-mvp-card__rank">#${escapeHtml(item?.ranking_position || "-")}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="historical-mvp-card__score-label">Puntuacion MVP</p>
|
||||||
|
<strong class="historical-mvp-card__score-value">${escapeHtml(
|
||||||
|
Number.isFinite(scoreValue) ? scoreValue.toFixed(1) : "0.0",
|
||||||
|
)}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong class="historical-mvp-card__player">${escapeHtml(
|
||||||
|
item?.player?.name || "Jugador no identificado",
|
||||||
|
)}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="historical-mvp-card__meta">
|
||||||
|
<article>
|
||||||
|
<span>Kills</span>
|
||||||
|
<strong>${escapeHtml(formatNumber(item?.totals?.kills))}</strong>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<span>Soporte</span>
|
||||||
|
<strong>${escapeHtml(formatNumber(item?.totals?.support))}</strong>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<span>KPM</span>
|
||||||
|
<strong>${escapeHtml(formatDecimal(item?.derived?.kpm, 2))}</strong>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<span>KDA</span>
|
||||||
|
<strong>${escapeHtml(formatDecimal(item?.derived?.kda, 2))}</strong>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<p class="historical-mvp-card__footer">
|
||||||
|
${escapeHtml(buildMonthlyMvpFooter(item, payload))}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function setState(node, message, isError = false) {
|
function setState(node, message, isError = false) {
|
||||||
node.textContent = message;
|
node.textContent = message;
|
||||||
node.hidden = false;
|
node.hidden = false;
|
||||||
@@ -807,6 +963,10 @@ function buildLeaderboardSnapshotKey(serverSlug, timeframeKey, metricKey) {
|
|||||||
return `leaderboard:${serverSlug}:${timeframeKey}:${metricKey}`;
|
return `leaderboard:${serverSlug}:${timeframeKey}:${metricKey}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildMonthlyMvpSnapshotKey(serverSlug) {
|
||||||
|
return `monthly-mvp:${serverSlug}`;
|
||||||
|
}
|
||||||
|
|
||||||
function buildRangeLabel(start, end) {
|
function buildRangeLabel(start, end) {
|
||||||
if (!start && !end) {
|
if (!start && !end) {
|
||||||
return "";
|
return "";
|
||||||
@@ -848,7 +1008,7 @@ function buildCoveragePeriodLabel(coverage, timeRange, serverSlug) {
|
|||||||
function buildSummaryNote(summaryBasis, weeklyWindowDays, coverage, serverSlug) {
|
function buildSummaryNote(summaryBasis, weeklyWindowDays, coverage, serverSlug) {
|
||||||
const basisLabel =
|
const basisLabel =
|
||||||
summaryBasis === "snapshot-precomputed"
|
summaryBasis === "snapshot-precomputed"
|
||||||
? "el snapshot precalculado del historico local"
|
? "el historico local"
|
||||||
: "el historico persistido disponible";
|
: "el historico persistido disponible";
|
||||||
const status = coverage?.status;
|
const status = coverage?.status;
|
||||||
void weeklyWindowDays;
|
void weeklyWindowDays;
|
||||||
@@ -857,7 +1017,7 @@ function buildSummaryNote(summaryBasis, weeklyWindowDays, coverage, serverSlug)
|
|||||||
return `Este bloque resume ${basisLabel}. La cobertura registrada todavia es inicial y puede crecer en los proximos dias.`;
|
return `Este bloque resume ${basisLabel}. La cobertura registrada todavia es inicial y puede crecer en los proximos dias.`;
|
||||||
}
|
}
|
||||||
if (serverSlug === "all-servers") {
|
if (serverSlug === "all-servers") {
|
||||||
return `Resumen global servido desde ${basisLabel} y combinado solo con los servidores que ya tienen historico disponible.`;
|
return `Resumen de los servidore desde ${basisLabel} y combinado solo los servidores actuales de la comunidad.`;
|
||||||
}
|
}
|
||||||
return `Resumen servido desde ${basisLabel}.`;
|
return `Resumen servido desde ${basisLabel}.`;
|
||||||
}
|
}
|
||||||
@@ -867,7 +1027,7 @@ function buildWeeklyWindowNote(payload) {
|
|||||||
const timeframeLabel = getLeaderboardTimeframeConfig(
|
const timeframeLabel = getLeaderboardTimeframeConfig(
|
||||||
payload?.timeframe || activeLeaderboardTimeframe,
|
payload?.timeframe || activeLeaderboardTimeframe,
|
||||||
).shortLabel;
|
).shortLabel;
|
||||||
return `No existe un snapshot ${timeframeLabel} listo para esta metrica en el alcance activo.`;
|
return `No existen datos en ${timeframeLabel} suficientes para esta metrica en el rango activo.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = formatTimestamp(payload?.window_start);
|
const start = formatTimestamp(payload?.window_start);
|
||||||
@@ -899,6 +1059,26 @@ function buildRecentMatchesNote(serverSlug) {
|
|||||||
return `Lista de cierres ya registrados para ${getHistoricalServerLabel(serverSlug)}.`;
|
return `Lista de cierres ya registrados para ${getHistoricalServerLabel(serverSlug)}.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildMonthlyMvpNote(payload) {
|
||||||
|
if (!payload?.found) {
|
||||||
|
return "El Top 3 mensual aparecera cuando exista un snapshot MVP listo para este alcance.";
|
||||||
|
}
|
||||||
|
const periodLabel =
|
||||||
|
payload?.window_label && payload?.month_key
|
||||||
|
? `${payload.window_label} (${formatMonthKey(payload.month_key)})`
|
||||||
|
: formatMonthKey(payload?.month_key);
|
||||||
|
const eligiblePlayers = formatNumber(payload?.eligible_players_count);
|
||||||
|
return `${periodLabel || "Periodo mensual activo"}. ${eligiblePlayers} jugadores cumplen los umbrales de elegibilidad.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMonthlyMvpFooter(item, payload) {
|
||||||
|
const hoursPlayed = Number(item?.totals?.time_seconds) / 3600;
|
||||||
|
const monthLabel = formatMonthKey(payload?.month_key);
|
||||||
|
return `${monthLabel || "Mes activo"} · ${formatNumber(
|
||||||
|
item?.matches_considered,
|
||||||
|
)} partidas · ${formatDecimal(hoursPlayed, 1)} h jugadas`;
|
||||||
|
}
|
||||||
|
|
||||||
function buildSnapshotMetaText(payload, missingMessage) {
|
function buildSnapshotMetaText(payload, missingMessage) {
|
||||||
if (!payload?.generated_at) {
|
if (!payload?.generated_at) {
|
||||||
return missingMessage;
|
return missingMessage;
|
||||||
@@ -977,6 +1157,35 @@ function formatNumber(value) {
|
|||||||
return new Intl.NumberFormat("es-ES").format(parsedValue);
|
return new Intl.NumberFormat("es-ES").format(parsedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDecimal(value, fractionDigits = 1) {
|
||||||
|
const parsedValue = Number(value);
|
||||||
|
if (!Number.isFinite(parsedValue)) {
|
||||||
|
return "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.NumberFormat("es-ES", {
|
||||||
|
minimumFractionDigits: fractionDigits,
|
||||||
|
maximumFractionDigits: fractionDigits,
|
||||||
|
}).format(parsedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMonthKey(monthKey) {
|
||||||
|
if (!monthKey) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = new Date(`${monthKey}-01T00:00:00Z`);
|
||||||
|
if (Number.isNaN(value.getTime())) {
|
||||||
|
return monthKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat("es-ES", {
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
timeZone: "UTC",
|
||||||
|
}).format(value);
|
||||||
|
}
|
||||||
|
|
||||||
function formatTimestamp(timestamp) {
|
function formatTimestamp(timestamp) {
|
||||||
if (!timestamp) {
|
if (!timestamp) {
|
||||||
return "Fecha no disponible";
|
return "Fecha no disponible";
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="content historical-content">
|
<main class="content historical-content">
|
||||||
<section class="panel historical-panel" aria-labelledby="historical-summary-title">
|
<section class="panel historical-panel" aria-labelledby="historical-summary-title" hidden>
|
||||||
<div class="panel__shell">
|
<div class="panel__shell">
|
||||||
<div class="panel__header historical-panel__header">
|
<div class="panel__header historical-panel__header">
|
||||||
<div>
|
<div>
|
||||||
@@ -107,6 +107,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="panel historical-panel" aria-labelledby="monthly-mvp-title">
|
||||||
|
<div class="panel__shell">
|
||||||
|
<div class="panel__header historical-panel__header">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow eyebrow--section">MVP mensual</p>
|
||||||
|
<h2 id="monthly-mvp-title">Top 3 del alcance activo</h2>
|
||||||
|
<p class="historical-panel__note" id="monthly-mvp-note">
|
||||||
|
Cargando periodo del MVP mensual...
|
||||||
|
</p>
|
||||||
|
<p class="historical-snapshot-meta" id="monthly-mvp-snapshot-meta">
|
||||||
|
Cargando snapshot del MVP mensual...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="historical-state" id="monthly-mvp-state" aria-live="polite">
|
||||||
|
Cargando Top 3 MVP mensual...
|
||||||
|
</p>
|
||||||
|
<div class="historical-mvp-grid" id="monthly-mvp-list"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="panel historical-panel" aria-labelledby="weekly-ranking-title">
|
<section class="panel historical-panel" aria-labelledby="weekly-ranking-title">
|
||||||
<div class="panel__shell">
|
<div class="panel__shell">
|
||||||
<div class="panel__header historical-panel__header">
|
<div class="panel__header historical-panel__header">
|
||||||
|
|||||||
Reference in New Issue
Block a user