fix: project current match live data safely

This commit is contained in:
devRaGonSa
2026-05-21 16:39:18 +02:00
parent da3f1643a2
commit 45801bb955
8 changed files with 715 additions and 29 deletions

View File

@@ -49,6 +49,7 @@ from .historical_storage import (
)
from .rcon_historical_read_model import get_rcon_historical_match_detail
from .normalizers import normalize_map_name
from .rcon_client import load_rcon_targets, query_live_server_sample
from .rcon_admin_log_storage import list_current_match_kill_feed
from .scoreboard_origins import get_trusted_public_scoreboard_origin
from .storage import list_latest_snapshots, list_server_history, list_snapshot_history
@@ -244,6 +245,61 @@ def build_current_match_payload(*, server_slug: str) -> dict[str, object]:
if origin is None:
raise ValueError("Unsupported current match server.")
sample = _query_current_match_rcon_sample(origin.slug)
if sample is not None:
normalized = sample["normalized"]
raw_session = sample["raw_session"]
captured_at = _utc_timestamp_now()
map_id = raw_session.get("mapId") or normalized.get("current_map")
map_name = raw_session.get("mapName") or map_id
map_pretty_name = normalize_map_name(map_name)
return {
"status": "ok",
"data": {
"found": True,
"server_slug": origin.slug,
"server_name": normalized.get("server_name") or origin.display_name,
"status": normalized.get("status") or "unavailable",
"map": map_pretty_name,
"map_id": map_id,
"map_pretty_name": map_pretty_name,
"game_mode": normalized.get("game_mode"),
"started_at": None,
"allied_score": normalized.get("allied_score"),
"axis_score": normalized.get("axis_score"),
"allied_players": normalized.get("allied_players"),
"axis_players": normalized.get("axis_players"),
"players": normalized.get("players"),
"max_players": normalized.get("max_players"),
# RCA: getSession currently reports 0 while the public scoreboard
# can show players, so session population is exposed but unverified.
"player_count_quality": (
"rcon-session-unverified"
if normalized.get("players") is not None
else None
),
"player_count_source": _source_when_present(
normalized.get("players"),
source="rcon-session",
),
"score_source": _source_when_present(
normalized.get("allied_score"),
normalized.get("axis_score"),
source="rcon-session",
),
"map_source": _source_when_present(map_id, map_name, source="rcon-session"),
"match_time_seconds": normalized.get("match_time_seconds"),
"remaining_match_time_seconds": normalized.get(
"remaining_match_time_seconds"
),
"captured_at": captured_at,
"updated_at": captured_at,
"public_scoreboard_url": origin.base_url,
},
}
# The generic live server snapshot is a fallback only. It intentionally
# drops richer RCON session fields such as game mode and current scores.
server_payload = build_servers_payload()
server_data = server_payload["data"]
item = next(
@@ -262,12 +318,31 @@ def build_current_match_payload(*, server_slug: str) -> dict[str, object]:
"server_name": item.get("server_name") if item else origin.display_name,
"status": item.get("status") if item else "unavailable",
"map": item.get("current_map") if item else None,
"map_id": None,
"map_pretty_name": item.get("current_map") if item else None,
"game_mode": item.get("game_mode") if item else None,
"started_at": item.get("started_at") if item else None,
"allied_score": item.get("allied_score") if item else None,
"axis_score": item.get("axis_score") if item else None,
"allied_players": item.get("allied_players") if item else None,
"axis_players": item.get("axis_players") if item else None,
"players": item.get("players") if item else None,
"max_players": item.get("max_players") if item else None,
"player_count_quality": _snapshot_player_count_quality(item),
"player_count_source": _snapshot_player_count_source(item),
"score_source": _source_when_present(
item.get("allied_score") if item else None,
item.get("axis_score") if item else None,
source="live-server-snapshot",
),
"map_source": _source_when_present(
item.get("current_map") if item else None,
source="live-server-snapshot",
),
"match_time_seconds": item.get("match_time_seconds") if item else None,
"remaining_match_time_seconds": (
item.get("remaining_match_time_seconds") if item else None
),
"captured_at": item.get("captured_at") if item else None,
"updated_at": server_data.get("last_snapshot_at"),
"public_scoreboard_url": origin.base_url,
@@ -295,6 +370,52 @@ def build_current_match_kill_feed_payload(
}
def _query_current_match_rcon_sample(server_slug: str) -> dict[str, object] | None:
"""Read one configured trusted RCON target for the current-match view."""
try:
targets = load_rcon_targets()
except (RuntimeError, ValueError):
return None
target = next(
(candidate for candidate in targets if candidate.external_server_id == server_slug),
None,
)
if target is None:
return None
try:
return query_live_server_sample(target)
except Exception: # noqa: BLE001 - fall back to the existing live snapshot read
return None
def _utc_timestamp_now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def _source_when_present(*values: object, source: str) -> str | None:
return source if any(value is not None for value in values) else None
def _snapshot_player_count_quality(item: dict[str, object] | None) -> str | None:
if item is None or item.get("players") is None:
return None
if item.get("snapshot_origin") == "real-rcon":
return "rcon-session-unverified"
if item.get("snapshot_origin") == "real-a2s":
return "a2s-query"
return "snapshot-unverified"
def _snapshot_player_count_source(item: dict[str, object] | None) -> str | None:
if item is None or item.get("players") is None:
return None
if item.get("snapshot_origin") == "real-rcon":
return "rcon-session"
if item.get("snapshot_origin") == "real-a2s":
return "a2s"
return "live-server-snapshot"
def build_error_payload(message: str) -> dict[str, str]:
"""Return the shared error payload shape used by the backend bootstrap."""
return {