"""PostgreSQL read/write storage for data displayed outside the RCON write path.""" from __future__ import annotations import json from contextlib import contextmanager from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any, Iterable, Mapping from .config import get_database_url, get_historical_weekly_fallback_max_weekday from .historical_models import HistoricalSnapshotRecord from .player_external_profiles import build_external_player_profile_fields from .scoreboard_origins import resolve_trusted_scoreboard_match_url ALL_SERVERS_SLUG = "all-servers" ALL_SERVERS_DISPLAY_NAME = "Todos" SUMMARY_SNAPSHOT_LIMIT = 6 DISPLAY_SCHEMA_SQL = """ CREATE TABLE IF NOT EXISTS game_sources ( id BIGSERIAL PRIMARY KEY, slug TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, provider_kind TEXT NOT NULL, is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS servers ( id BIGSERIAL PRIMARY KEY, game_source_id BIGINT NOT NULL REFERENCES game_sources(id), external_server_id TEXT, server_name TEXT NOT NULL, region TEXT, first_seen_at TEXT NOT NULL, last_seen_at TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(game_source_id, external_server_id) ); CREATE TABLE IF NOT EXISTS server_snapshots ( id BIGSERIAL PRIMARY KEY, server_id BIGINT NOT NULL REFERENCES servers(id), captured_at TEXT NOT NULL, status TEXT NOT NULL, players INTEGER, max_players INTEGER, current_map TEXT, source_name TEXT NOT NULL, snapshot_origin TEXT, source_ref TEXT, raw_payload_ref TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(server_id, captured_at, source_name, source_ref) ); CREATE INDEX IF NOT EXISTS idx_pg_server_snapshots_server_time ON server_snapshots(server_id, captured_at DESC); CREATE TABLE IF NOT EXISTS historical_servers ( id BIGSERIAL PRIMARY KEY, slug TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, scoreboard_base_url TEXT NOT NULL UNIQUE, server_number INTEGER, source_kind TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS historical_maps ( id BIGSERIAL PRIMARY KEY, external_map_id TEXT UNIQUE, map_name TEXT, pretty_name TEXT, game_mode TEXT, image_name TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS historical_matches ( id BIGSERIAL PRIMARY KEY, historical_server_id BIGINT NOT NULL REFERENCES historical_servers(id), external_match_id TEXT NOT NULL, historical_map_id BIGINT REFERENCES historical_maps(id), created_at_source TEXT, started_at TEXT, ended_at TEXT, map_name TEXT, map_pretty_name TEXT, game_mode TEXT, image_name TEXT, allied_score INTEGER, axis_score INTEGER, last_seen_at TEXT NOT NULL, raw_payload_ref TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(historical_server_id, external_match_id) ); CREATE TABLE IF NOT EXISTS historical_players ( id BIGSERIAL PRIMARY KEY, stable_player_key TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, steam_id TEXT, source_player_id TEXT, first_seen_at TEXT NOT NULL, last_seen_at TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS historical_player_match_stats ( id BIGSERIAL PRIMARY KEY, historical_match_id BIGINT NOT NULL REFERENCES historical_matches(id), historical_player_id BIGINT NOT NULL REFERENCES historical_players(id), match_player_ref TEXT, team_side TEXT, level INTEGER, kills INTEGER, deaths INTEGER, teamkills INTEGER, time_seconds INTEGER, kills_per_minute DOUBLE PRECISION, deaths_per_minute DOUBLE PRECISION, kill_death_ratio DOUBLE PRECISION, combat INTEGER, offense INTEGER, defense INTEGER, support INTEGER, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(historical_match_id, historical_player_id) ); CREATE INDEX IF NOT EXISTS idx_pg_historical_matches_server_end ON historical_matches(historical_server_id, ended_at DESC, started_at DESC); CREATE INDEX IF NOT EXISTS idx_pg_historical_player_stats_match ON historical_player_match_stats(historical_match_id); CREATE TABLE IF NOT EXISTS displayed_historical_snapshots ( server_key TEXT NOT NULL, snapshot_type TEXT NOT NULL, metric TEXT NOT NULL DEFAULT '', snapshot_window TEXT NOT NULL DEFAULT '', payload_json TEXT NOT NULL, generated_at TEXT NOT NULL, source_range_start TEXT, source_range_end TEXT, is_stale BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY(server_key, snapshot_type, metric, snapshot_window) ); CREATE TABLE IF NOT EXISTS player_event_raw_ledger ( id BIGSERIAL PRIMARY KEY, event_id TEXT NOT NULL UNIQUE, event_type TEXT NOT NULL, occurred_at TEXT, server_slug TEXT NOT NULL, external_match_id TEXT NOT NULL, source_kind TEXT NOT NULL, source_ref TEXT, raw_event_ref TEXT, killer_player_key TEXT, killer_display_name TEXT, victim_player_key TEXT, victim_display_name TEXT, weapon_name TEXT, weapon_category TEXT, kill_category TEXT, is_teamkill BOOLEAN NOT NULL DEFAULT FALSE, event_value INTEGER NOT NULL DEFAULT 1, inserted_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_pg_player_event_raw_occurred_at ON player_event_raw_ledger(occurred_at DESC); """ def initialize_postgres_display_storage() -> None: with connect_postgres() as connection: connection.execute(DISPLAY_SCHEMA_SQL) def connect_postgres(): try: import psycopg from psycopg.rows import dict_row except ImportError as error: # pragma: no cover - environment-specific raise RuntimeError("psycopg is required when HLL_BACKEND_DATABASE_URL is set.") from error database_url = get_database_url() if not database_url: raise RuntimeError("HLL_BACKEND_DATABASE_URL is required for displayed PostgreSQL storage.") return psycopg.connect(database_url, row_factory=dict_row) class PostgresCompatConnection: """Small placeholder shim for SQLite-shaped displayed read queries.""" def __init__(self, connection: Any): self.connection = connection def execute(self, sql: str, params: Iterable[object] | None = None): return self.connection.execute(sql.replace("?", "%s"), tuple(params or ())) @contextmanager def connect_postgres_compat(): initialize_postgres_display_storage() with connect_postgres() as connection: yield PostgresCompatConnection(connection) def persist_snapshot_record(snapshot: Mapping[str, object]) -> HistoricalSnapshotRecord: initialize_postgres_display_storage() generated_at = _iso(snapshot.get("generated_at")) or _utc_now_iso() metric = str(snapshot.get("metric") or "") window = str(snapshot.get("window") or "") payload = snapshot.get("payload") payload_json = json.dumps( payload, ensure_ascii=True, separators=(",", ":"), default=_json_payload_default, ) with connect_postgres() as connection: connection.execute( """ INSERT INTO displayed_historical_snapshots ( server_key, snapshot_type, metric, snapshot_window, payload_json, generated_at, source_range_start, source_range_end, is_stale ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) ON CONFLICT(server_key, snapshot_type, metric, snapshot_window) DO UPDATE SET payload_json = EXCLUDED.payload_json, generated_at = EXCLUDED.generated_at, source_range_start = EXCLUDED.source_range_start, source_range_end = EXCLUDED.source_range_end, is_stale = EXCLUDED.is_stale, updated_at = CURRENT_TIMESTAMP """, ( str(snapshot["server_key"]), str(snapshot["snapshot_type"]), metric, window, payload_json, generated_at, _iso(snapshot.get("source_range_start")), _iso(snapshot.get("source_range_end")), bool(snapshot.get("is_stale", False)), ), ) return HistoricalSnapshotRecord( server_key=str(snapshot["server_key"]), snapshot_type=str(snapshot["snapshot_type"]), metric=metric or None, window=window or None, payload_json=payload_json, generated_at=_parse_datetime(generated_at) or datetime.now(timezone.utc), source_range_start=_parse_datetime(_iso(snapshot.get("source_range_start"))), source_range_end=_parse_datetime(_iso(snapshot.get("source_range_end"))), is_stale=bool(snapshot.get("is_stale", False)), ) def get_snapshot( *, server_key: str, snapshot_type: str, metric: str | None, window: str | None, ) -> dict[str, object] | None: initialize_postgres_display_storage() with connect_postgres() as connection: row = connection.execute( """ SELECT * FROM displayed_historical_snapshots WHERE server_key = %s AND snapshot_type = %s AND metric = %s AND snapshot_window = %s """, (server_key, snapshot_type, metric or "", window or ""), ).fetchone() if not row: return None return { "server_key": row["server_key"], "snapshot_type": row["snapshot_type"], "metric": row["metric"] or None, "window": row["snapshot_window"] or None, "generated_at": row["generated_at"], "source_range_start": row["source_range_start"], "source_range_end": row["source_range_end"], "is_stale": bool(row["is_stale"]), "payload": json.loads(row["payload_json"]), } def list_latest_server_snapshots() -> list[dict[str, object]]: initialize_postgres_display_storage() with connect_postgres() as connection: rows = connection.execute( """ SELECT s.id AS server_id, s.external_server_id, s.server_name, s.region, g.slug AS context, snap.source_name, snap.snapshot_origin, snap.source_ref, snap.captured_at, snap.status, snap.players, snap.max_players, snap.current_map FROM servers AS s JOIN game_sources AS g ON g.id = s.game_source_id JOIN server_snapshots AS snap ON snap.server_id = s.id JOIN ( SELECT server_id, MAX(captured_at) AS captured_at FROM server_snapshots GROUP BY server_id ) AS latest ON latest.server_id = snap.server_id AND latest.captured_at = snap.captured_at ORDER BY s.server_name ASC """ ).fetchall() return [_attach_server_history(connection, dict(row)) for row in rows] def persist_server_snapshots( snapshots: Iterable[Mapping[str, object]], *, source_name: str, captured_at: str, game_source: Mapping[str, str], ) -> dict[str, object]: initialize_postgres_display_storage() persisted = 0 with connect_postgres() as connection: source = connection.execute( """ INSERT INTO game_sources (slug, display_name, provider_kind, is_active) VALUES (%s, %s, %s, TRUE) ON CONFLICT(slug) DO UPDATE SET display_name = EXCLUDED.display_name, provider_kind = EXCLUDED.provider_kind, is_active = TRUE, updated_at = CURRENT_TIMESTAMP RETURNING id """, (game_source["slug"], game_source["display_name"], game_source["provider_kind"]), ).fetchone() for snapshot in snapshots: external_server_id = str(snapshot.get("external_server_id") or "").strip() if not external_server_id: external_server_id = _fallback_external_id(snapshot.get("server_name")) server = connection.execute( """ INSERT INTO servers ( game_source_id, external_server_id, server_name, region, first_seen_at, last_seen_at ) VALUES (%s, %s, %s, %s, %s, %s) ON CONFLICT(game_source_id, external_server_id) DO UPDATE SET server_name = EXCLUDED.server_name, region = EXCLUDED.region, last_seen_at = EXCLUDED.last_seen_at, updated_at = CURRENT_TIMESTAMP RETURNING id """, ( source["id"], external_server_id, str(snapshot.get("server_name") or "Unknown server"), snapshot.get("region"), captured_at, captured_at, ), ).fetchone() connection.execute( """ INSERT INTO server_snapshots ( server_id, captured_at, status, players, max_players, current_map, source_name, snapshot_origin, source_ref, raw_payload_ref ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, NULL) ON CONFLICT(server_id, captured_at, source_name, source_ref) DO UPDATE SET status = EXCLUDED.status, players = EXCLUDED.players, max_players = EXCLUDED.max_players, current_map = EXCLUDED.current_map, snapshot_origin = EXCLUDED.snapshot_origin """, ( server["id"], captured_at, snapshot.get("status") or "unknown", snapshot.get("players"), snapshot.get("max_players"), snapshot.get("current_map"), snapshot.get("source_name") or source_name, snapshot.get("snapshot_origin"), snapshot.get("source_ref") or snapshot.get("source_name") or source_name, ), ) persisted += 1 return { "db_path": "postgresql", "captured_at": captured_at, "persisted_snapshots": persisted, "game_source_slug": game_source["slug"], } def upsert_player_event_rows(events: Iterable[object]) -> dict[str, int]: initialize_postgres_display_storage() inserted = 0 duplicates = 0 with connect_postgres() as connection: for event in events: row = connection.execute( """ INSERT INTO player_event_raw_ledger ( event_id, event_type, occurred_at, server_slug, external_match_id, source_kind, source_ref, raw_event_ref, killer_player_key, killer_display_name, victim_player_key, victim_display_name, weapon_name, weapon_category, kill_category, is_teamkill, event_value ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) ON CONFLICT(event_id) DO NOTHING RETURNING id """, ( event.event_id, event.event_type, event.occurred_at, event.server_slug, event.external_match_id, event.source_kind, event.source_ref, event.raw_event_ref, event.killer_player_key, event.killer_display_name, event.victim_player_key, event.victim_display_name, event.weapon_name, event.weapon_category, event.kill_category, bool(event.is_teamkill), max(1, int(event.event_value)), ), ).fetchone() inserted += int(bool(row)) duplicates += int(not row) return {"events_inserted": inserted, "duplicate_events": duplicates} def list_server_snapshot_history(*, server_id: str | None = None, limit: int) -> list[dict[str, object]]: initialize_postgres_display_storage() where = "" params: list[object] = [] if server_id: if server_id.strip().isdigit(): where = "WHERE s.id = %s" params.append(int(server_id)) else: where = "WHERE s.external_server_id = %s" params.append(server_id.strip()) with connect_postgres() as connection: rows = connection.execute( f""" SELECT s.id AS server_id, s.external_server_id, s.server_name, s.region, g.slug AS context, snap.source_name, snap.snapshot_origin, snap.source_ref, snap.captured_at, snap.status, snap.players, snap.max_players, snap.current_map FROM server_snapshots AS snap JOIN servers AS s ON s.id = snap.server_id JOIN game_sources AS g ON g.id = s.game_source_id {where} ORDER BY snap.captured_at DESC, s.server_name ASC LIMIT %s """, (*params, limit), ).fetchall() return [dict(row) for row in rows] def list_recent_scoreboard_matches(*, server_slug: str | None, limit: int) -> list[dict[str, object]]: initialize_postgres_display_storage() where = "" params: list[object] = [] if server_slug and server_slug != ALL_SERVERS_SLUG: where = "WHERE hs.slug = %s" params.append(server_slug) with connect_postgres() as connection: rows = connection.execute( f""" SELECT hs.slug AS server_slug, hs.display_name AS server_name, hm.external_match_id, hm.started_at, hm.ended_at, hm.map_pretty_name, hm.map_name, hm.allied_score, hm.axis_score, hm.raw_payload_ref, COUNT(stats.id) AS player_count FROM historical_matches AS hm JOIN historical_servers AS hs ON hs.id = hm.historical_server_id LEFT JOIN historical_player_match_stats AS stats ON stats.historical_match_id = hm.id {where} GROUP BY hm.id, hs.slug, hs.display_name ORDER BY COALESCE(hm.ended_at, hm.started_at) DESC LIMIT %s """, (*params, limit), ).fetchall() return [_recent_match_row(row) for row in rows] def get_scoreboard_match_detail(*, server_slug: str, match_id: str) -> dict[str, object] | None: initialize_postgres_display_storage() with connect_postgres() as connection: row = connection.execute( """ SELECT hm.id AS match_pk, hs.slug AS server_slug, hs.display_name AS server_name, hm.external_match_id, hm.started_at, hm.ended_at, hm.map_pretty_name, hm.map_name, hm.allied_score, hm.axis_score, hm.raw_payload_ref, COUNT(stats.id) AS player_count, SUM(COALESCE(stats.time_seconds, 0)) AS total_time_seconds FROM historical_matches AS hm JOIN historical_servers AS hs ON hs.id = hm.historical_server_id LEFT JOIN historical_player_match_stats AS stats ON stats.historical_match_id = hm.id WHERE hs.slug = %s AND hm.external_match_id = %s GROUP BY hm.id, hs.slug, hs.display_name LIMIT 1 """, (server_slug, match_id), ).fetchone() if not row: return None players = connection.execute( """ SELECT hp.display_name, hp.stable_player_key, hp.steam_id, stats.team_side, stats.level, stats.kills, stats.deaths, stats.teamkills, stats.combat, stats.offense, stats.defense, stats.support, stats.time_seconds FROM historical_player_match_stats AS stats JOIN historical_players AS hp ON hp.id = stats.historical_player_id WHERE stats.historical_match_id = %s ORDER BY COALESCE(stats.kills, 0) DESC, hp.display_name ASC """, (row["match_pk"],), ).fetchall() started_at = row["started_at"] ended_at = row["ended_at"] return { "server": {"slug": row["server_slug"], "name": row["server_name"]}, "match_id": row["external_match_id"], "started_at": started_at, "ended_at": ended_at, "closed_at": ended_at or started_at, "duration_seconds": _duration_seconds(started_at, ended_at), "map": {"name": row["map_name"], "pretty_name": row["map_pretty_name"] or row["map_name"]}, "result": _match_result(row["allied_score"], row["axis_score"]), "player_count": int(row["player_count"] or 0), "total_time_seconds": _int(row["total_time_seconds"]), "players": [ { "name": player["display_name"], "stable_player_key": player["stable_player_key"], "team_side": player["team_side"], **build_external_player_profile_fields(steam_id=player["steam_id"]), **{ key: _int(player[key]) for key in ( "level", "kills", "deaths", "teamkills", "combat", "offense", "defense", "support", "time_seconds", ) }, } for player in players ], "capture_basis": "public-scoreboard-match", "match_url": resolve_trusted_scoreboard_match_url(row["raw_payload_ref"], row["server_slug"]), } def list_scoreboard_server_summaries(*, server_slug: str | None) -> list[dict[str, object]]: initialize_postgres_display_storage() if server_slug == ALL_SERVERS_SLUG: rows = list_scoreboard_server_summaries(server_slug=None) return [_all_server_summary(rows)] where = "WHERE hs.slug = %s" if server_slug else "" params = (server_slug,) if server_slug else () with connect_postgres() as connection: rows = connection.execute( f""" SELECT hs.slug AS server_slug, hs.display_name AS server_name, COUNT(DISTINCT hm.id) AS matches_count, COUNT(DISTINCT hp.id) AS unique_players, COALESCE(SUM(stats.kills), 0) AS total_kills, COUNT(DISTINCT COALESCE(hm.map_pretty_name, hm.map_name)) AS map_count, MIN(COALESCE(hm.ended_at, hm.started_at, hm.created_at_source)) AS first_match_at, MAX(COALESCE(hm.ended_at, hm.started_at, hm.created_at_source)) AS last_match_at FROM historical_servers AS hs LEFT JOIN historical_matches AS hm ON hm.historical_server_id = hs.id LEFT JOIN historical_player_match_stats AS stats ON stats.historical_match_id = hm.id LEFT JOIN historical_players AS hp ON hp.id = stats.historical_player_id {where} GROUP BY hs.id ORDER BY hs.server_number ASC, hs.slug ASC """, params, ).fetchall() map_rows = connection.execute( f""" SELECT hs.slug AS server_slug, COALESCE(hm.map_pretty_name, hm.map_name, 'Mapa no disponible') AS map_name, COUNT(*) AS matches_count FROM historical_matches AS hm JOIN historical_servers AS hs ON hs.id = hm.historical_server_id {where} GROUP BY hs.slug, COALESCE(hm.map_pretty_name, hm.map_name, 'Mapa no disponible') ORDER BY hs.slug ASC, matches_count DESC, map_name ASC """, params, ).fetchall() maps: dict[str, list[dict[str, object]]] = {} for row in map_rows: maps.setdefault(str(row["server_slug"]), []) if len(maps[str(row["server_slug"])]) < 3: maps[str(row["server_slug"])].append( {"map_name": row["map_name"], "matches_count": int(row["matches_count"] or 0)} ) return [_summary_row(row, maps.get(str(row["server_slug"]), [])) for row in rows] def list_scoreboard_leaderboard( *, timeframe: str, metric: str, server_id: str | None, limit: int ) -> dict[str, object]: current = datetime.now(timezone.utc) if timeframe == "monthly": current_start = current.replace(day=1, hour=0, minute=0, second=0, microsecond=0) previous_start = (current_start - timedelta(days=1)).replace( day=1, hour=0, minute=0, second=0, microsecond=0 ) label = ("current-month", "Mes actual", "previous-closed-month-fallback", "Mes cerrado anterior") else: current_midnight = current.replace(hour=0, minute=0, second=0, microsecond=0) current_start = current_midnight - timedelta(days=current_midnight.weekday()) previous_start = current_start - timedelta(days=7) label = ("current-week", "Semana actual", "previous-closed-week-fallback", "Semana cerrada anterior") current_count = _count_scoreboard_matches(server_id, current_start, current) previous_count = _count_scoreboard_matches(server_id, previous_start, current_start) fallback = current_count <= 0 and previous_count > 0 start, end = (previous_start, current_start) if fallback else (current_start, current) rows = _leaderboard_rows(server_id=server_id, metric=metric, start=start, end=end, limit=limit) window_days = max(1, int(((end - start).total_seconds() + 86399) // 86400)) result = { "metric": metric, "window_start": _iso(start), "window_end": _iso(end), "window_days": window_days, "window_kind": label[2] if fallback else label[0], "window_label": label[3] if fallback else label[1], "uses_fallback": fallback, "selection_reason": ( "no-current-month-matches" if fallback and timeframe == "monthly" else "insufficient-current-week-sample" if fallback else label[0] ), "items": rows, } if timeframe == "monthly": result.update( { "timeframe": "monthly", "current_month_start": _iso(current_start), "current_month_closed_matches": current_count, "previous_month_closed_matches": previous_count, "sufficient_sample": { "minimum_closed_matches": 1, "current_month_closed_matches": current_count, "current_month_has_sufficient_sample": current_count > 0, "is_early_month": current.day <= 3, }, } ) else: result.update( { "current_week_start": _iso(current_start), "current_week_closed_matches": current_count, "previous_week_closed_matches": previous_count, "sufficient_sample": { "minimum_closed_matches": 1, "current_week_closed_matches": current_count, "current_week_has_sufficient_sample": current_count > 0, "is_early_week": current.weekday() <= get_historical_weekly_fallback_max_weekday(), "fallback_max_weekday": get_historical_weekly_fallback_max_weekday(), }, } ) return result def table_counts() -> dict[str, int]: initialize_postgres_display_storage() tables = ( "historical_matches", "historical_player_match_stats", "displayed_historical_snapshots", "player_event_raw_ledger", "server_snapshots", ) with connect_postgres() as connection: return { table: int(connection.execute(f"SELECT COUNT(*) AS count FROM {table}").fetchone()["count"] or 0) for table in tables } def _leaderboard_rows( *, server_id: str | None, metric: str, start: datetime, end: datetime, limit: int ) -> list[dict[str, object]]: metric_sql = { "kills": "COALESCE(SUM(stats.kills), 0)", "deaths": "COALESCE(SUM(stats.deaths), 0)", "support": "COALESCE(SUM(stats.support), 0)", "matches_over_100_kills": ( "COALESCE(SUM(CASE WHEN COALESCE(stats.kills, 0) >= 100 THEN 1 ELSE 0 END), 0)" ), }[metric] aggregate = server_id == ALL_SERVERS_SLUG where, server_params = _server_where(server_id) server_slug = f"'{ALL_SERVERS_SLUG}'" if aggregate else "hs.slug" server_name = f"'{ALL_SERVERS_DISPLAY_NAME}'" if aggregate else "hs.display_name" partition = f"'{ALL_SERVERS_SLUG}'" if aggregate else "hs.slug" group_by = "hp.id" if aggregate else "hs.slug, hs.display_name, hp.id" with connect_postgres() as connection: rows = connection.execute( f""" WITH ranked AS ( SELECT {server_slug} AS server_slug, {server_name} AS server_name, hp.stable_player_key, hp.display_name AS player_name, hp.steam_id, COUNT(DISTINCT hm.id) AS matches_count, {metric_sql} AS metric_value, ROW_NUMBER() OVER ( PARTITION BY {partition} ORDER BY {metric_sql} DESC, COUNT(DISTINCT hm.id) ASC, hp.display_name ASC ) AS ranking_position FROM historical_player_match_stats AS stats JOIN historical_matches AS hm ON hm.id = stats.historical_match_id JOIN historical_servers AS hs ON hs.id = hm.historical_server_id JOIN historical_players AS hp ON hp.id = stats.historical_player_id WHERE hm.ended_at IS NOT NULL AND hm.ended_at >= %s AND hm.ended_at < %s {where} GROUP BY {group_by} ) SELECT * FROM ranked WHERE ranking_position <= %s ORDER BY server_slug ASC, ranking_position ASC """, (_iso(start), _iso(end), *server_params, limit), ).fetchall() return [ { "server": {"slug": row["server_slug"], "name": row["server_name"]}, "time_range": {"start": _iso(start), "end": _iso(end), "window_days": max(1, (end - start).days or 1)}, "player": { "stable_player_key": row["stable_player_key"], "name": row["player_name"], "steam_id": row["steam_id"], }, "metric": metric, "ranking_position": int(row["ranking_position"]), "metric_value": int(row["metric_value"] or 0), "matches_considered": int(row["matches_count"] or 0), } for row in rows ] def _count_scoreboard_matches(server_id: str | None, start: datetime, end: datetime) -> int: where, server_params = _server_where(server_id) with connect_postgres() as connection: row = connection.execute( f""" SELECT COUNT(DISTINCT hm.id) AS count FROM historical_matches AS hm JOIN historical_servers AS hs ON hs.id = hm.historical_server_id JOIN historical_player_match_stats AS stats ON stats.historical_match_id = hm.id WHERE hm.ended_at IS NOT NULL AND hm.ended_at >= %s AND hm.ended_at < %s {where} """, (_iso(start), _iso(end), *server_params), ).fetchone() return int(row["count"] or 0) def _server_where(server_id: str | None) -> tuple[str, tuple[object, ...]]: if not server_id or server_id == ALL_SERVERS_SLUG: return "", () return "AND (hs.slug = %s OR CAST(hs.server_number AS TEXT) = %s)", (server_id, server_id) def _recent_match_row(row: Mapping[str, object]) -> dict[str, object]: return { "server": {"slug": row["server_slug"], "name": row["server_name"]}, "match_id": row["external_match_id"], "started_at": row["started_at"], "ended_at": row["ended_at"], "closed_at": row["ended_at"] or row["started_at"], "map": {"name": row["map_name"], "pretty_name": row["map_pretty_name"] or row["map_name"]}, "result": _match_result(row["allied_score"], row["axis_score"]), "player_count": int(row["player_count"] or 0), "match_url": resolve_trusted_scoreboard_match_url(row["raw_payload_ref"], row["server_slug"]), } def _summary_row(row: Mapping[str, object], top_maps: list[dict[str, object]]) -> dict[str, object]: first = row["first_match_at"] last = row["last_match_at"] matches = int(row["matches_count"] or 0) return { "server": {"slug": row["server_slug"], "name": row["server_name"]}, "matches_count": matches, "imported_matches_count": matches, "unique_players": int(row["unique_players"] or 0), "total_kills": int(row["total_kills"] or 0), "map_count": int(row["map_count"] or 0), "top_maps": top_maps, "coverage": { "basis": "postgres-migrated-public-scoreboard", "status": "available" if matches else "empty", "imported_matches_count": matches, "discovered_total_matches": None, "first_match_at": first, "last_match_at": last, "coverage_days": _coverage_days(first, last), }, "backfill": {}, "time_range": {"start": first, "end": last}, } def _all_server_summary(items: list[dict[str, object]]) -> dict[str, object]: starts = [item["time_range"]["start"] for item in items if item["time_range"]["start"]] ends = [item["time_range"]["end"] for item in items if item["time_range"]["end"]] return { "server": {"slug": ALL_SERVERS_SLUG, "name": ALL_SERVERS_DISPLAY_NAME}, "matches_count": sum(int(item["matches_count"]) for item in items), "imported_matches_count": sum(int(item["imported_matches_count"]) for item in items), "unique_players": None, "total_kills": sum(int(item["total_kills"]) for item in items), "map_count": None, "top_maps": [], "coverage": {"basis": "postgres-migrated-public-scoreboard", "status": "available" if items else "empty"}, "backfill": {}, "time_range": {"start": min(starts) if starts else None, "end": max(ends) if ends else None}, } def _attach_server_history(connection: Any, item: dict[str, object]) -> dict[str, object]: rows = connection.execute( """ SELECT captured_at, status, players FROM server_snapshots WHERE server_id = %s ORDER BY captured_at DESC LIMIT %s """, (item["server_id"], SUMMARY_SNAPSHOT_LIMIT), ).fetchall() players = [int(row["players"]) for row in rows if row["players"] is not None] online = [row for row in rows if row["status"] == "online"] item["history_summary"] = { "window_size": SUMMARY_SNAPSHOT_LIMIT, "recent_capture_count": len(rows), "recent_online_count": len(online), "recent_average_players": round(sum(players) / len(players), 1) if players else None, "recent_peak_players": max(players, default=None), "last_seen_online_at": online[0]["captured_at"] if online else None, "minutes_since_last_capture": _minutes_since(rows[0]["captured_at"]) if rows else None, } return item def _match_result(allied: object, axis: object) -> dict[str, object]: allied_int, axis_int = _int(allied), _int(axis) winner = None if allied_int is not None and axis_int is not None: winner = "allied" if allied_int > axis_int else "axis" if axis_int > allied_int else "draw" return {"allied_score": allied_int, "axis_score": axis_int, "winner": winner} def _duration_seconds(start: object, end: object) -> int | None: start_point, end_point = _parse_datetime(_iso(start)), _parse_datetime(_iso(end)) return max(0, int((end_point - start_point).total_seconds())) if start_point and end_point else None def _coverage_days(start: object, end: object) -> int | None: seconds = _duration_seconds(start, end) return max(1, int((seconds + 86399) // 86400)) if seconds is not None else None def _minutes_since(value: object) -> int | None: point = _parse_datetime(_iso(value)) return max(0, int((datetime.now(timezone.utc) - point).total_seconds() // 60)) if point else None def _int(value: object) -> int | None: try: return None if value is None else int(value) except (TypeError, ValueError): return None def _fallback_external_id(value: object) -> str: normalized = "".join( character.lower() if character.isalnum() else "-" for character in str(value or "unknown-server") ) compact = "-".join(part for part in normalized.split("-") if part) return compact or "unknown-server" def _iso(value: object) -> str | None: if isinstance(value, datetime): point = value if value.tzinfo else value.replace(tzinfo=timezone.utc) return point.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") text = str(value or "").strip() return text or None def _json_payload_default(value: object) -> str: if isinstance(value, datetime): return _iso(value) or "" raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable") def _parse_datetime(value: str | None) -> datetime | None: if not value: return None try: point = datetime.fromisoformat(value.replace("Z", "+00:00")) except ValueError: return None return point.astimezone(timezone.utc) if point.tzinfo else point.replace(tzinfo=timezone.utc) def _utc_now_iso() -> str: return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")