fix: improve historical worker visibility and recent pagination
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
`,
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user