Merge remote-tracking branch 'origin/fix/rcon-scoreboard-layout-actual'

This commit is contained in:
devRaGonSa
2026-05-20 09:32:46 +02:00
3 changed files with 188 additions and 146 deletions

View File

@@ -0,0 +1,111 @@
.historical-scoreboard-detail {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(180px, 280px) minmax(0, 1fr);
align-items: stretch;
gap: 14px;
margin-bottom: 14px;
padding: 18px;
border: 1px solid rgba(210, 182, 118, 0.24);
border-radius: 22px;
background:
radial-gradient(circle at top center, rgba(210, 182, 118, 0.14), transparent 44%),
linear-gradient(180deg, rgba(29, 34, 24, 0.96), rgba(11, 14, 10, 0.98));
box-shadow: var(--shadow-soft);
}
.historical-scoreboard-detail__side,
.historical-scoreboard-detail__score {
display: grid;
align-content: center;
justify-items: center;
min-height: 142px;
padding: 18px;
border: 1px solid rgba(159, 168, 141, 0.14);
border-radius: 18px;
background: rgba(13, 17, 12, 0.48);
text-align: center;
}
.historical-scoreboard-detail__side span,
.historical-scoreboard-detail__score span,
.historical-match-compact-meta span {
margin-bottom: 7px;
color: var(--muted);
font-size: 0.72rem;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.historical-scoreboard-detail__side strong {
color: var(--text);
font-size: clamp(1.35rem, 2.8vw, 2.6rem);
line-height: 1;
}
.historical-scoreboard-detail__side em,
.historical-scoreboard-detail__score em {
min-height: 1.2em;
margin-top: 10px;
color: var(--accent-strong);
font-style: normal;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.historical-scoreboard-detail__side.is-winner {
border-color: rgba(210, 182, 118, 0.42);
background:
radial-gradient(circle at top, rgba(210, 182, 118, 0.14), transparent 55%),
rgba(25, 24, 16, 0.7);
}
.historical-scoreboard-detail__score {
border-color: rgba(210, 182, 118, 0.36);
background:
radial-gradient(circle at center, rgba(210, 182, 118, 0.18), transparent 62%),
rgba(11, 14, 10, 0.72);
}
.historical-scoreboard-detail__score strong {
color: var(--text);
font-size: clamp(3.4rem, 7vw, 6.2rem);
line-height: 0.95;
letter-spacing: 0.02em;
}
.historical-match-compact-meta {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 170px), 1fr));
gap: 12px;
}
.historical-match-compact-meta article {
padding: 13px 14px;
border: 1px solid rgba(159, 168, 141, 0.14);
border-radius: 16px;
background: rgba(13, 17, 12, 0.44);
}
.historical-match-compact-meta strong {
display: block;
color: var(--text);
font-size: 1rem;
line-height: 1.35;
}
@media (max-width: 720px) {
.historical-scoreboard-detail {
grid-template-columns: 1fr;
padding: 14px;
}
.historical-scoreboard-detail__side,
.historical-scoreboard-detail__score {
min-height: 112px;
}
.historical-scoreboard-detail__score {
order: -1;
}
}

View File

