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", + ` +
Pagina 1 de 1
+ +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
- Las estadisticas en vivo apareceran cuando haya datos suficientes. -
++ Las estadisticas en vivo apareceran cuando haya datos suficientes. +
++ Jugadores detectados: 0 +
+