From dfdb53d2b602f9cc468d0a77e8621a5622fe3460 Mon Sep 17 00:00:00 2001 From: devRaGonSa Date: Fri, 22 May 2026 10:57:30 +0200 Subject: [PATCH] fix: improve historical worker visibility and recent pagination --- backend/app/rcon_historical_worker.py | 99 ++++++++++++++--- frontend/assets/css/historico.css | 85 ++++++++++++++- frontend/assets/js/historico.js | 146 +++++++++++++++++++++++++- frontend/assets/js/partida-actual.js | 3 - frontend/partida-actual.html | 11 +- 5 files changed, 323 insertions(+), 21 deletions(-) diff --git a/backend/app/rcon_historical_worker.py b/backend/app/rcon_historical_worker.py index 88206cb..00f2f10 100644 --- a/backend/app/rcon_historical_worker.py +++ b/backend/app/rcon_historical_worker.py @@ -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") diff --git a/frontend/assets/css/historico.css b/frontend/assets/css/historico.css index 1a3d407..a3b08ba 100644 --- a/frontend/assets/css/historico.css +++ b/frontend/assets/css/historico.css @@ -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%; } diff --git a/frontend/assets/js/historico.js b/frontend/assets/js/historico.js index c4312a5..0d90568 100644 --- a/frontend/assets/js/historico.js +++ b/frontend/assets/js/historico.js @@ -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", + ` + + `, + ); + 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, diff --git a/frontend/assets/js/partida-actual.js b/frontend/assets/js/partida-actual.js index ba08361..d0e4505 100644 --- a/frontend/assets/js/partida-actual.js +++ b/frontend/assets/js/partida-actual.js @@ -225,9 +225,6 @@ function initializePlayerStats(nodes) {

Cargando estadisticas en vivo...

-

- Jugadores detectados: 0 -

`, ); diff --git a/frontend/partida-actual.html b/frontend/partida-actual.html index ec0d0e4..ecf00ce 100644 --- a/frontend/partida-actual.html +++ b/frontend/partida-actual.html @@ -119,9 +119,14 @@

Jugadores

Estadisticas en vivo

-

- Las estadisticas en vivo apareceran cuando haya datos suficientes. -

+
+

+ Las estadisticas en vivo apareceran cuando haya datos suficientes. +

+

+ Jugadores detectados: 0 +

+