@@ -25,7 +25,7 @@ document.addEventListener("DOMContentLoaded", () => {
if (!serverSlug || !matchId) { if (!serverSlug || !matchId) {
nodes.title.textContent = "Partida no seleccionada"; nodes.title.textContent = "Partida no seleccionada";
nodes.summary.textContent = "Vuelve al historico y abre una partida registrada."; nodes.summary.textContent = "Vuelve al historico y abre una partida registrada.";
nodes.note.textContent = "Faltan parametros internos para cargar este detalle."; nodes.note.textContent = "";
setState(nodes.state, "No hay una partida seleccionada.", true); setState(nodes.state, "No hay una partida seleccionada.", true);
return; return;
} }
@@ -46,8 +46,7 @@ async function loadMatchDetail({ backendBaseUrl, serverSlug, matchId, nodes }) {
nodes.title.textContent = "Detalle no disponible"; nodes.title.textContent = "Detalle no disponible";
nodes.summary.textContent = nodes.summary.textContent =
"La partida existe como enlace interno, pero todavia no hay detalle suficiente para mostrar."; "La partida existe como enlace interno, pero todavia no hay detalle suficiente para mostrar.";
nodes.note.textContent = nodes.note.textContent = "";
"El historico local puede tener solo una ventana RCON parcial o ningun registro ampliado.";
setState(nodes.state, "Detalle no disponible para esta partida."); setState(nodes.state, "Detalle no disponible para esta partida.");
return; return;
} }
@@ -56,7 +55,7 @@ async function loadMatchDetail({ backendBaseUrl, serverSlug, matchId, nodes }) {
} catch (error) { } catch (error) {
nodes.title.textContent = "Detalle no disponible"; nodes.title.textContent = "Detalle no disponible";
nodes.summary.textContent = "No se pudo conectar con el backend local."; nodes.summary.textContent = "No se pudo conectar con el backend local.";
nodes.note.textContent = "Comprueba que el backend este levantado y vuelve a intentarlo."; nodes.note.textContent = "";
setState(nodes.state, "Error al cargar el detalle de la partida.", true); setState(nodes.state, "Error al cargar el detalle de la partida.", true);
} }
} }
@@ -66,31 +65,81 @@ function renderMatchDetail(item, nodes) {
const serverName = item.server?.name || item.server?.slug || "Servidor no disponible"; const serverName = item.server?.name || item.server?.slug || "Servidor no disponible";
nodes.title.textContent = mapName; nodes.title.textContent = mapName;
nodes.summary.textContent = `${serverName} - ${formatDetailSubtitle(item)}`; nodes.summary.textContent = `${serverName} - ${formatDetailSubtitle(item)}`;
nodes.note.textContent = buildDetailNote(item); nodes.note.textContent = "";
nodes.grid.innerHTML = [ nodes.grid.innerHTML = renderScoreboardDetail(item, { mapName, serverName });
renderDetailCard("Servidor", serverName),
renderDetailCard("Mapa", mapName),
renderDetailCard("Modo", formatGameMode(item.game_mode || item.gamestate?.game_mode)),
renderDetailCard("Marcador", formatScore(item.result)),
renderDetailCard("Ganador", formatWinner(item.winner || item.result?.winner)),
renderDetailCard("Resultado", formatMatchResult(item.result)),
renderDetailCard("Inicio", formatMatchTimestamp(item, "start")),
renderDetailCard("Fin", formatMatchTimestamp(item, "end")),
renderDetailCard("Duracion", formatDuration(item.duration_seconds)),
renderDetailCard("Confianza", formatConfidence(item.confidence)),
renderDetailCard(
"Fuente",
formatSourceBasis(item.source_basis || item.result_source) || "No disponible",
),
renderDetailCard("Base", formatCaptureBasis(item.capture_basis)),
].join("");
renderPlayerSection(item, nodes); renderPlayerSection(item, nodes);
renderTimelineSection(item, nodes); hideTimelineSection(nodes);
renderActions(item, nodes.actions); renderActions(item, nodes.actions);
nodes.state.hidden = true; nodes.state.hidden = true;
nodes.grid.hidden = false; nodes.grid.hidden = false;
} }
function renderScoreboardDetail(item, { mapName, serverName }) {
const result = item.result || {};
const alliedScore = Number.isFinite(Number(result.allied_score))
? formatNumber(result.allied_score)
: "-";
const axisScore = Number.isFinite(Number(result.axis_score))
? formatNumber(result.axis_score)
: "-";
const winner = String(item.winner || result.winner || "").toLowerCase();
const isAlliedWinner = winner === "allies" || winner === "allied";
const isAxisWinner = winner === "axis";
const metadata = [
["Servidor", serverName],
["Mapa", mapName],
["Modo", formatGameMode(item.game_mode || item.gamestate?.game_mode)],
["Duracion", formatDuration(item.duration_seconds)],
["Inicio", formatMatchTimestamp(item, "start")],
];
if (item.ended_at) {
metadata.push(["Fin", formatMatchTimestamp(item, "end")]);
}
return `
<section class="historical-scoreboard-detail" aria-label="Marcador de la partida">
<div class="historical-scoreboard-detail__side ${isAlliedWinner ? "is-winner" : ""}">
<span>Bando aliado</span>
<strong>Aliados</strong>
<em>${isAlliedWinner ? "Ganador" : ""}</em>
</div>
<div class="historical-scoreboard-detail__score">
<span>Marcador</span>
<strong>${escapeHtml(alliedScore)} - ${escapeHtml(axisScore)}</strong>
<em>${escapeHtml(formatWinner(winner))}</em>
</div>
<div class="historical-scoreboard-detail__side historical-scoreboard-detail__side--axis ${isAxisWinner ? "is-winner" : ""}">
<span>Bando eje</span>
<strong>Eje</strong>
<em>${isAxisWinner ? "Ganador" : ""}</em>
</div>
</section>
<section class="historical-match-compact-meta" aria-label="Datos principales de la partida">
${metadata.map(([label, value]) => renderCompactMeta(label, value)).join("")}
</section>
`;
}
function renderCompactMeta(label, value) {
return `
<article>
<span>${escapeHtml(label)}</span>
<strong>${escapeHtml(value || "No disponible")}</strong>
</article>
`;
}
function hideTimelineSection(nodes) {
if (!nodes.timelineSection) {
return;
}
nodes.timelineSection.hidden = true;
nodes.timelineNote.textContent = "";
nodes.timelineState.hidden = true;
nodes.timelineGrid.hidden = true;
nodes.timelineGrid.innerHTML = "";
}
function renderPlayerSection(item, nodes) { function renderPlayerSection(item, nodes) {
const players = Array.isArray(item.players) ? item.players : []; const players = Array.isArray(item.players) ? item.players : [];
nodes.playersSection.hidden = false; nodes.playersSection.hidden = false;
@@ -128,37 +177,6 @@ function renderPlayerRow(player) {
`; `;
} }
function renderTimelineSection(item, nodes) {
const eventCounts = Array.isArray(item.timeline?.event_counts)
? item.timeline.event_counts
: [];
nodes.timelineSection.hidden = false;
if (eventCounts.length === 0) {
nodes.timelineNote.textContent =
"No hay resumen de eventos disponible para esta partida.";
setState(nodes.timelineState, "Sin eventos agregados para mostrar.");
nodes.timelineGrid.hidden = true;
nodes.timelineGrid.innerHTML = "";
return;
}
nodes.timelineNote.textContent = `${formatNumber(eventCounts.length)} tipos de evento registrados.`;
nodes.timelineState.hidden = true;
nodes.timelineGrid.innerHTML = eventCounts.map((event) => renderTimelineCard(event)).join("");
nodes.timelineGrid.hidden = false;
}
function renderTimelineCard(event) {
const label =
event.event_type ||
event.type ||
event.name ||
event.label ||
"Evento registrado";
const count = event.count ?? event.total ?? event.event_count ?? 0;
return renderDetailCard(formatEventType(label), formatNumber(count));
}
function renderActions(item, actionsNode) { function renderActions(item, actionsNode) {
const matchUrl = normalizeExternalMatchUrl(item.match_url); const matchUrl = normalizeExternalMatchUrl(item.match_url);
if (!matchUrl) { if (!matchUrl) {
@@ -179,23 +197,6 @@ function renderActions(item, actionsNode) {
actionsNode.hidden = false; actionsNode.hidden = false;
} }
function renderDetailCard(label, value) {
return `
<article class="historical-stat-card">
<p>${escapeHtml(label)}</p>
<strong>${escapeHtml(value)}</strong>
</article>
`;
}
function buildDetailNote(item) {
const source = formatSourceBasis(item.source_basis || item.result_source);
if (source) {
return `Detalle servido desde ${source}. Los campos sin cobertura fiable se muestran como no disponibles.`;
}
return "Detalle servido desde el historico local disponible para esta partida.";
}
function formatDetailSubtitle(item) { function formatDetailSubtitle(item) {
if (item.capture_basis === "rcon-materialized-admin-log") { if (item.capture_basis === "rcon-materialized-admin-log") {
return "Partida RCON materializada"; return "Partida RCON materializada";
@@ -209,52 +210,13 @@ function formatDetailSubtitle(item) {
return "Partida historica"; return "Partida historica";
} }
function formatCaptureBasis(value) {
if (value === "rcon-materialized-admin-log") {
return "Registro RCON materializado";
}
if (value === "rcon-competitive-window") {
return "Ventana competitiva RCON";
}
if (value === "public-scoreboard-match") {
return "Scoreboard persistido";
}
return value ? String(value).replaceAll("-", " ") : "Historico local";
}
function formatSourceBasis(value) {
if (value === "admin-log-match-ended") {
return "cierre RCON confirmado";
}
if (value === "rcon-session") {
return "sesion RCON registrada";
}
if (value === "public-scoreboard-match") {
return "scoreboard externo";
}
return value ? String(value).replaceAll("-", " ") : "";
}
function formatConfidence(value) {
if (value === "exact") {
return "Exacta";
}
if (value === "approximate") {
return "Aproximada";
}
if (value === "partial") {
return "Parcial";
}
return value ? String(value).replaceAll("-", " ") : "No disponible";
}
function formatTeamSide(value) { function formatTeamSide(value) {
const normalized = String(value || "").toLowerCase(); const normalized = String(value || "").toLowerCase();
if (normalized === "allies" || normalized === "allied") { if (normalized === "allies" || normalized === "allied") {
return "Aliados"; return "Aliados";
} }
if (normalized === "axis") { if (normalized === "axis") {
return "Axis"; return "Eje";
} }
return value || "No disponible"; return value || "No disponible";
} }
@@ -281,40 +243,19 @@ function formatDuration(value) {
return `${formatNumber(hours)} h ${formatNumber(remainingMinutes)} min`; return `${formatNumber(hours)} h ${formatNumber(remainingMinutes)} min`;
} }
function formatMatchResult(result) {
if (hasMatchScore(result)) {
return `${formatWinner(result.winner)} (${formatScore(result)})`;
}
return result?.winner ? formatWinner(result.winner) : "Resultado no disponible";
}
function formatWinner(value) { function formatWinner(value) {
const normalized = String(value || "").toLowerCase(); const normalized = String(value || "").toLowerCase();
if (normalized === "allies" || normalized === "allied") { if (normalized === "allies" || normalized === "allied") {
return "Aliados"; return "Ganador: Aliados";
} }
if (normalized === "axis") { if (normalized === "axis") {
return "Axis"; return "Ganador: Eje";
} }
if (normalized === "draw") { if (normalized === "draw") {
return "Empate"; return "Empate";
} }
return "No disponible";
}
function formatScore(result) {
if (!hasMatchScore(result)) {
return "Resultado no disponible"; return "Resultado no disponible";
} }
return `${Number(result.allied_score)} - ${Number(result.axis_score)}`;
}
function hasMatchScore(result) {
return (
Number.isFinite(Number(result?.allied_score)) &&
Number.isFinite(Number(result?.axis_score))
);
}
function formatOptionalNumber(value) { function formatOptionalNumber(value) {
return value === null || value === undefined ? "No disponible" : formatNumber(value); return value === null || value === undefined ? "No disponible" : formatNumber(value);
@@ -346,14 +287,6 @@ function formatNamedCounts(items) {
.join(" / "); .join(" / ");
} }
function formatEventType(value) {
const normalized = String(value || "").replaceAll("_", " ").replaceAll("-", " ");
if (!normalized) {
return "Evento";
}
return normalized.charAt(0).toUpperCase() + normalized.slice(1);
}
function formatNumber(value) { function formatNumber(value) {
const parsedValue = Number(value); const parsedValue = Number(value);
if (!Number.isFinite(parsedValue)) { if (!Number.isFinite(parsedValue)) {
@@ -394,9 +327,6 @@ function formatMatchTimestamp(item, kind) {
if (timestamp) { if (timestamp) {
return formatTimestamp(timestamp); return formatTimestamp(timestamp);
} }
if (item.timestamp_confidence === "server-time-only") {
return "No disponible";
}
return "No disponible"; return "No disponible";
} }

View File

@@ -10,6 +10,7 @@
<title>Detalle de partida - HLL Vietnam</title> <title>Detalle de partida - HLL Vietnam</title>
<link rel="stylesheet" href="./assets/css/styles.css" /> <link rel="stylesheet" href="./assets/css/styles.css" />
<link rel="stylesheet" href="./assets/css/historico.css" /> <link rel="stylesheet" href="./assets/css/historico.css" />
<link rel="stylesheet" href="./assets/css/historico-scoreboard-detail.css" />
</head> </head>
<body data-backend-base-url="http://127.0.0.1:8000"> <body data-backend-base-url="http://127.0.0.1:8000">
<div class="page-shell historical-shell"> <div class="page-shell historical-shell">