fix: improve historical worker visibility and recent pagination

This commit is contained in:
devRaGonSa
2026-05-22 10:57:30 +02:00
parent 13cba1dcf1
commit dfdb53d2b6
5 changed files with 323 additions and 21 deletions

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
import argparse
from datetime import date, datetime
import json
import os
import time
@@ -201,34 +202,52 @@ def run_periodic_rcon_historical_capture(
) -> None:
"""Run prospective RCON capture in a local loop."""
completed_runs = 0
print(
json.dumps(
{
"event": "rcon-historical-capture-loop-started",
"interval_seconds": interval_seconds,
"max_retries": max_retries,
"retry_delay_seconds": retry_delay_seconds,
"target_scope": target_key or "all-configured-rcon-targets",
},
indent=2,
)
startup_targets = _describe_loop_targets(target_key)
_emit_worker_event(
"rcon-historical-capture-worker-started",
interval_seconds=interval_seconds,
max_retries=max_retries,
retry_delay_seconds=retry_delay_seconds,
target_scope=target_key or "all-configured-rcon-targets",
target_count=len(startup_targets),
targets=startup_targets,
)
print("Press Ctrl+C to stop.")
try:
while max_runs is None or completed_runs < max_runs:
completed_runs += 1
_emit_worker_event(
"rcon-historical-capture-cycle-started",
run=completed_runs,
)
payload = _run_capture_with_retries(
max_retries=max_retries,
retry_delay_seconds=retry_delay_seconds,
target_key=target_key,
)
print(json.dumps({"run": completed_runs, **payload}, indent=2), flush=True)
_emit_worker_event(
"rcon-historical-capture-cycle-finished",
run=completed_runs,
result=payload,
)
if max_runs is not None and completed_runs >= max_runs:
break
_emit_worker_event(
"rcon-historical-capture-sleep-started",
run=completed_runs,
sleep_seconds=interval_seconds,
)
time.sleep(interval_seconds)
except KeyboardInterrupt:
print("\nRCON historical capture loop stopped by user.")
except Exception as exc:
_emit_worker_event(
"rcon-historical-capture-worker-exited-unexpectedly",
error_type=type(exc).__name__,
message=str(exc),
)
raise
def _run_capture_with_retries(
@@ -248,12 +267,32 @@ def _run_capture_with_retries(
}
except Exception as exc:
if attempt > max_retries:
_emit_worker_event(
"rcon-historical-capture-attempt-failed",
attempt=attempt,
max_retries=max_retries,
error_type=type(exc).__name__,
message=str(exc),
retries_exhausted=True,
)
return {
"status": "error",
"attempts_used": attempt,
"error": str(exc),
}
_emit_worker_event(
"rcon-historical-capture-attempt-failed",
attempt=attempt,
max_retries=max_retries,
error_type=type(exc).__name__,
message=str(exc),
)
if retry_delay_seconds > 0:
_emit_worker_event(
"rcon-historical-capture-retry-sleep-started",
attempt=attempt,
sleep_seconds=retry_delay_seconds,
)
time.sleep(retry_delay_seconds)
@@ -275,6 +314,42 @@ def _select_targets(target_key: str | None) -> list[object]:
return selected
def _describe_loop_targets(target_key: str | None) -> list[dict[str, str]]:
"""Describe configured worker targets without exposing credentials."""
try:
targets = _select_targets(target_key)
except Exception as exc: # noqa: BLE001 - startup logging must not hide capture error
return [
{
"status": "unavailable",
"error_type": type(exc).__name__,
"message": str(exc),
}
]
return [
{
"target_key": build_rcon_target_key(target),
"external_server_id": str(target.external_server_id or ""),
"name": str(target.name or ""),
}
for target in targets
]
def _emit_worker_event(event: str, **fields: object) -> None:
"""Print one JSON worker event using safe date/time serialization."""
print(
json.dumps({"event": event, **fields}, indent=2, default=_json_default),
flush=True,
)
def _json_default(value: object) -> str:
if isinstance(value, (date, datetime)):
return value.isoformat()
return str(value)
def get_rcon_admin_log_lookback_minutes() -> int:
"""Return the AdminLog lookback window used by periodic RCON capture."""
configured_value = os.getenv("HLL_BACKEND_RCON_ADMIN_LOG_LOOKBACK_MINUTES", "60")

View File

@@ -589,6 +589,61 @@
gap: 14px;
}
.historical-pagination {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
margin-top: 16px;
}
.historical-pagination[hidden] {
display: none;
}
.historical-pagination__size,
.historical-pagination__nav {
display: flex;
align-items: center;
gap: 10px;
}
.historical-pagination__size {
color: var(--text-soft);
font-size: 0.86rem;
font-weight: 700;
}
.historical-pagination__size select {
min-height: 42px;
padding: 0 34px 0 12px;
border: 1px solid rgba(159, 168, 141, 0.2);
border-radius: 6px;
color: var(--text);
background: rgba(13, 17, 12, 0.72);
font: inherit;
}
.historical-pagination__nav p {
margin: 0;
min-width: 110px;
color: var(--text-soft);
font-size: 0.86rem;
font-weight: 700;
text-align: center;
}
.historical-pagination .historical-tab {
margin: 0;
}
.historical-pagination .historical-tab:disabled {
transform: none;
border-color: rgba(159, 168, 141, 0.12);
color: rgba(169, 173, 154, 0.48);
cursor: default;
}
.current-match-killfeed-screen {
position: relative;
width: 100%;
@@ -795,9 +850,22 @@
color: var(--accent-warm);
}
.current-match-player-intro {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 10px 18px;
margin-top: 10px;
}
.current-match-player-intro .historical-panel__note {
margin-top: 0;
}
.current-match-player-count {
flex: 0 0 auto;
width: fit-content;
margin: -6px 0 16px;
margin: 0;
padding: 7px 10px;
border: 1px solid rgba(159, 168, 141, 0.16);
border-radius: 6px;
@@ -1004,6 +1072,11 @@
flex-direction: column;
}
.current-match-player-intro {
align-items: flex-start;
flex-direction: column;
}
.historical-mvp-card__meta {
grid-template-columns: 1fr;
}
@@ -1025,6 +1098,16 @@
justify-content: flex-start;
}
.historical-pagination,
.historical-pagination__nav {
align-items: stretch;
flex-direction: column;
}
.historical-pagination__size {
justify-content: space-between;
}
.historical-tab {
width: 100%;
}

View File

@@ -19,11 +19,15 @@ const DEFAULT_HISTORICAL_SERVER = "all-servers";
const SNAPSHOT_CACHE_TTL_MS = 120000;
const STALE_SNAPSHOT_CACHE_TTL_MS = 30000;
const NEGATIVE_SNAPSHOT_CACHE_TTL_MS = 15000;
const RECENT_MATCHES_LIMIT = 100;
const DEFAULT_RECENT_MATCHES_PAGE_SIZE = 10;
const RECENT_MATCHES_PAGE_SIZES = Object.freeze([10, 25, 50, 100]);
let activeServerSlug = DEFAULT_HISTORICAL_SERVER;
let activeLeaderboardMetric;
let activeLeaderboardTimeframe;
let activeServerRequestId = 0;
let activeLeaderboardRequestId = 0;
let recentMatchesPagination;
const LEADERBOARD_TIMEFRAMES = Object.freeze([
{
key: "weekly",
@@ -100,6 +104,7 @@ document.addEventListener("DOMContentLoaded", () => {
const recentSnapshotMetaNode = document.getElementById(
"recent-matches-snapshot-meta",
);
recentMatchesPagination = initializeRecentMatchesPagination(recentListNode);
const params = new URLSearchParams(window.location.search);
activeServerSlug = normalizeServerSlug(params.get("server"));
@@ -126,7 +131,7 @@ document.addEventListener("DOMContentLoaded", () => {
recentMatchesCache,
pendingRequestCache,
buildRecentMatchesSnapshotKey(serverSlug),
`${backendBaseUrl}/api/historical/snapshots/recent-matches?server=${encodeURIComponent(serverSlug)}&limit=10`,
`${backendBaseUrl}/api/historical/snapshots/recent-matches?server=${encodeURIComponent(serverSlug)}&limit=${RECENT_MATCHES_LIMIT}`,
);
const getLeaderboardSnapshot = (serverSlug, timeframeKey, metricKey) =>
@@ -169,6 +174,7 @@ document.addEventListener("DOMContentLoaded", () => {
weeklySnapshotMetaNode,
`Preparando datos ${activeTimeframeConfig.shortLabel}...`,
);
resetRecentMatchesPagination();
recentListNode.innerHTML = "";
recentNoteNode.textContent = buildRecentMatchesNote(activeServerSlug);
setState(recentStateNode, "Cargando partidas recientes...");
@@ -635,20 +641,156 @@ function hydrateRecentMatches(result, stateNode, listNode, snapshotMetaNode) {
buildSnapshotMetaText(payload, "Partidas pendientes de generacion."),
);
if (!payload?.found) {
resetRecentMatchesPagination();
listNode.innerHTML = "";
setState(stateNode, emptyState.recentMessage);
return;
}
const items = payload?.items;
if (!Array.isArray(items) || items.length === 0) {
resetRecentMatchesPagination();
listNode.innerHTML = "";
setState(stateNode, emptyState.recentMessage);
return;
}
listNode.innerHTML = items.map((item) => renderRecentMatchCard(item)).join("");
setRecentMatchesPaginationItems(items.slice(0, RECENT_MATCHES_LIMIT), listNode);
stateNode.hidden = true;
}
function initializeRecentMatchesPagination(listNode) {
if (!listNode) {
return null;
}
listNode.insertAdjacentHTML(
"afterend",
`
<div class="historical-pagination" id="recent-matches-pagination" hidden>
<label class="historical-pagination__size">
<span>Partidas por pagina</span>
<select id="recent-matches-page-size" aria-label="Partidas por pagina">
${RECENT_MATCHES_PAGE_SIZES.map(
(pageSize) => `
<option value="${pageSize}"${pageSize === DEFAULT_RECENT_MATCHES_PAGE_SIZE ? " selected" : ""}>
${pageSize}
</option>
`,
).join("")}
</select>
</label>
<div class="historical-pagination__nav">
<button class="historical-tab" id="recent-matches-page-prev" type="button">
Anterior
</button>
<p id="recent-matches-page-status">Pagina 1 de 1</p>
<button class="historical-tab" id="recent-matches-page-next" type="button">
Siguiente
</button>
</div>
</div>
`,
);
const pagination = {
items: [],
page: 1,
pageSize: DEFAULT_RECENT_MATCHES_PAGE_SIZE,
root: document.getElementById("recent-matches-pagination"),
pageSizeSelect: document.getElementById("recent-matches-page-size"),
previousButton: document.getElementById("recent-matches-page-prev"),
nextButton: document.getElementById("recent-matches-page-next"),
status: document.getElementById("recent-matches-page-status"),
};
pagination.previousButton?.addEventListener("click", () => {
if (pagination.page <= 1) {
return;
}
pagination.page -= 1;
renderRecentMatchesPage(listNode);
});
pagination.nextButton?.addEventListener("click", () => {
if (pagination.page >= getRecentMatchesPageCount(pagination)) {
return;
}
pagination.page += 1;
renderRecentMatchesPage(listNode);
});
pagination.pageSizeSelect?.addEventListener("change", () => {
pagination.pageSize = normalizeRecentMatchesPageSize(
pagination.pageSizeSelect.value,
);
pagination.page = 1;
renderRecentMatchesPage(listNode);
});
return pagination;
}
function resetRecentMatchesPagination() {
if (!recentMatchesPagination) {
return;
}
recentMatchesPagination.items = [];
recentMatchesPagination.page = 1;
recentMatchesPagination.pageSize = DEFAULT_RECENT_MATCHES_PAGE_SIZE;
if (recentMatchesPagination.pageSizeSelect) {
recentMatchesPagination.pageSizeSelect.value = String(
DEFAULT_RECENT_MATCHES_PAGE_SIZE,
);
}
if (recentMatchesPagination.root) {
recentMatchesPagination.root.hidden = true;
}
}
function setRecentMatchesPaginationItems(items, listNode) {
if (!recentMatchesPagination) {
listNode.innerHTML = items.map((item) => renderRecentMatchCard(item)).join("");
return;
}
recentMatchesPagination.items = items;
recentMatchesPagination.page = 1;
renderRecentMatchesPage(listNode);
}
function renderRecentMatchesPage(listNode) {
const pagination = recentMatchesPagination;
if (!pagination) {
return;
}
const pageCount = getRecentMatchesPageCount(pagination);
pagination.page = Math.min(Math.max(1, pagination.page), pageCount);
const pageStart = (pagination.page - 1) * pagination.pageSize;
const visibleItems = pagination.items.slice(pageStart, pageStart + pagination.pageSize);
listNode.innerHTML = visibleItems.map((item) => renderRecentMatchCard(item)).join("");
if (pagination.status) {
pagination.status.textContent = `Pagina ${pagination.page} de ${pageCount}`;
}
if (pagination.previousButton) {
pagination.previousButton.disabled = pagination.page <= 1;
}
if (pagination.nextButton) {
pagination.nextButton.disabled = pagination.page >= pageCount;
}
if (pagination.root) {
pagination.root.hidden = pagination.items.length <= DEFAULT_RECENT_MATCHES_PAGE_SIZE;
}
}
function getRecentMatchesPageCount(pagination) {
return Math.max(1, Math.ceil(pagination.items.length / pagination.pageSize));
}
function normalizeRecentMatchesPageSize(rawValue) {
const pageSize = Number(rawValue);
return RECENT_MATCHES_PAGE_SIZES.includes(pageSize)
? pageSize
: DEFAULT_RECENT_MATCHES_PAGE_SIZE;
}
function hydrateMonthlyMvp(
result,
stateNode,

View File

@@ -225,9 +225,6 @@ function initializePlayerStats(nodes) {
<p class="historical-state" id="current-match-player-stats-state" aria-live="polite">
Cargando estadisticas en vivo...
</p>
<p class="current-match-player-count" id="current-match-player-count">
Jugadores detectados: 0
</p>
<div class="historical-table-shell" id="current-match-player-stats-shell" hidden></div>
`,
);

View File

@@ -119,9 +119,14 @@
<div>
<p class="eyebrow eyebrow--section">Jugadores</p>
<h2 id="current-match-players-title">Estadisticas en vivo</h2>
<p class="historical-panel__note">
Las estadisticas en vivo apareceran cuando haya datos suficientes.
</p>
<div class="current-match-player-intro">
<p class="historical-panel__note">
Las estadisticas en vivo apareceran cuando haya datos suficientes.
</p>
<p class="current-match-player-count" id="current-match-player-count">
Jugadores detectados: 0
</p>
</div>
</div>
</div>
</div>