"""SQLite persistence for historical CRCON scoreboard data.""" from __future__ import annotations import sqlite3 from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Mapping from .config import ( get_historical_refresh_overlap_hours, get_historical_weekly_fallback_max_weekday, get_historical_weekly_fallback_min_matches, get_storage_path, use_postgres_rcon_storage, ) from .historical_models import HistoricalServerDefinition from .monthly_mvp import build_monthly_mvp_rankings from .monthly_mvp_v2 import build_monthly_mvp_v2_rankings from .player_external_profiles import build_external_player_profile_fields from .scoreboard_origins import ( list_trusted_public_scoreboard_origins, resolve_trusted_scoreboard_match_url, ) from .sqlite_utils import connect_sqlite_writer DEFAULT_HISTORICAL_SERVERS = tuple( HistoricalServerDefinition( slug=origin.slug, display_name=origin.display_name, scoreboard_base_url=origin.base_url, server_number=origin.server_number, source_kind=origin.source_kind, ) for origin in list_trusted_public_scoreboard_origins() ) ALL_SERVERS_SLUG = "all-servers" ALL_SERVERS_DISPLAY_NAME = "Todos" DEFAULT_WEEKLY_WINDOW_DAYS = 7 SUPPORTED_WEEKLY_LEADERBOARD_METRICS = frozenset( { "kills", "deaths", "support", "matches_over_100_kills", } ) SUPPORTED_MONTHLY_LEADERBOARD_METRICS = SUPPORTED_WEEKLY_LEADERBOARD_METRICS def initialize_historical_storage(*, db_path: Path | None = None) -> Path: """Create or migrate the local SQLite schema for historical data.""" resolved_path = db_path or get_storage_path() resolved_path.parent.mkdir(parents=True, exist_ok=True) with _connect(resolved_path) as connection: legacy_historical_schema = _has_legacy_historical_schema(connection) if legacy_historical_schema: _rename_legacy_historical_tables(connection) connection.executescript( """ CREATE TABLE IF NOT EXISTS historical_servers ( id INTEGER PRIMARY KEY AUTOINCREMENT, 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 INTEGER PRIMARY KEY AUTOINCREMENT, 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 INTEGER PRIMARY KEY AUTOINCREMENT, historical_server_id INTEGER NOT NULL, external_match_id TEXT NOT NULL, historical_map_id INTEGER, 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), FOREIGN KEY (historical_server_id) REFERENCES historical_servers(id), FOREIGN KEY (historical_map_id) REFERENCES historical_maps(id) ); CREATE TABLE IF NOT EXISTS historical_players ( id INTEGER PRIMARY KEY AUTOINCREMENT, 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 INTEGER PRIMARY KEY AUTOINCREMENT, historical_match_id INTEGER NOT NULL, historical_player_id INTEGER NOT NULL, match_player_ref TEXT, team_side TEXT, level INTEGER, kills INTEGER, deaths INTEGER, teamkills INTEGER, time_seconds INTEGER, kills_per_minute REAL, deaths_per_minute REAL, kill_death_ratio REAL, 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), FOREIGN KEY (historical_match_id) REFERENCES historical_matches(id), FOREIGN KEY (historical_player_id) REFERENCES historical_players(id) ); CREATE TABLE IF NOT EXISTS historical_ingestion_runs ( id INTEGER PRIMARY KEY AUTOINCREMENT, mode TEXT NOT NULL, status TEXT NOT NULL, started_at TEXT NOT NULL, completed_at TEXT, target_server_slug TEXT, pages_processed INTEGER NOT NULL DEFAULT 0, matches_seen INTEGER NOT NULL DEFAULT 0, matches_inserted INTEGER NOT NULL DEFAULT 0, matches_updated INTEGER NOT NULL DEFAULT 0, player_rows_inserted INTEGER NOT NULL DEFAULT 0, player_rows_updated INTEGER NOT NULL DEFAULT 0, notes TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS historical_backfill_progress ( historical_server_id INTEGER NOT NULL, mode TEXT NOT NULL, next_page INTEGER NOT NULL DEFAULT 1, last_completed_page INTEGER, discovered_total_matches INTEGER, discovered_total_pages INTEGER, archive_exhausted INTEGER NOT NULL DEFAULT 0, last_run_id INTEGER, last_run_status TEXT, last_run_started_at TEXT, last_run_completed_at TEXT, last_error TEXT, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (historical_server_id, mode), FOREIGN KEY (historical_server_id) REFERENCES historical_servers(id) ); CREATE INDEX IF NOT EXISTS idx_historical_matches_server_end ON historical_matches(historical_server_id, ended_at DESC, started_at DESC); CREATE INDEX IF NOT EXISTS idx_historical_player_stats_match ON historical_player_match_stats(historical_match_id); CREATE INDEX IF NOT EXISTS idx_historical_players_steam ON historical_players(steam_id); CREATE INDEX IF NOT EXISTS idx_historical_backfill_progress_run ON historical_backfill_progress(last_run_id); """ ) _seed_default_historical_servers(connection) if legacy_historical_schema: _migrate_legacy_historical_data(connection) _normalize_historical_player_identities(connection) _normalize_historical_match_identities(connection) return resolved_path def list_historical_servers(*, db_path: Path | None = None) -> list[dict[str, object]]: """Return configured CRCON historical sources.""" resolved_path = initialize_historical_storage(db_path=db_path) with _connect(resolved_path) as connection: rows = connection.execute( """ SELECT slug, display_name, scoreboard_base_url, server_number, source_kind FROM historical_servers ORDER BY slug ASC """ ).fetchall() return [dict(row) for row in rows] def start_ingestion_run( *, mode: str, target_server_slug: str | None = None, db_path: Path | None = None, ) -> int: """Create a row tracking one ingestion execution.""" resolved_path = initialize_historical_storage(db_path=db_path) with _connect(resolved_path) as connection: cursor = connection.execute( """ INSERT INTO historical_ingestion_runs ( mode, status, started_at, target_server_slug ) VALUES (?, 'running', ?, ?) """, (mode, _utc_now_iso(), target_server_slug), ) return int(cursor.lastrowid) def finalize_ingestion_run( run_id: int, *, status: str, pages_processed: int, matches_seen: int, matches_inserted: int, matches_updated: int, player_rows_inserted: int, player_rows_updated: int, notes: str | None = None, db_path: Path | None = None, ) -> None: """Update an ingestion run row with outcome metrics.""" resolved_path = initialize_historical_storage(db_path=db_path) with _connect(resolved_path) as connection: connection.execute( """ UPDATE historical_ingestion_runs SET status = ?, completed_at = ?, pages_processed = ?, matches_seen = ?, matches_inserted = ?, matches_updated = ?, player_rows_inserted = ?, player_rows_updated = ?, notes = ? WHERE id = ? """, ( status, _utc_now_iso(), pages_processed, matches_seen, matches_inserted, matches_updated, player_rows_inserted, player_rows_updated, notes, run_id, ), ) def mark_backfill_progress_started( *, server_slug: str, mode: str, run_id: int, db_path: Path | None = None, ) -> None: """Persist the start of one resumable historical backfill attempt.""" resolved_path = initialize_historical_storage(db_path=db_path) with _connect(resolved_path) as connection: server_row = _resolve_historical_server(connection, server_slug) connection.execute( """ INSERT INTO historical_backfill_progress ( historical_server_id, mode, next_page, archive_exhausted, last_run_id, last_run_status, last_run_started_at, last_run_completed_at, last_error ) VALUES (?, ?, 1, 0, ?, 'running', ?, NULL, NULL) ON CONFLICT(historical_server_id, mode) DO UPDATE SET last_run_id = excluded.last_run_id, last_run_status = excluded.last_run_status, last_run_started_at = excluded.last_run_started_at, last_run_completed_at = NULL, last_error = NULL, archive_exhausted = CASE WHEN excluded.mode = 'bootstrap' THEN 0 ELSE historical_backfill_progress.archive_exhausted END, updated_at = CURRENT_TIMESTAMP """, (server_row["id"], mode, run_id, _utc_now_iso()), ) def mark_backfill_progress_page_completed( *, server_slug: str, mode: str, page_number: int, page_size: int, run_id: int, discovered_total_matches: int | None, db_path: Path | None = None, ) -> None: """Persist the latest completed page so bootstraps can resume safely.""" resolved_path = initialize_historical_storage(db_path=db_path) discovered_total_pages = None if discovered_total_matches and page_size > 0: discovered_total_pages = (discovered_total_matches + page_size - 1) // page_size with _connect(resolved_path) as connection: server_row = _resolve_historical_server(connection, server_slug) connection.execute( """ INSERT INTO historical_backfill_progress ( historical_server_id, mode, next_page, last_completed_page, discovered_total_matches, discovered_total_pages, archive_exhausted, last_run_id, last_run_status, last_run_started_at, last_run_completed_at, last_error ) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, NULL, NULL) ON CONFLICT(historical_server_id, mode) DO UPDATE SET next_page = excluded.next_page, last_completed_page = excluded.last_completed_page, discovered_total_matches = COALESCE( excluded.discovered_total_matches, historical_backfill_progress.discovered_total_matches ), discovered_total_pages = COALESCE( excluded.discovered_total_pages, historical_backfill_progress.discovered_total_pages ), archive_exhausted = 0, last_run_id = excluded.last_run_id, last_run_status = excluded.last_run_status, last_run_started_at = excluded.last_run_started_at, last_run_completed_at = NULL, last_error = NULL, updated_at = CURRENT_TIMESTAMP """, ( server_row["id"], mode, page_number + 1, page_number, discovered_total_matches, discovered_total_pages, run_id, "running", _utc_now_iso(), ), ) def finalize_backfill_progress( *, server_slug: str, mode: str, run_id: int, status: str, archive_exhausted: bool = False, error_message: str | None = None, db_path: Path | None = None, ) -> None: """Persist the final state of one resumable historical backfill attempt.""" resolved_path = initialize_historical_storage(db_path=db_path) with _connect(resolved_path) as connection: server_row = _resolve_historical_server(connection, server_slug) connection.execute( """ INSERT INTO historical_backfill_progress ( historical_server_id, mode, next_page, archive_exhausted, last_run_id, last_run_status, last_run_started_at, last_run_completed_at, last_error ) VALUES (?, ?, 1, ?, ?, ?, ?, ?, ?) ON CONFLICT(historical_server_id, mode) DO UPDATE SET archive_exhausted = CASE WHEN excluded.last_run_status = 'success' AND excluded.archive_exhausted = 1 THEN 1 WHEN excluded.last_run_status = 'success' THEN historical_backfill_progress.archive_exhausted ELSE historical_backfill_progress.archive_exhausted END, last_run_id = excluded.last_run_id, last_run_status = excluded.last_run_status, last_run_started_at = COALESCE( historical_backfill_progress.last_run_started_at, excluded.last_run_started_at ), last_run_completed_at = excluded.last_run_completed_at, last_error = excluded.last_error, updated_at = CURRENT_TIMESTAMP """, ( server_row["id"], mode, 1 if archive_exhausted else 0, run_id, status, _utc_now_iso(), _utc_now_iso(), error_message, ), ) def get_backfill_resume_page( server_slug: str, *, mode: str = "bootstrap", db_path: Path | None = None, ) -> int: """Return the next page recorded for one resumable historical backfill.""" resolved_path = initialize_historical_storage(db_path=db_path) with _connect(resolved_path) as connection: server_row = _resolve_historical_server(connection, server_slug) row = connection.execute( """ SELECT next_page FROM historical_backfill_progress WHERE historical_server_id = ? AND mode = ? """, (server_row["id"], mode), ).fetchone() return max(1, int(row["next_page"])) if row and row["next_page"] else 1 def list_historical_backfill_progress( *, server_slug: str | None = None, mode: str = "bootstrap", db_path: Path | None = None, ) -> list[dict[str, object]]: """Return persisted resume checkpoints and last run state per server.""" resolved_path = initialize_historical_storage(db_path=db_path) where_clause = "" params: list[object] = [mode] if server_slug: where_clause = "WHERE historical_servers.slug = ?" params.append(server_slug) with _connect(resolved_path) as connection: rows = connection.execute( f""" SELECT historical_servers.slug AS server_slug, historical_servers.display_name AS server_name, progress.mode AS mode, progress.next_page AS next_page, progress.last_completed_page AS last_completed_page, progress.discovered_total_matches AS discovered_total_matches, progress.discovered_total_pages AS discovered_total_pages, progress.archive_exhausted AS archive_exhausted, progress.last_run_id AS last_run_id, progress.last_run_status AS last_run_status, progress.last_run_started_at AS last_run_started_at, progress.last_run_completed_at AS last_run_completed_at, progress.last_error AS last_error FROM historical_servers LEFT JOIN historical_backfill_progress AS progress ON progress.historical_server_id = historical_servers.id AND progress.mode = ? {where_clause} ORDER BY historical_servers.server_number ASC, historical_servers.slug ASC """, params, ).fetchall() items: list[dict[str, object]] = [] for row in rows: items.append( { "server": { "slug": row["server_slug"], "name": row["server_name"], }, "mode": row["mode"] or mode, "next_page": int(row["next_page"] or 1), "last_completed_page": _coerce_int(row["last_completed_page"]), "discovered_total_matches": _coerce_int(row["discovered_total_matches"]), "discovered_total_pages": _coerce_int(row["discovered_total_pages"]), "archive_exhausted": bool(row["archive_exhausted"]), "last_run": { "run_id": _coerce_int(row["last_run_id"]), "status": _stringify(row["last_run_status"]), "started_at": _stringify(row["last_run_started_at"]), "completed_at": _stringify(row["last_run_completed_at"]), "error": _stringify(row["last_error"]), }, } ) return items def upsert_historical_match( *, server_slug: str, match_payload: Mapping[str, object], db_path: Path | None = None, ) -> dict[str, int]: """Persist one historical match and its player stats idempotently.""" resolved_path = initialize_historical_storage(db_path=db_path) match_external_id = _stringify(match_payload.get("id")) if not match_external_id: raise ValueError("Historical match payload is missing a stable id.") with _connect(resolved_path) as connection: server_row = _resolve_historical_server(connection, server_slug) map_id = _upsert_historical_map(connection, match_payload) match_row = connection.execute( """ SELECT id FROM historical_matches WHERE historical_server_id = ? AND external_match_id = ? """, (server_row["id"], match_external_id), ).fetchone() match_exists = match_row is not None connection.execute( """ INSERT INTO historical_matches ( historical_server_id, external_match_id, historical_map_id, created_at_source, started_at, ended_at, map_name, map_pretty_name, game_mode, image_name, allied_score, axis_score, last_seen_at, raw_payload_ref ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(historical_server_id, external_match_id) DO UPDATE SET historical_map_id = excluded.historical_map_id, created_at_source = excluded.created_at_source, started_at = excluded.started_at, ended_at = excluded.ended_at, map_name = excluded.map_name, map_pretty_name = excluded.map_pretty_name, game_mode = excluded.game_mode, image_name = excluded.image_name, allied_score = excluded.allied_score, axis_score = excluded.axis_score, last_seen_at = excluded.last_seen_at, raw_payload_ref = excluded.raw_payload_ref, updated_at = CURRENT_TIMESTAMP """, ( server_row["id"], match_external_id, map_id, _normalize_timestamp(match_payload.get("creation_time")), _normalize_timestamp(match_payload.get("start")), _normalize_timestamp(match_payload.get("end")), _extract_map_name(match_payload), _extract_map_pretty_name(match_payload), _extract_map_game_mode(match_payload), _extract_map_image_name(match_payload), _coerce_int(_get_nested(match_payload, "result", "allied")), _coerce_int(_get_nested(match_payload, "result", "axis")), _utc_now_iso(), f"{server_row['scoreboard_base_url']}/games/{match_external_id}", ), ) match_id_row = connection.execute( """ SELECT id FROM historical_matches WHERE historical_server_id = ? AND external_match_id = ? """, (server_row["id"], match_external_id), ).fetchone() if match_id_row is None: raise RuntimeError("Failed to persist historical match.") player_rows_inserted = 0 player_rows_updated = 0 for player_payload in _coerce_list(match_payload.get("player_stats")): player_id = _upsert_historical_player(connection, player_payload) stat_exists = connection.execute( """ SELECT id FROM historical_player_match_stats WHERE historical_match_id = ? AND historical_player_id = ? """, (match_id_row["id"], player_id), ).fetchone() connection.execute( """ INSERT INTO historical_player_match_stats ( historical_match_id, historical_player_id, match_player_ref, team_side, level, kills, deaths, teamkills, time_seconds, kills_per_minute, deaths_per_minute, kill_death_ratio, combat, offense, defense, support ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(historical_match_id, historical_player_id) DO UPDATE SET match_player_ref = excluded.match_player_ref, team_side = excluded.team_side, level = excluded.level, kills = excluded.kills, deaths = excluded.deaths, teamkills = excluded.teamkills, time_seconds = excluded.time_seconds, kills_per_minute = excluded.kills_per_minute, deaths_per_minute = excluded.deaths_per_minute, kill_death_ratio = excluded.kill_death_ratio, combat = excluded.combat, offense = excluded.offense, defense = excluded.defense, support = excluded.support, updated_at = CURRENT_TIMESTAMP """, ( match_id_row["id"], player_id, _stringify(player_payload.get("id")), _stringify(_get_nested(player_payload, "team", "side")), _coerce_int(player_payload.get("level")), _coerce_int(player_payload.get("kills")), _coerce_int(player_payload.get("deaths")), _coerce_int(player_payload.get("teamkills")), _coerce_int(player_payload.get("time_seconds")), _coerce_float(player_payload.get("kills_per_minute")), _coerce_float(player_payload.get("deaths_per_minute")), _coerce_float(player_payload.get("kill_death_ratio")), _coerce_int(player_payload.get("combat")), _coerce_int(player_payload.get("offense")), _coerce_int(player_payload.get("defense")), _coerce_int(player_payload.get("support")), ), ) if stat_exists is None: player_rows_inserted += 1 else: player_rows_updated += 1 return { "matches_inserted": 0 if match_exists else 1, "matches_updated": 1 if match_exists else 0, "player_rows_inserted": player_rows_inserted, "player_rows_updated": player_rows_updated, } def get_refresh_cutoff_for_server( server_slug: str, *, overlap_hours: int | None = None, db_path: Path | None = None, ) -> str | None: """Return the timestamp used to stop incremental scans once older pages appear.""" resolved_overlap_hours = ( get_historical_refresh_overlap_hours() if overlap_hours is None else overlap_hours ) if resolved_overlap_hours < 0: raise ValueError("overlap_hours must be zero or positive.") resolved_path = initialize_historical_storage(db_path=db_path) with _connect(resolved_path) as connection: server_row = _resolve_historical_server(connection, server_slug) row = connection.execute( """ SELECT COALESCE(MAX(ended_at), MAX(started_at), MAX(created_at_source)) AS latest_seen_at FROM historical_matches WHERE historical_server_id = ? """, (server_row["id"],), ).fetchone() latest_seen_at = _stringify(row["latest_seen_at"] if row else None) if not latest_seen_at: return None cutoff = _parse_timestamp(latest_seen_at) - timedelta(hours=resolved_overlap_hours) return cutoff.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") def list_recent_historical_matches( *, server_slug: str | None = None, limit: int = 20, db_path: Path | None = None, ) -> list[dict[str, object]]: """Return recent persisted matches grouped for the historical API layer.""" if use_postgres_rcon_storage(explicit_sqlite_path=db_path): from .postgres_display_storage import list_recent_scoreboard_matches return list_recent_scoreboard_matches(server_slug=server_slug, limit=limit) resolved_path = initialize_historical_storage(db_path=db_path) where_clause = "" params: list[object] = [] if server_slug and not _is_all_servers_selector(server_slug): where_clause = "WHERE historical_servers.slug = ?" params.append(server_slug) params.append(limit) with _connect(resolved_path) as connection: rows = connection.execute( f""" SELECT historical_servers.slug AS server_slug, historical_servers.display_name AS server_name, historical_matches.external_match_id, historical_matches.started_at, historical_matches.ended_at, historical_matches.map_pretty_name, historical_matches.map_name, historical_matches.allied_score, historical_matches.axis_score, historical_matches.raw_payload_ref, historical_servers.slug, historical_servers.scoreboard_base_url, COUNT(historical_player_match_stats.id) AS player_count FROM historical_matches INNER JOIN historical_servers ON historical_servers.id = historical_matches.historical_server_id LEFT JOIN historical_player_match_stats ON historical_player_match_stats.historical_match_id = historical_matches.id {where_clause} GROUP BY historical_matches.id ORDER BY COALESCE(historical_matches.ended_at, historical_matches.started_at) DESC LIMIT ? """, params, ).fetchall() items: list[dict[str, object]] = [] for row in rows: items.append( { "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": { "allied_score": _coerce_int(row["allied_score"]), "axis_score": _coerce_int(row["axis_score"]), "winner": _resolve_match_winner( row["allied_score"], row["axis_score"], ), }, "player_count": int(row["player_count"] or 0), "match_url": _resolve_safe_match_url( row["raw_payload_ref"], row["server_slug"], ), } ) return items def get_historical_match_detail( *, server_slug: str, match_id: str, db_path: Path | None = None, ) -> dict[str, object] | None: """Return one persisted public-scoreboard match detail for the historical API layer.""" normalized_server_slug = _stringify(server_slug) normalized_match_id = _stringify(match_id) if not normalized_server_slug or not normalized_match_id: return None if use_postgres_rcon_storage(explicit_sqlite_path=db_path): from .postgres_display_storage import get_scoreboard_match_detail return get_scoreboard_match_detail( server_slug=normalized_server_slug, match_id=normalized_match_id, ) resolved_path = initialize_historical_storage(db_path=db_path) with _connect(resolved_path) as connection: row = connection.execute( """ SELECT historical_matches.id AS match_pk, historical_servers.slug AS server_slug, historical_servers.display_name AS server_name, historical_matches.external_match_id, historical_matches.started_at, historical_matches.ended_at, historical_matches.map_pretty_name, historical_matches.map_name, historical_matches.allied_score, historical_matches.axis_score, historical_matches.raw_payload_ref, historical_servers.slug, historical_servers.scoreboard_base_url, COUNT(historical_player_match_stats.id) AS player_count, SUM(COALESCE(historical_player_match_stats.time_seconds, 0)) AS total_time_seconds FROM historical_matches INNER JOIN historical_servers ON historical_servers.id = historical_matches.historical_server_id LEFT JOIN historical_player_match_stats ON historical_player_match_stats.historical_match_id = historical_matches.id WHERE historical_servers.slug = ? AND historical_matches.external_match_id = ? GROUP BY historical_matches.id LIMIT 1 """, (normalized_server_slug, normalized_match_id), ).fetchone() player_rows = [] if row is not None: player_rows = connection.execute( """ SELECT historical_players.display_name, historical_players.stable_player_key, historical_players.steam_id, historical_player_match_stats.team_side, historical_player_match_stats.level, historical_player_match_stats.kills, historical_player_match_stats.deaths, historical_player_match_stats.teamkills, historical_player_match_stats.combat, historical_player_match_stats.offense, historical_player_match_stats.defense, historical_player_match_stats.support, historical_player_match_stats.time_seconds FROM historical_player_match_stats INNER JOIN historical_players ON historical_players.id = historical_player_match_stats.historical_player_id WHERE historical_player_match_stats.historical_match_id = ? ORDER BY COALESCE(historical_player_match_stats.kills, 0) DESC, historical_players.display_name ASC """, (row["match_pk"],), ).fetchall() if row is None: return None 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": _calculate_match_duration_seconds(started_at, ended_at), "map": { "name": row["map_name"], "pretty_name": row["map_pretty_name"] or row["map_name"], }, "result": { "allied_score": _coerce_int(row["allied_score"]), "axis_score": _coerce_int(row["axis_score"]), "winner": _resolve_match_winner( row["allied_score"], row["axis_score"], ), }, "player_count": int(row["player_count"] or 0), "total_time_seconds": _coerce_int(row["total_time_seconds"]), "players": [ { "name": player_row["display_name"], "stable_player_key": player_row["stable_player_key"], "team_side": player_row["team_side"], **build_external_player_profile_fields(steam_id=player_row["steam_id"]), "level": _coerce_int(player_row["level"]), "kills": _coerce_int(player_row["kills"]), "deaths": _coerce_int(player_row["deaths"]), "teamkills": _coerce_int(player_row["teamkills"]), "combat": _coerce_int(player_row["combat"]), "offense": _coerce_int(player_row["offense"]), "defense": _coerce_int(player_row["defense"]), "support": _coerce_int(player_row["support"]), "time_seconds": _coerce_int(player_row["time_seconds"]), } for player_row in player_rows ], "capture_basis": "public-scoreboard-match", "match_url": _resolve_safe_match_url( row["raw_payload_ref"], row["server_slug"], ), } def list_historical_server_summaries( *, server_slug: str | None = None, db_path: Path | None = None, ) -> list[dict[str, object]]: """Return aggregate historical metrics per server.""" if use_postgres_rcon_storage(explicit_sqlite_path=db_path): from .postgres_display_storage import list_scoreboard_server_summaries return list_scoreboard_server_summaries(server_slug=server_slug) resolved_path = initialize_historical_storage(db_path=db_path) if _is_all_servers_selector(server_slug): return [_build_all_servers_summary(db_path=resolved_path)] where_clause = "" params: list[object] = [] if server_slug: where_clause = "WHERE historical_servers.slug = ?" params.append(server_slug) with _connect(resolved_path) as connection: summary_rows = connection.execute( f""" SELECT historical_servers.slug AS server_slug, historical_servers.display_name AS server_name, COUNT(DISTINCT historical_matches.id) AS matches_count, COUNT(DISTINCT historical_players.id) AS unique_players, COALESCE(SUM(historical_player_match_stats.kills), 0) AS total_kills, COUNT(DISTINCT COALESCE(historical_matches.map_pretty_name, historical_matches.map_name)) AS map_count, MIN(COALESCE(historical_matches.ended_at, historical_matches.started_at, historical_matches.created_at_source)) AS first_match_at, MAX(COALESCE(historical_matches.ended_at, historical_matches.started_at, historical_matches.created_at_source)) AS last_match_at FROM historical_servers LEFT JOIN historical_matches ON historical_matches.historical_server_id = historical_servers.id LEFT JOIN historical_player_match_stats ON historical_player_match_stats.historical_match_id = historical_matches.id LEFT JOIN historical_players ON historical_players.id = historical_player_match_stats.historical_player_id {where_clause} GROUP BY historical_servers.id ORDER BY historical_servers.server_number ASC, historical_servers.slug ASC """, params, ).fetchall() map_rows = connection.execute( f""" SELECT historical_servers.slug AS server_slug, COALESCE(historical_matches.map_pretty_name, historical_matches.map_name, 'Mapa no disponible') AS map_name, COUNT(*) AS matches_count FROM historical_matches INNER JOIN historical_servers ON historical_servers.id = historical_matches.historical_server_id {where_clause} GROUP BY historical_servers.slug, COALESCE(historical_matches.map_pretty_name, historical_matches.map_name, 'Mapa no disponible') ORDER BY historical_servers.slug ASC, matches_count DESC, map_name ASC """, params, ).fetchall() progress_by_server = { item["server"]["slug"]: item for item in list_historical_backfill_progress( server_slug=server_slug, db_path=resolved_path, ) } top_maps_by_server: dict[str, list[dict[str, object]]] = {} for row in map_rows: server_key = str(row["server_slug"]) top_maps_by_server.setdefault(server_key, []) if len(top_maps_by_server[server_key]) >= 3: continue top_maps_by_server[server_key].append( { "map_name": row["map_name"], "matches_count": int(row["matches_count"] or 0), } ) items: list[dict[str, object]] = [] for row in summary_rows: matches_count = int(row["matches_count"] or 0) first_match_at = _stringify(row["first_match_at"]) last_match_at = _stringify(row["last_match_at"]) coverage_days = _calculate_coverage_days(first_match_at, last_match_at) progress = progress_by_server.get(str(row["server_slug"]), {}) discovered_total_matches = _coerce_int(progress.get("discovered_total_matches")) items.append( { "server": { "slug": row["server_slug"], "name": row["server_name"], }, "matches_count": matches_count, "imported_matches_count": matches_count, "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_by_server.get(str(row["server_slug"]), []), "coverage": { "basis": "persisted-import", "status": _classify_coverage_status(matches_count, coverage_days), "imported_matches_count": matches_count, "discovered_total_matches": discovered_total_matches, "first_match_at": first_match_at, "last_match_at": last_match_at, "coverage_days": coverage_days, }, "backfill": { "mode": progress.get("mode", "bootstrap"), "next_page": _coerce_int(progress.get("next_page")) or 1, "last_completed_page": _coerce_int(progress.get("last_completed_page")), "discovered_total_matches": discovered_total_matches, "discovered_total_pages": _coerce_int(progress.get("discovered_total_pages")), "remaining_matches_estimate": ( max(discovered_total_matches - matches_count, 0) if discovered_total_matches is not None else None ), "archive_exhausted": bool(progress.get("archive_exhausted")), "last_run": progress.get("last_run"), }, "time_range": { "start": first_match_at, "end": last_match_at, }, } ) return items def list_historical_coverage_report( *, server_slug: str | None = None, db_path: Path | None = None, ) -> list[dict[str, object]]: """Return persisted coverage metrics used to validate historical bootstrap depth.""" resolved_path = initialize_historical_storage(db_path=db_path) where_clause = "" params: list[object] = [] if server_slug: where_clause = "WHERE historical_servers.slug = ?" params.append(server_slug) with _connect(resolved_path) as connection: rows = connection.execute( f""" SELECT historical_servers.slug AS server_slug, historical_servers.display_name AS server_name, historical_servers.scoreboard_base_url AS scoreboard_base_url, historical_servers.server_number AS server_number, COUNT(DISTINCT historical_matches.id) AS imported_matches_count, COUNT(DISTINCT historical_players.id) AS unique_players, COUNT(DISTINCT historical_player_match_stats.id) AS player_stat_rows, MIN(COALESCE(historical_matches.ended_at, historical_matches.started_at, historical_matches.created_at_source)) AS first_match_at, MAX(COALESCE(historical_matches.ended_at, historical_matches.started_at, historical_matches.created_at_source)) AS last_match_at FROM historical_servers LEFT JOIN historical_matches ON historical_matches.historical_server_id = historical_servers.id LEFT JOIN historical_player_match_stats ON historical_player_match_stats.historical_match_id = historical_matches.id LEFT JOIN historical_players ON historical_players.id = historical_player_match_stats.historical_player_id {where_clause} GROUP BY historical_servers.id ORDER BY historical_servers.server_number ASC, historical_servers.slug ASC """, params, ).fetchall() items: list[dict[str, object]] = [] progress_by_server = { item["server"]["slug"]: item for item in list_historical_backfill_progress( server_slug=server_slug, db_path=resolved_path, ) } for row in rows: first_match_at = _stringify(row["first_match_at"]) last_match_at = _stringify(row["last_match_at"]) progress = progress_by_server.get(str(row["server_slug"]), {}) items.append( { "server": { "slug": row["server_slug"], "name": row["server_name"], "server_number": row["server_number"], "scoreboard_base_url": row["scoreboard_base_url"], }, "imported_matches_count": int(row["imported_matches_count"] or 0), "unique_players": int(row["unique_players"] or 0), "player_stat_rows": int(row["player_stat_rows"] or 0), "first_match_at": first_match_at, "last_match_at": last_match_at, "coverage_days": _calculate_coverage_days(first_match_at, last_match_at), "backfill": { "next_page": _coerce_int(progress.get("next_page")) or 1, "last_completed_page": _coerce_int(progress.get("last_completed_page")), "discovered_total_matches": _coerce_int( progress.get("discovered_total_matches") ), "discovered_total_pages": _coerce_int( progress.get("discovered_total_pages") ), "archive_exhausted": bool(progress.get("archive_exhausted")), "last_run": progress.get("last_run"), }, } ) return items def get_historical_player_profile( player_id: str, *, db_path: Path | None = None, ) -> dict[str, object] | None: """Return aggregate historical metrics for one player identity.""" resolved_player_id = player_id.strip() if not resolved_player_id: return None resolved_path = initialize_historical_storage(db_path=db_path) with _connect(resolved_path) as connection: player_row = connection.execute( """ SELECT historical_players.id, historical_players.stable_player_key, historical_players.display_name, historical_players.steam_id, historical_players.source_player_id, COUNT(DISTINCT historical_matches.id) AS matches_count, COALESCE(SUM(historical_player_match_stats.kills), 0) AS total_kills, MIN(COALESCE(historical_matches.ended_at, historical_matches.started_at, historical_matches.created_at_source)) AS first_match_at, MAX(COALESCE(historical_matches.ended_at, historical_matches.started_at, historical_matches.created_at_source)) AS last_match_at FROM historical_players LEFT JOIN historical_player_match_stats ON historical_player_match_stats.historical_player_id = historical_players.id LEFT JOIN historical_matches ON historical_matches.id = historical_player_match_stats.historical_match_id WHERE historical_players.stable_player_key = ? OR historical_players.steam_id = ? OR historical_players.source_player_id = ? GROUP BY historical_players.id ORDER BY historical_players.display_name ASC LIMIT 1 """, (resolved_player_id, resolved_player_id, resolved_player_id), ).fetchone() if player_row is None: return None server_rows = connection.execute( """ SELECT historical_servers.slug AS server_slug, historical_servers.display_name AS server_name, COUNT(DISTINCT historical_matches.id) AS matches_count, COALESCE(SUM(historical_player_match_stats.kills), 0) AS total_kills, MIN(COALESCE(historical_matches.ended_at, historical_matches.started_at, historical_matches.created_at_source)) AS first_match_at, MAX(COALESCE(historical_matches.ended_at, historical_matches.started_at, historical_matches.created_at_source)) AS last_match_at FROM historical_player_match_stats INNER JOIN historical_matches ON historical_matches.id = historical_player_match_stats.historical_match_id INNER JOIN historical_servers ON historical_servers.id = historical_matches.historical_server_id WHERE historical_player_match_stats.historical_player_id = ? GROUP BY historical_servers.id ORDER BY total_kills DESC, historical_servers.server_number ASC, historical_servers.slug ASC """, (player_row["id"],), ).fetchall() return { "player": { "stable_player_key": player_row["stable_player_key"], "name": player_row["display_name"], "steam_id": player_row["steam_id"], "source_player_id": player_row["source_player_id"], }, "matches_count": int(player_row["matches_count"] or 0), "total_kills": int(player_row["total_kills"] or 0), "time_range": { "start": player_row["first_match_at"], "end": player_row["last_match_at"], }, "servers": [ { "server": { "slug": row["server_slug"], "name": row["server_name"], }, "matches_count": int(row["matches_count"] or 0), "total_kills": int(row["total_kills"] or 0), "time_range": { "start": row["first_match_at"], "end": row["last_match_at"], }, } for row in server_rows ], } def list_weekly_leaderboard( *, limit: int = 10, server_id: str | None = None, metric: str = "kills", db_path: Path | None = None, ) -> dict[str, object]: """Return ranked weekly leaderboard totals from persisted historical match stats.""" if use_postgres_rcon_storage(explicit_sqlite_path=db_path): from .postgres_display_storage import list_scoreboard_leaderboard return list_scoreboard_leaderboard( timeframe="weekly", metric=metric, server_id=server_id, limit=limit, ) resolved_path = initialize_historical_storage(db_path=db_path) aggregate_all_servers = _is_all_servers_selector(server_id) current_time = datetime.now(timezone.utc) current_week_start = _start_of_week(current_time) previous_week_start = current_week_start - timedelta(days=DEFAULT_WEEKLY_WINDOW_DAYS) normalized_metric = metric.strip() if isinstance(metric, str) else "" if normalized_metric not in SUPPORTED_WEEKLY_LEADERBOARD_METRICS: raise ValueError(f"Unsupported weekly leaderboard metric: {metric}") weekly_window = _select_weekly_window( server_id=server_id, current_time=current_time, current_week_start=current_week_start, previous_week_start=previous_week_start, db_path=resolved_path, ) window_start = weekly_window["window_start"] window_end = weekly_window["window_end"] where_clauses = [ "historical_matches.ended_at IS NOT NULL", "historical_matches.ended_at >= ?", "historical_matches.ended_at < ?", ] params: list[object] = [ window_start.isoformat().replace("+00:00", "Z"), window_end.isoformat().replace("+00:00", "Z"), ] if server_id and not aggregate_all_servers: normalized_server_id = server_id.strip() where_clauses.append( "(historical_servers.slug = ? OR CAST(historical_servers.server_number AS TEXT) = ?)" ) params.extend([normalized_server_id, normalized_server_id]) server_slug_expression = ( f"'{ALL_SERVERS_SLUG}'" if aggregate_all_servers else "historical_servers.slug" ) server_name_expression = ( f"'{ALL_SERVERS_DISPLAY_NAME}'" if aggregate_all_servers else "historical_servers.display_name" ) partition_expression = ( f"'{ALL_SERVERS_SLUG}'" if aggregate_all_servers else "historical_servers.slug" ) group_by_expression = ( "historical_players.id" if aggregate_all_servers else "historical_servers.slug, historical_players.id" ) metric_sum_expression = { "kills": "COALESCE(SUM(historical_player_match_stats.kills), 0)", "deaths": "COALESCE(SUM(historical_player_match_stats.deaths), 0)", "support": "COALESCE(SUM(historical_player_match_stats.support), 0)", "matches_over_100_kills": ( "COALESCE(SUM(CASE WHEN COALESCE(historical_player_match_stats.kills, 0) >= 100 " "THEN 1 ELSE 0 END), 0)" ), }[normalized_metric] with _connect(resolved_path) as connection: rows = connection.execute( f""" WITH ranked_players AS ( SELECT {server_slug_expression} AS server_slug, {server_name_expression} AS server_name, historical_players.stable_player_key, historical_players.display_name AS player_name, historical_players.steam_id, COUNT(DISTINCT historical_matches.id) AS matches_count, {metric_sum_expression} AS metric_value, ROW_NUMBER() OVER ( PARTITION BY {partition_expression} ORDER BY {metric_sum_expression} DESC, COUNT(DISTINCT historical_matches.id) ASC, historical_players.display_name ASC ) AS ranking_position FROM historical_player_match_stats INNER JOIN historical_matches ON historical_matches.id = historical_player_match_stats.historical_match_id INNER JOIN historical_servers ON historical_servers.id = historical_matches.historical_server_id INNER JOIN historical_players ON historical_players.id = historical_player_match_stats.historical_player_id WHERE {" AND ".join(where_clauses)} GROUP BY {group_by_expression} ) SELECT * FROM ranked_players WHERE ranking_position <= ? ORDER BY server_slug ASC, ranking_position ASC """, [*params, limit], ).fetchall() items: list[dict[str, object]] = [] for row in rows: items.append( { "server": { "slug": row["server_slug"], "name": row["server_name"], }, "time_range": { "start": window_start.isoformat().replace("+00:00", "Z"), "end": window_end.isoformat().replace("+00:00", "Z"), "window_days": DEFAULT_WEEKLY_WINDOW_DAYS, }, "player": { "stable_player_key": row["stable_player_key"], "name": row["player_name"], "steam_id": row["steam_id"], }, "metric": normalized_metric, "ranking_position": int(row["ranking_position"]), "metric_value": int(row["metric_value"] or 0), "matches_considered": int(row["matches_count"] or 0), } ) return { "metric": normalized_metric, "window_start": window_start.isoformat().replace("+00:00", "Z"), "window_end": window_end.isoformat().replace("+00:00", "Z"), "window_days": DEFAULT_WEEKLY_WINDOW_DAYS, "window_kind": weekly_window["window_kind"], "window_label": weekly_window["window_label"], "uses_fallback": weekly_window["uses_fallback"], "selection_reason": weekly_window["selection_reason"], "current_week_start": current_week_start.isoformat().replace("+00:00", "Z"), "current_week_closed_matches": weekly_window["current_week_closed_matches"], "previous_week_closed_matches": weekly_window["previous_week_closed_matches"], "sufficient_sample": { "minimum_closed_matches": weekly_window["minimum_closed_matches"], "current_week_closed_matches": weekly_window["current_week_closed_matches"], "current_week_has_sufficient_sample": weekly_window["current_week_has_sufficient_sample"], "is_early_week": weekly_window["is_early_week"], "fallback_max_weekday": weekly_window["fallback_max_weekday"], }, "items": items, } def list_weekly_top_kills( *, limit: int = 10, server_id: str | None = None, db_path: Path | None = None, ) -> dict[str, object]: """Return ranked weekly kill totals from persisted historical match stats.""" result = list_weekly_leaderboard( limit=limit, server_id=server_id, metric="kills", db_path=db_path, ) items = [] for item in result["items"]: legacy_item = dict(item) legacy_item["weekly_kills"] = legacy_item["metric_value"] items.append(legacy_item) return { "metric": "kills", "window_start": result["window_start"], "window_end": result["window_end"], "items": items, } def list_monthly_leaderboard( *, limit: int = 10, server_id: str | None = None, metric: str = "kills", db_path: Path | None = None, ) -> dict[str, object]: """Return ranked monthly leaderboard totals from persisted historical match stats.""" if use_postgres_rcon_storage(explicit_sqlite_path=db_path): from .postgres_display_storage import list_scoreboard_leaderboard return list_scoreboard_leaderboard( timeframe="monthly", metric=metric, server_id=server_id, limit=limit, ) resolved_path = initialize_historical_storage(db_path=db_path) aggregate_all_servers = _is_all_servers_selector(server_id) current_time = datetime.now(timezone.utc) current_month_start = _start_of_month(current_time) previous_month_start = _start_of_previous_month(current_month_start) normalized_metric = metric.strip() if isinstance(metric, str) else "" if normalized_metric not in SUPPORTED_MONTHLY_LEADERBOARD_METRICS: raise ValueError(f"Unsupported monthly leaderboard metric: {metric}") monthly_window = _select_monthly_window( server_id=server_id, current_time=current_time, current_month_start=current_month_start, previous_month_start=previous_month_start, db_path=resolved_path, ) window_start = monthly_window["window_start"] window_end = monthly_window["window_end"] where_clauses = [ "historical_matches.ended_at IS NOT NULL", "historical_matches.ended_at >= ?", "historical_matches.ended_at < ?", ] params: list[object] = [ window_start.isoformat().replace("+00:00", "Z"), window_end.isoformat().replace("+00:00", "Z"), ] if server_id and not aggregate_all_servers: normalized_server_id = server_id.strip() where_clauses.append( "(historical_servers.slug = ? OR CAST(historical_servers.server_number AS TEXT) = ?)" ) params.extend([normalized_server_id, normalized_server_id]) server_slug_expression = ( f"'{ALL_SERVERS_SLUG}'" if aggregate_all_servers else "historical_servers.slug" ) server_name_expression = ( f"'{ALL_SERVERS_DISPLAY_NAME}'" if aggregate_all_servers else "historical_servers.display_name" ) partition_expression = ( f"'{ALL_SERVERS_SLUG}'" if aggregate_all_servers else "historical_servers.slug" ) group_by_expression = ( "historical_players.id" if aggregate_all_servers else "historical_servers.slug, historical_players.id" ) metric_sum_expression = { "kills": "COALESCE(SUM(historical_player_match_stats.kills), 0)", "deaths": "COALESCE(SUM(historical_player_match_stats.deaths), 0)", "support": "COALESCE(SUM(historical_player_match_stats.support), 0)", "matches_over_100_kills": ( "COALESCE(SUM(CASE WHEN COALESCE(historical_player_match_stats.kills, 0) >= 100 " "THEN 1 ELSE 0 END), 0)" ), }[normalized_metric] with _connect(resolved_path) as connection: rows = connection.execute( f""" WITH ranked_players AS ( SELECT {server_slug_expression} AS server_slug, {server_name_expression} AS server_name, historical_players.stable_player_key, historical_players.display_name AS player_name, historical_players.steam_id, COUNT(DISTINCT historical_matches.id) AS matches_count, {metric_sum_expression} AS metric_value, ROW_NUMBER() OVER ( PARTITION BY {partition_expression} ORDER BY {metric_sum_expression} DESC, COUNT(DISTINCT historical_matches.id) ASC, historical_players.display_name ASC ) AS ranking_position FROM historical_player_match_stats INNER JOIN historical_matches ON historical_matches.id = historical_player_match_stats.historical_match_id INNER JOIN historical_servers ON historical_servers.id = historical_matches.historical_server_id INNER JOIN historical_players ON historical_players.id = historical_player_match_stats.historical_player_id WHERE {" AND ".join(where_clauses)} GROUP BY {group_by_expression} ) SELECT * FROM ranked_players WHERE ranking_position <= ? ORDER BY server_slug ASC, ranking_position ASC """, [*params, limit], ).fetchall() window_days = _calculate_window_days(window_start=window_start, window_end=window_end) items: list[dict[str, object]] = [] for row in rows: items.append( { "server": { "slug": row["server_slug"], "name": row["server_name"], }, "time_range": { "start": window_start.isoformat().replace("+00:00", "Z"), "end": window_end.isoformat().replace("+00:00", "Z"), "window_days": window_days, }, "player": { "stable_player_key": row["stable_player_key"], "name": row["player_name"], "steam_id": row["steam_id"], }, "metric": normalized_metric, "ranking_position": int(row["ranking_position"]), "metric_value": int(row["metric_value"] or 0), "matches_considered": int(row["matches_count"] or 0), } ) return { "timeframe": "monthly", "metric": normalized_metric, "window_start": window_start.isoformat().replace("+00:00", "Z"), "window_end": window_end.isoformat().replace("+00:00", "Z"), "window_days": window_days, "window_kind": monthly_window["window_kind"], "window_label": monthly_window["window_label"], "uses_fallback": monthly_window["uses_fallback"], "selection_reason": monthly_window["selection_reason"], "current_month_start": current_month_start.isoformat().replace("+00:00", "Z"), "current_month_closed_matches": monthly_window["current_month_closed_matches"], "previous_month_closed_matches": monthly_window["previous_month_closed_matches"], "sufficient_sample": { "minimum_closed_matches": monthly_window["minimum_closed_matches"], "current_month_closed_matches": monthly_window["current_month_closed_matches"], "current_month_has_sufficient_sample": monthly_window["current_month_has_sufficient_sample"], "is_early_month": monthly_window["is_early_month"], }, "items": items, } def list_monthly_mvp_ranking( *, limit: int = 10, server_id: str | None = None, db_path: Path | None = None, ) -> dict[str, object]: """Return the monthly MVP V1 ranking built from persisted historical totals.""" resolved_path = initialize_historical_storage(db_path=db_path) aggregate_all_servers = _is_all_servers_selector(server_id) current_time = datetime.now(timezone.utc) current_month_start = _start_of_month(current_time) previous_month_start = _start_of_previous_month(current_month_start) monthly_window = _select_monthly_window( server_id=server_id, current_time=current_time, current_month_start=current_month_start, previous_month_start=previous_month_start, db_path=resolved_path, ) window_start = monthly_window["window_start"] window_end = monthly_window["window_end"] where_clauses = [ "historical_matches.ended_at IS NOT NULL", "historical_matches.ended_at >= ?", "historical_matches.ended_at < ?", ] params: list[object] = [ window_start.isoformat().replace("+00:00", "Z"), window_end.isoformat().replace("+00:00", "Z"), ] if server_id and not aggregate_all_servers: normalized_server_id = server_id.strip() where_clauses.append( "(historical_servers.slug = ? OR CAST(historical_servers.server_number AS TEXT) = ?)" ) params.extend([normalized_server_id, normalized_server_id]) server_slug_expression = ( f"'{ALL_SERVERS_SLUG}'" if aggregate_all_servers else "historical_servers.slug" ) server_name_expression = ( f"'{ALL_SERVERS_DISPLAY_NAME}'" if aggregate_all_servers else "historical_servers.display_name" ) group_by_expression = ( "historical_players.id" if aggregate_all_servers else "historical_servers.slug, historical_players.id" ) with _connect(resolved_path) as connection: rows = connection.execute( f""" SELECT {server_slug_expression} AS server_slug, {server_name_expression} AS server_name, historical_players.stable_player_key, historical_players.display_name AS player_name, historical_players.steam_id, COUNT(DISTINCT historical_matches.id) AS matches_count, COALESCE(SUM(historical_player_match_stats.kills), 0) AS total_kills, COALESCE(SUM(historical_player_match_stats.deaths), 0) AS total_deaths, COALESCE(SUM(historical_player_match_stats.support), 0) AS total_support, COALESCE(SUM(historical_player_match_stats.teamkills), 0) AS total_teamkills, COALESCE(SUM(historical_player_match_stats.time_seconds), 0) AS total_time_seconds FROM historical_player_match_stats INNER JOIN historical_matches ON historical_matches.id = historical_player_match_stats.historical_match_id INNER JOIN historical_servers ON historical_servers.id = historical_matches.historical_server_id INNER JOIN historical_players ON historical_players.id = historical_player_match_stats.historical_player_id WHERE {" AND ".join(where_clauses)} GROUP BY {group_by_expression} """, params, ).fetchall() ranking_result = build_monthly_mvp_rankings( [dict(row) for row in rows], limit=limit, ) window_days = _calculate_window_days(window_start=window_start, window_end=window_end) for item in ranking_result["items"]: item["time_range"] = { "start": window_start.isoformat().replace("+00:00", "Z"), "end": window_end.isoformat().replace("+00:00", "Z"), "window_days": window_days, } return { "timeframe": "monthly", "metric": "mvp", "ranking_version": ranking_result["ranking_version"], "window_start": window_start.isoformat().replace("+00:00", "Z"), "window_end": window_end.isoformat().replace("+00:00", "Z"), "window_days": window_days, "window_kind": monthly_window["window_kind"], "window_label": monthly_window["window_label"], "uses_fallback": monthly_window["uses_fallback"], "selection_reason": monthly_window["selection_reason"], "current_month_start": current_month_start.isoformat().replace("+00:00", "Z"), "current_month_closed_matches": monthly_window["current_month_closed_matches"], "previous_month_closed_matches": monthly_window["previous_month_closed_matches"], "sufficient_sample": { "minimum_closed_matches": monthly_window["minimum_closed_matches"], "current_month_closed_matches": monthly_window["current_month_closed_matches"], "current_month_has_sufficient_sample": monthly_window["current_month_has_sufficient_sample"], "is_early_month": monthly_window["is_early_month"], }, "eligibility": ranking_result["eligibility"], "eligible_players_count": ranking_result["eligible_players_count"], "items": ranking_result["items"], } def list_monthly_mvp_v2_ranking( *, limit: int = 10, server_id: str | None = None, db_path: Path | None = None, ) -> dict[str, object]: """Return the monthly MVP V2 ranking built from monthly totals plus V2 signals.""" resolved_path = initialize_historical_storage(db_path=db_path) aggregate_all_servers = _is_all_servers_selector(server_id) current_time = datetime.now(timezone.utc) current_month_start = _start_of_month(current_time) previous_month_start = _start_of_previous_month(current_month_start) monthly_window = _select_monthly_window( server_id=server_id, current_time=current_time, current_month_start=current_month_start, previous_month_start=previous_month_start, db_path=resolved_path, ) window_start = monthly_window["window_start"] window_end = monthly_window["window_end"] month_key = window_start.strftime("%Y-%m") event_coverage = _get_monthly_player_event_coverage( server_id=server_id, month_key=month_key, db_path=resolved_path, ) window_days = _calculate_window_days(window_start=window_start, window_end=window_end) empty_result = { "timeframe": "monthly", "metric": "mvp-v2", "ranking_version": "v2", "window_start": window_start.isoformat().replace("+00:00", "Z"), "window_end": window_end.isoformat().replace("+00:00", "Z"), "window_days": window_days, "window_kind": monthly_window["window_kind"], "window_label": monthly_window["window_label"], "uses_fallback": monthly_window["uses_fallback"], "selection_reason": monthly_window["selection_reason"], "current_month_start": current_month_start.isoformat().replace("+00:00", "Z"), "current_month_closed_matches": monthly_window["current_month_closed_matches"], "previous_month_closed_matches": monthly_window["previous_month_closed_matches"], "sufficient_sample": { "minimum_closed_matches": monthly_window["minimum_closed_matches"], "current_month_closed_matches": monthly_window["current_month_closed_matches"], "current_month_has_sufficient_sample": monthly_window["current_month_has_sufficient_sample"], "is_early_month": monthly_window["is_early_month"], }, "event_coverage": event_coverage, } if not bool(event_coverage["ready"]): return { **empty_result, "eligibility": None, "eligible_players_count": 0, "items": [], } where_clauses = [ "historical_matches.ended_at IS NOT NULL", "historical_matches.ended_at >= ?", "historical_matches.ended_at < ?", ] params: list[object] = [ window_start.isoformat().replace("+00:00", "Z"), window_end.isoformat().replace("+00:00", "Z"), ] if server_id and not aggregate_all_servers: normalized_server_id = server_id.strip() where_clauses.append( "(historical_servers.slug = ? OR CAST(historical_servers.server_number AS TEXT) = ?)" ) params.extend([normalized_server_id, normalized_server_id]) event_scope_sql, event_scope_params = _build_player_event_scope_sql(server_id) server_slug_expression = ( f"'{ALL_SERVERS_SLUG}'" if aggregate_all_servers else "historical_servers.slug" ) server_name_expression = ( f"'{ALL_SERVERS_DISPLAY_NAME}'" if aggregate_all_servers else "historical_servers.display_name" ) group_by_expression = ( "historical_players.id" if aggregate_all_servers else "historical_servers.slug, historical_players.id" ) with _connect(resolved_path) as connection: rows = connection.execute( f""" WITH most_killed_pairs AS ( SELECT killer_player_key AS stable_player_key, victim_player_key, COALESCE(SUM(event_value), 0) AS total_kills FROM player_event_raw_ledger WHERE event_type = 'player_kill_summary' AND occurred_at IS NOT NULL AND substr(CAST(occurred_at AS TEXT), 1, 7) = ? AND {event_scope_sql} AND killer_player_key IS NOT NULL AND victim_player_key IS NOT NULL GROUP BY killer_player_key, victim_player_key ), most_killed_by_player AS ( SELECT stable_player_key, MAX(total_kills) AS most_killed_count FROM most_killed_pairs GROUP BY stable_player_key ), death_by_pairs AS ( SELECT victim_player_key AS stable_player_key, killer_player_key, COALESCE(SUM(event_value), 0) AS total_kills FROM player_event_raw_ledger WHERE event_type = 'player_death_summary' AND occurred_at IS NOT NULL AND substr(CAST(occurred_at AS TEXT), 1, 7) = ? AND {event_scope_sql} AND killer_player_key IS NOT NULL AND victim_player_key IS NOT NULL GROUP BY victim_player_key, killer_player_key ), death_by_player AS ( SELECT stable_player_key, MAX(total_kills) AS death_by_count FROM death_by_pairs GROUP BY stable_player_key ), duel_pairs AS ( SELECT CASE WHEN COALESCE(killer_player_key, '') <= COALESCE(victim_player_key, '') THEN killer_player_key ELSE victim_player_key END AS player_a_key, CASE WHEN COALESCE(killer_player_key, '') <= COALESCE(victim_player_key, '') THEN victim_player_key ELSE killer_player_key END AS player_b_key, COALESCE( SUM( CASE WHEN COALESCE(killer_player_key, '') <= COALESCE(victim_player_key, '') THEN event_value ELSE -event_value END ), 0 ) AS net_duel_value FROM player_event_raw_ledger WHERE event_type = 'player_kill_summary' AND occurred_at IS NOT NULL AND substr(CAST(occurred_at AS TEXT), 1, 7) = ? AND {event_scope_sql} AND killer_player_key IS NOT NULL AND victim_player_key IS NOT NULL GROUP BY CASE WHEN COALESCE(killer_player_key, '') <= COALESCE(victim_player_key, '') THEN killer_player_key ELSE victim_player_key END, CASE WHEN COALESCE(killer_player_key, '') <= COALESCE(victim_player_key, '') THEN victim_player_key ELSE killer_player_key END ), duel_player_values AS ( SELECT player_a_key AS stable_player_key, CASE WHEN net_duel_value > 0 THEN net_duel_value ELSE 0 END AS positive_duel_value FROM duel_pairs UNION ALL SELECT player_b_key AS stable_player_key, CASE WHEN net_duel_value < 0 THEN -net_duel_value ELSE 0 END AS positive_duel_value FROM duel_pairs ), ranked_duel_values AS ( SELECT stable_player_key, positive_duel_value, ROW_NUMBER() OVER ( PARTITION BY stable_player_key ORDER BY positive_duel_value DESC ) AS duel_rank FROM duel_player_values WHERE stable_player_key IS NOT NULL AND positive_duel_value > 0 ), duel_control_by_player AS ( SELECT stable_player_key, COALESCE(SUM(positive_duel_value), 0) AS duel_control_raw FROM ranked_duel_values WHERE duel_rank <= 3 GROUP BY stable_player_key ) SELECT {server_slug_expression} AS server_slug, {server_name_expression} AS server_name, historical_players.stable_player_key, historical_players.display_name AS player_name, historical_players.steam_id, COUNT(DISTINCT historical_matches.id) AS matches_count, COALESCE(SUM(historical_player_match_stats.kills), 0) AS total_kills, COALESCE(SUM(historical_player_match_stats.deaths), 0) AS total_deaths, COALESCE(SUM(historical_player_match_stats.support), 0) AS total_support, COALESCE(SUM(historical_player_match_stats.teamkills), 0) AS total_teamkills, COALESCE(SUM(historical_player_match_stats.time_seconds), 0) AS total_time_seconds, COALESCE(MAX(most_killed_by_player.most_killed_count), 0) AS most_killed_count, COALESCE(MAX(death_by_player.death_by_count), 0) AS death_by_count, COALESCE(MAX(duel_control_by_player.duel_control_raw), 0) AS duel_control_raw FROM historical_player_match_stats INNER JOIN historical_matches ON historical_matches.id = historical_player_match_stats.historical_match_id INNER JOIN historical_servers ON historical_servers.id = historical_matches.historical_server_id INNER JOIN historical_players ON historical_players.id = historical_player_match_stats.historical_player_id LEFT JOIN most_killed_by_player ON most_killed_by_player.stable_player_key = historical_players.stable_player_key LEFT JOIN death_by_player ON death_by_player.stable_player_key = historical_players.stable_player_key LEFT JOIN duel_control_by_player ON duel_control_by_player.stable_player_key = historical_players.stable_player_key WHERE {" AND ".join(where_clauses)} GROUP BY {group_by_expression} """, [ month_key, *event_scope_params, month_key, *event_scope_params, month_key, *event_scope_params, *params, ], ).fetchall() ranking_result = build_monthly_mvp_v2_rankings( [dict(row) for row in rows], limit=limit, ) for item in ranking_result["items"]: item["time_range"] = { "start": window_start.isoformat().replace("+00:00", "Z"), "end": window_end.isoformat().replace("+00:00", "Z"), "window_days": window_days, } return { **empty_result, "ranking_version": ranking_result["ranking_version"], "eligibility": ranking_result["eligibility"], "eligible_players_count": ranking_result["eligible_players_count"], "items": ranking_result["items"], } def _get_monthly_player_event_coverage( *, server_id: str | None, month_key: str, db_path: Path, ) -> dict[str, object]: scope_sql, scope_params = _build_player_event_scope_sql(server_id) with _connect(db_path) as connection: latest_row = connection.execute( f""" SELECT MAX(substr(CAST(occurred_at AS TEXT), 1, 7)) AS latest_month_key FROM player_event_raw_ledger WHERE occurred_at IS NOT NULL AND {scope_sql} """, scope_params, ).fetchone() month_row = connection.execute( f""" SELECT COUNT(*) AS event_count, MIN(occurred_at) AS source_range_start, MAX(occurred_at) AS source_range_end FROM player_event_raw_ledger WHERE occurred_at IS NOT NULL AND substr(CAST(occurred_at AS TEXT), 1, 7) = ? AND {scope_sql} """, [month_key, *scope_params], ).fetchone() latest_month_key = str(latest_row["latest_month_key"]) if latest_row and latest_row["latest_month_key"] else None event_count = int(month_row["event_count"] or 0) if month_row else 0 return { "month_key": month_key, "latest_month_key": latest_month_key, "ready": bool(event_count > 0 and latest_month_key == month_key), "event_count": event_count, "source_range_start": month_row["source_range_start"] if month_row else None, "source_range_end": month_row["source_range_end"] if month_row else None, "selection_reason": ( "month-key-aligned" if event_count > 0 and latest_month_key == month_key else "player-event-month-mismatch-or-missing" ), } def _build_player_event_scope_sql(server_id: str | None) -> tuple[str, list[object]]: if not server_id or _is_all_servers_selector(server_id): return "1 = 1", [] normalized_server_id = server_id.strip() return "server_slug = ?", [normalized_server_id] def _connect(db_path: Path) -> sqlite3.Connection: return connect_sqlite_writer(db_path) def _resolve_match_winner(allied_score: object, axis_score: object) -> str | None: allied = _coerce_int(allied_score) axis = _coerce_int(axis_score) if allied is None or axis is None: return None if allied > axis: return "allies" if axis > allied: return "axis" return "draw" def _has_legacy_historical_schema(connection: sqlite3.Connection) -> bool: columns = { str(row["name"]) for row in connection.execute("PRAGMA table_info(historical_matches)").fetchall() } return bool(columns) and "historical_server_id" not in columns def _rename_legacy_historical_tables(connection: sqlite3.Connection) -> None: rename_plan = ( ("historical_player_match_stats", "historical_player_match_stats_legacy"), ("historical_players", "historical_players_legacy"), ("historical_matches", "historical_matches_legacy"), ) for current_name, legacy_name in rename_plan: table_exists = connection.execute( """ SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? """, (current_name,), ).fetchone() if not table_exists: continue legacy_exists = connection.execute( """ SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? """, (legacy_name,), ).fetchone() if legacy_exists: continue connection.execute(f"ALTER TABLE {current_name} RENAME TO {legacy_name}") def _migrate_legacy_historical_data(connection: sqlite3.Connection) -> None: matches_table = connection.execute( """ SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'historical_matches_legacy' """ ).fetchone() if not matches_table: return player_map: dict[int, int] = {} for row in connection.execute( """ SELECT id, source_player_ref, canonical_name, last_seen_name FROM historical_players_legacy ORDER BY id ASC """ ).fetchall(): stable_player_key = _stringify(row["source_player_ref"]) or f"legacy-player:{row['id']}" display_name = _stringify(row["last_seen_name"]) or _stringify(row["canonical_name"]) or "Unknown player" now = _utc_now_iso() connection.execute( """ INSERT INTO historical_players ( stable_player_key, display_name, steam_id, source_player_id, first_seen_at, last_seen_at ) VALUES (?, ?, NULL, NULL, ?, ?) ON CONFLICT(stable_player_key) DO UPDATE SET display_name = excluded.display_name, last_seen_at = excluded.last_seen_at, updated_at = CURRENT_TIMESTAMP """, (stable_player_key, display_name, now, now), ) new_row = connection.execute( "SELECT id FROM historical_players WHERE stable_player_key = ?", (stable_player_key,), ).fetchone() if new_row is not None: player_map[int(row["id"])] = int(new_row["id"]) match_map: dict[int, int] = {} for row in connection.execute( """ SELECT * FROM historical_matches_legacy ORDER BY id ASC """ ).fetchall(): server_slug = _stringify(row["external_server_id"]) or "comunidad-hispana-01" server_row = _resolve_historical_server(connection, server_slug) connection.execute( """ INSERT INTO historical_matches ( historical_server_id, external_match_id, historical_map_id, created_at_source, started_at, ended_at, map_name, map_pretty_name, game_mode, image_name, allied_score, axis_score, last_seen_at, raw_payload_ref ) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL, ?, ?) ON CONFLICT(historical_server_id, external_match_id) DO UPDATE SET started_at = excluded.started_at, ended_at = excluded.ended_at, map_name = excluded.map_name, map_pretty_name = excluded.map_pretty_name, game_mode = excluded.game_mode, last_seen_at = excluded.last_seen_at, raw_payload_ref = excluded.raw_payload_ref, updated_at = CURRENT_TIMESTAMP """, ( server_row["id"], _stringify(row["source_match_ref"]) or f"legacy-match:{row['id']}", _stringify(row["created_at"]), _stringify(row["started_at"]), _stringify(row["ended_at"]), _stringify(row["map_name"]), _stringify(row["map_name"]), _stringify(row["mode_name"]), _utc_now_iso(), _stringify(row["source_url"]), ), ) new_row = connection.execute( """ SELECT id FROM historical_matches WHERE historical_server_id = ? AND external_match_id = ? """, ( server_row["id"], _stringify(row["source_match_ref"]) or f"legacy-match:{row['id']}", ), ).fetchone() if new_row is not None: match_map[int(row["id"])] = int(new_row["id"]) for row in connection.execute( """ SELECT * FROM historical_player_match_stats_legacy ORDER BY id ASC """ ).fetchall(): new_match_id = match_map.get(int(row["match_id"])) new_player_id = player_map.get(int(row["player_id"])) if new_match_id is None or new_player_id is None: continue connection.execute( """ INSERT INTO historical_player_match_stats ( historical_match_id, historical_player_id, match_player_ref, team_side, level, kills, deaths, teamkills, time_seconds, kills_per_minute, deaths_per_minute, kill_death_ratio, combat, offense, defense, support ) VALUES (?, ?, NULL, NULL, NULL, ?, ?, NULL, ?, NULL, NULL, NULL, NULL, NULL, NULL, NULL) ON CONFLICT(historical_match_id, historical_player_id) DO UPDATE SET kills = excluded.kills, deaths = excluded.deaths, time_seconds = excluded.time_seconds, updated_at = CURRENT_TIMESTAMP """, ( new_match_id, new_player_id, _coerce_int(row["kills"]), _coerce_int(row["deaths"]), _coerce_int(row["time_seconds"]), ), ) def _normalize_historical_player_identities(connection: sqlite3.Connection) -> None: rows = connection.execute( """ SELECT id, stable_player_key, display_name, steam_id, source_player_id FROM historical_players ORDER BY id ASC """ ).fetchall() for row in rows: player_id = int(row["id"]) canonical_key, steam_id, source_player_id, display_name = _canonicalize_stored_player_row(row) existing = connection.execute( """ SELECT id FROM historical_players WHERE stable_player_key = ? """, (canonical_key,), ).fetchone() if existing is not None and int(existing["id"]) != player_id: _merge_historical_player_rows( connection, source_player_id=player_id, target_player_id=int(existing["id"]), display_name=display_name, steam_id=steam_id, source_ref=source_player_id, ) continue connection.execute( """ UPDATE historical_players SET stable_player_key = ?, display_name = ?, steam_id = ?, source_player_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? """, (canonical_key, display_name, steam_id, source_player_id, player_id), ) def _normalize_historical_match_identities(connection: sqlite3.Connection) -> None: rows = connection.execute( """ SELECT historical_matches.id, historical_matches.historical_server_id, historical_matches.external_match_id, historical_matches.started_at, historical_matches.ended_at, historical_matches.created_at_source, historical_matches.map_name, historical_matches.map_pretty_name, COUNT(historical_player_match_stats.id) AS player_count FROM historical_matches LEFT JOIN historical_player_match_stats ON historical_player_match_stats.historical_match_id = historical_matches.id WHERE historical_matches.started_at IS NOT NULL GROUP BY historical_matches.id ORDER BY historical_matches.historical_server_id ASC, historical_matches.started_at ASC, historical_matches.id ASC """ ).fetchall() grouped_matches: dict[tuple[int, str, str], list[sqlite3.Row]] = {} for row in rows: group_key = ( int(row["historical_server_id"]), str(row["started_at"]), _normalize_match_identity_label(row["map_pretty_name"] or row["map_name"]), ) grouped_matches.setdefault(group_key, []).append(row) for grouped_rows in grouped_matches.values(): if len(grouped_rows) < 2: continue target_row = max(grouped_rows, key=_match_identity_preference) for source_row in grouped_rows: if int(source_row["id"]) == int(target_row["id"]): continue _merge_historical_match_rows( connection, source_match_id=int(source_row["id"]), target_match_id=int(target_row["id"]), ) def _seed_default_historical_servers(connection: sqlite3.Connection) -> None: for server in DEFAULT_HISTORICAL_SERVERS: connection.execute( """ INSERT INTO historical_servers ( slug, display_name, scoreboard_base_url, server_number, source_kind ) VALUES (?, ?, ?, ?, ?) ON CONFLICT(slug) DO UPDATE SET display_name = excluded.display_name, scoreboard_base_url = excluded.scoreboard_base_url, server_number = excluded.server_number, source_kind = excluded.source_kind, updated_at = CURRENT_TIMESTAMP """, ( server.slug, server.display_name, server.scoreboard_base_url, server.server_number, server.source_kind, ), ) def _resolve_historical_server( connection: sqlite3.Connection, server_slug: str, ) -> sqlite3.Row: row = connection.execute( """ SELECT id, slug, scoreboard_base_url FROM historical_servers WHERE slug = ? """, (server_slug,), ).fetchone() if row is None: raise ValueError(f"Unknown historical server slug: {server_slug}") return row def _upsert_historical_map( connection: sqlite3.Connection, match_payload: Mapping[str, object], ) -> int | None: external_map_id = _stringify(_get_nested(match_payload, "map", "id")) if not external_map_id: return None connection.execute( """ INSERT INTO historical_maps ( external_map_id, map_name, pretty_name, game_mode, image_name ) VALUES (?, ?, ?, ?, ?) ON CONFLICT(external_map_id) DO UPDATE SET map_name = excluded.map_name, pretty_name = excluded.pretty_name, game_mode = excluded.game_mode, image_name = excluded.image_name, updated_at = CURRENT_TIMESTAMP """, ( external_map_id, _extract_map_name(match_payload), _extract_map_pretty_name(match_payload), _extract_map_game_mode(match_payload), _extract_map_image_name(match_payload), ), ) row = connection.execute( "SELECT id FROM historical_maps WHERE external_map_id = ?", (external_map_id,), ).fetchone() return int(row["id"]) if row is not None else None def _upsert_historical_player( connection: sqlite3.Connection, player_payload: Mapping[str, object], ) -> int: stable_player_key, steam_id, source_player_id = _derive_player_identity(player_payload) display_name = _normalize_player_display_name(player_payload.get("player")) or "Unknown player" seen_at = _utc_now_iso() connection.execute( """ INSERT INTO historical_players ( stable_player_key, display_name, steam_id, source_player_id, first_seen_at, last_seen_at ) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(stable_player_key) DO UPDATE SET display_name = excluded.display_name, steam_id = COALESCE(excluded.steam_id, historical_players.steam_id), source_player_id = COALESCE(excluded.source_player_id, historical_players.source_player_id), last_seen_at = excluded.last_seen_at, updated_at = CURRENT_TIMESTAMP """, ( stable_player_key, display_name, steam_id, source_player_id, seen_at, seen_at, ), ) row = connection.execute( "SELECT id FROM historical_players WHERE stable_player_key = ?", (stable_player_key,), ).fetchone() if row is None: raise RuntimeError("Failed to persist historical player identity.") return int(row["id"]) def _build_stable_player_key(player_payload: Mapping[str, object]) -> str: stable_player_key, _, _ = _derive_player_identity(player_payload) return stable_player_key def _derive_player_identity(player_payload: Mapping[str, object]) -> tuple[str, str | None, str | None]: steam_id = _stringify(_get_nested(player_payload, "steaminfo", "profile", "steamid")) source_player_id = _stringify(player_payload.get("player_id")) steaminfo_id = _stringify(_get_nested(player_payload, "steaminfo", "id")) if steam_id: return f"steam:{steam_id}", steam_id, source_player_id if _is_probable_steam_id(source_player_id): return f"steam:{source_player_id}", source_player_id, source_player_id if source_player_id: return f"crcon-player:{source_player_id}", None, source_player_id if steaminfo_id: return f"steaminfo:{steaminfo_id}", None, None player_name = _normalize_player_display_name(player_payload.get("player")) or "unknown-player" return f"name:{_normalize_name_key(player_name)}", None, None def _extract_map_name(match_payload: Mapping[str, object]) -> str | None: return _stringify(match_payload.get("map_name")) or _stringify(_get_nested(match_payload, "map", "name")) def _extract_map_pretty_name(match_payload: Mapping[str, object]) -> str | None: return _stringify(_get_nested(match_payload, "map", "pretty_name")) or _extract_map_name(match_payload) def _extract_map_game_mode(match_payload: Mapping[str, object]) -> str | None: return _stringify(_get_nested(match_payload, "map", "game_mode")) def _extract_map_image_name(match_payload: Mapping[str, object]) -> str | None: return _stringify(_get_nested(match_payload, "map", "image_name")) def _coerce_list(value: object) -> list[Mapping[str, object]]: if not isinstance(value, list): return [] return [item for item in value if isinstance(item, Mapping)] def _coerce_int(value: object) -> int | None: if value in (None, ""): return None try: return int(value) except (TypeError, ValueError): return None def _coerce_float(value: object) -> float | None: if value in (None, ""): return None try: return float(value) except (TypeError, ValueError): return None def _stringify(value: object) -> str | None: if value is None: return None text = str(value).strip() return text or None def _normalize_player_display_name(value: object) -> str | None: text = _stringify(value) if not text: return None return " ".join(text.split()) def _normalize_name_key(player_name: str) -> str: normalized_name = "".join( character.lower() if character.isalnum() else "-" for character in player_name ) compact_name = "-".join(part for part in normalized_name.split("-") if part) return compact_name or "unknown-player" def _is_probable_steam_id(value: object) -> bool: text = _stringify(value) return bool(text and text.isdigit() and len(text) >= 16) def _canonicalize_stored_player_row( row: sqlite3.Row, ) -> tuple[str, str | None, str | None, str]: stable_player_key = _stringify(row["stable_player_key"]) display_name = _normalize_player_display_name(row["display_name"]) or "Unknown player" steam_id = _stringify(row["steam_id"]) source_player_id = _stringify(row["source_player_id"]) if _is_probable_steam_id(steam_id): return f"steam:{steam_id}", steam_id, source_player_id, display_name if _is_probable_steam_id(source_player_id): return f"steam:{source_player_id}", source_player_id, source_player_id, display_name if source_player_id: return f"crcon-player:{source_player_id}", None, source_player_id, display_name if stable_player_key and stable_player_key.startswith("steaminfo:"): return stable_player_key, None, None, display_name if stable_player_key and stable_player_key.startswith("name:"): return stable_player_key, None, None, display_name if stable_player_key and stable_player_key.startswith("steam:"): return stable_player_key, steam_id, source_player_id, display_name if stable_player_key and stable_player_key.startswith("crcon-player:"): source_ref = stable_player_key.removeprefix("crcon-player:") return stable_player_key, None, source_player_id or source_ref, display_name if stable_player_key: if _is_probable_steam_id(stable_player_key): return f"steam:{stable_player_key}", stable_player_key, source_player_id, display_name return f"crcon-player:{stable_player_key}", None, source_player_id or stable_player_key, display_name return f"name:{_normalize_name_key(display_name)}", None, None, display_name def _merge_historical_player_rows( connection: sqlite3.Connection, *, source_player_id: int, target_player_id: int, display_name: str, steam_id: str | None, source_ref: str | None, ) -> None: target_row = connection.execute( """ SELECT display_name, steam_id, source_player_id, first_seen_at, last_seen_at FROM historical_players WHERE id = ? """, (target_player_id,), ).fetchone() if target_row is None: return connection.execute( """ UPDATE historical_players SET display_name = ?, steam_id = ?, source_player_id = ?, first_seen_at = MIN(first_seen_at, ?), last_seen_at = MAX(last_seen_at, ?), updated_at = CURRENT_TIMESTAMP WHERE id = ? """, ( _pick_preferred_display_name(target_row["display_name"], display_name), _pick_preferred_steam_id(target_row["steam_id"], steam_id), _pick_preferred_source_player_id(target_row["source_player_id"], source_ref), connection.execute( "SELECT first_seen_at FROM historical_players WHERE id = ?", (source_player_id,), ).fetchone()["first_seen_at"], connection.execute( "SELECT last_seen_at FROM historical_players WHERE id = ?", (source_player_id,), ).fetchone()["last_seen_at"], target_player_id, ), ) stats_rows = connection.execute( """ SELECT * FROM historical_player_match_stats WHERE historical_player_id = ? ORDER BY id ASC """, (source_player_id,), ).fetchall() for stat_row in stats_rows: existing = connection.execute( """ SELECT * FROM historical_player_match_stats WHERE historical_match_id = ? AND historical_player_id = ? """, (stat_row["historical_match_id"], target_player_id), ).fetchone() if existing is None: connection.execute( """ UPDATE historical_player_match_stats SET historical_player_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? """, (target_player_id, stat_row["id"]), ) continue _merge_player_match_stats_row(connection, existing["id"], stat_row) connection.execute( "DELETE FROM historical_player_match_stats WHERE id = ?", (stat_row["id"],), ) connection.execute( "DELETE FROM historical_players WHERE id = ?", (source_player_id,), ) def _normalize_match_identity_label(value: object) -> str: text = _stringify(value) or "unknown-map" return " ".join(text.lower().split()) def _match_identity_preference(row: sqlite3.Row) -> tuple[int, int, int, str, int]: return ( 1 if _stringify(row["ended_at"]) else 0, 1 if (_stringify(row["external_match_id"]) or "").isdigit() else 0, int(row["player_count"] or 0), _stringify(row["created_at_source"]) or "", int(row["id"]), ) def _merge_historical_match_rows( connection: sqlite3.Connection, *, source_match_id: int, target_match_id: int, ) -> None: source_row = connection.execute( "SELECT * FROM historical_matches WHERE id = ?", (source_match_id,), ).fetchone() target_row = connection.execute( "SELECT * FROM historical_matches WHERE id = ?", (target_match_id,), ).fetchone() if source_row is None or target_row is None: return connection.execute( """ UPDATE historical_matches SET historical_map_id = COALESCE(historical_map_id, ?), created_at_source = COALESCE(created_at_source, ?), started_at = COALESCE(started_at, ?), ended_at = COALESCE(ended_at, ?), map_name = COALESCE(map_name, ?), map_pretty_name = COALESCE(map_pretty_name, ?), game_mode = COALESCE(game_mode, ?), image_name = COALESCE(image_name, ?), allied_score = COALESCE(allied_score, ?), axis_score = COALESCE(axis_score, ?), raw_payload_ref = COALESCE(raw_payload_ref, ?), last_seen_at = MAX(last_seen_at, ?), updated_at = CURRENT_TIMESTAMP WHERE id = ? """, ( source_row["historical_map_id"], source_row["created_at_source"], source_row["started_at"], source_row["ended_at"], source_row["map_name"], source_row["map_pretty_name"], source_row["game_mode"], source_row["image_name"], source_row["allied_score"], source_row["axis_score"], source_row["raw_payload_ref"], source_row["last_seen_at"], target_match_id, ), ) stats_rows = connection.execute( """ SELECT * FROM historical_player_match_stats WHERE historical_match_id = ? ORDER BY id ASC """, (source_match_id,), ).fetchall() for stat_row in stats_rows: existing = connection.execute( """ SELECT * FROM historical_player_match_stats WHERE historical_match_id = ? AND historical_player_id = ? """, (target_match_id, stat_row["historical_player_id"]), ).fetchone() if existing is None: connection.execute( """ UPDATE historical_player_match_stats SET historical_match_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? """, (target_match_id, stat_row["id"]), ) continue _merge_player_match_stats_row(connection, existing["id"], stat_row) connection.execute( "DELETE FROM historical_player_match_stats WHERE id = ?", (stat_row["id"],), ) connection.execute( "DELETE FROM historical_matches WHERE id = ?", (source_match_id,), ) def _merge_player_match_stats_row( connection: sqlite3.Connection, target_stat_id: int, source_row: sqlite3.Row, ) -> None: target_row = connection.execute( "SELECT * FROM historical_player_match_stats WHERE id = ?", (target_stat_id,), ).fetchone() if target_row is None: return connection.execute( """ UPDATE historical_player_match_stats SET match_player_ref = COALESCE(match_player_ref, ?), team_side = COALESCE(team_side, ?), level = ?, kills = ?, deaths = ?, teamkills = ?, time_seconds = ?, kills_per_minute = ?, deaths_per_minute = ?, kill_death_ratio = ?, combat = ?, offense = ?, defense = ?, support = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? """, ( source_row["match_player_ref"], source_row["team_side"], _max_int_value(target_row["level"], source_row["level"]), _max_int_value(target_row["kills"], source_row["kills"]), _max_int_value(target_row["deaths"], source_row["deaths"]), _max_int_value(target_row["teamkills"], source_row["teamkills"]), _max_int_value(target_row["time_seconds"], source_row["time_seconds"]), _max_float_value(target_row["kills_per_minute"], source_row["kills_per_minute"]), _max_float_value(target_row["deaths_per_minute"], source_row["deaths_per_minute"]), _max_float_value(target_row["kill_death_ratio"], source_row["kill_death_ratio"]), _max_int_value(target_row["combat"], source_row["combat"]), _max_int_value(target_row["offense"], source_row["offense"]), _max_int_value(target_row["defense"], source_row["defense"]), _max_int_value(target_row["support"], source_row["support"]), target_stat_id, ), ) def _pick_preferred_display_name(current_value: object, incoming_value: object) -> str: current_name = _normalize_player_display_name(current_value) incoming_name = _normalize_player_display_name(incoming_value) if not current_name: return incoming_name or "Unknown player" if not incoming_name: return current_name if len(incoming_name) > len(current_name): return incoming_name return current_name def _pick_preferred_steam_id(current_value: object, incoming_value: object) -> str | None: current_id = _stringify(current_value) incoming_id = _stringify(incoming_value) if _is_probable_steam_id(current_id): return current_id if _is_probable_steam_id(incoming_id): return incoming_id return None def _pick_preferred_source_player_id(current_value: object, incoming_value: object) -> str | None: current_id = _stringify(current_value) incoming_id = _stringify(incoming_value) if current_id: return current_id return incoming_id def _max_int_value(current_value: object, incoming_value: object) -> int | None: current_number = _coerce_int(current_value) incoming_number = _coerce_int(incoming_value) if current_number is None: return incoming_number if incoming_number is None: return current_number return max(current_number, incoming_number) def _max_float_value(current_value: object, incoming_value: object) -> float | None: current_number = _coerce_float(current_value) incoming_number = _coerce_float(incoming_value) if current_number is None: return incoming_number if incoming_number is None: return current_number return max(current_number, incoming_number) def _normalize_timestamp(value: object) -> str | None: text = _stringify(value) if not text: return None try: return _parse_timestamp(text).astimezone(timezone.utc).isoformat().replace("+00:00", "Z") except ValueError: return text def _parse_timestamp(value: str) -> datetime: normalized = value.strip().replace("Z", "+00:00") parsed = datetime.fromisoformat(normalized) if parsed.tzinfo is None: parsed = parsed.replace(tzinfo=timezone.utc) return parsed def _calculate_coverage_days( first_match_at: str | None, last_match_at: str | None, ) -> float | None: if not first_match_at or not last_match_at: return None try: delta = _parse_timestamp(last_match_at) - _parse_timestamp(first_match_at) except ValueError: return None return round(delta.total_seconds() / 86400, 2) def _select_weekly_window( *, server_id: str | None, current_time: datetime, current_week_start: datetime, previous_week_start: datetime, db_path: Path, ) -> dict[str, object]: fallback_max_weekday = get_historical_weekly_fallback_max_weekday() current_week_closed_matches = _count_valid_matches_with_stats_in_window( server_id=server_id, window_start=current_week_start, window_end=current_time, db_path=db_path, ) previous_week_closed_matches = _count_valid_matches_with_stats_in_window( server_id=server_id, window_start=previous_week_start, window_end=current_week_start, db_path=db_path, ) is_early_week = current_time.weekday() <= fallback_max_weekday min_matches = 1 current_week_has_sufficient_sample = current_week_closed_matches >= min_matches uses_fallback = ( not current_week_has_sufficient_sample and previous_week_closed_matches > 0 ) if uses_fallback: return { "window_start": previous_week_start, "window_end": current_week_start, "window_kind": "previous-closed-week-fallback", "window_label": "Semana cerrada anterior", "uses_fallback": True, "selection_reason": "insufficient-current-week-sample", "minimum_closed_matches": min_matches, "current_week_closed_matches": current_week_closed_matches, "previous_week_closed_matches": previous_week_closed_matches, "current_week_has_sufficient_sample": False, "is_early_week": is_early_week, "fallback_max_weekday": fallback_max_weekday, } return { "window_start": current_week_start, "window_end": current_time, "window_kind": "current-week", "window_label": "Semana actual", "uses_fallback": False, "selection_reason": "current-week", "minimum_closed_matches": min_matches, "current_week_closed_matches": current_week_closed_matches, "previous_week_closed_matches": previous_week_closed_matches, "current_week_has_sufficient_sample": current_week_has_sufficient_sample, "is_early_week": is_early_week, "fallback_max_weekday": fallback_max_weekday, } def _select_monthly_window( *, server_id: str | None, current_time: datetime, current_month_start: datetime, previous_month_start: datetime, db_path: Path, ) -> dict[str, object]: current_month_closed_matches = _count_closed_matches_in_window( server_id=server_id, window_start=current_month_start, window_end=current_time, db_path=db_path, ) previous_month_closed_matches = _count_closed_matches_in_window( server_id=server_id, window_start=previous_month_start, window_end=current_month_start, db_path=db_path, ) is_early_month = current_time.day <= 3 uses_fallback = current_month_closed_matches <= 0 and previous_month_closed_matches > 0 if uses_fallback: return { "window_start": previous_month_start, "window_end": current_month_start, "window_kind": "previous-closed-month-fallback", "window_label": "Mes cerrado anterior", "uses_fallback": True, "selection_reason": "no-current-month-matches", "minimum_closed_matches": 1, "current_month_closed_matches": current_month_closed_matches, "previous_month_closed_matches": previous_month_closed_matches, "current_month_has_sufficient_sample": False, "is_early_month": is_early_month, } return { "window_start": current_month_start, "window_end": current_time, "window_kind": "current-month", "window_label": "Mes actual", "uses_fallback": False, "selection_reason": "current-month", "minimum_closed_matches": 1, "current_month_closed_matches": current_month_closed_matches, "previous_month_closed_matches": previous_month_closed_matches, "current_month_has_sufficient_sample": current_month_closed_matches > 0, "is_early_month": is_early_month, } def _count_closed_matches_in_window( *, server_id: str | None, window_start: datetime, window_end: datetime, db_path: Path, ) -> int: where_clauses = [ "historical_matches.ended_at IS NOT NULL", "historical_matches.ended_at >= ?", "historical_matches.ended_at < ?", ] params: list[object] = [ window_start.isoformat().replace("+00:00", "Z"), window_end.isoformat().replace("+00:00", "Z"), ] if server_id and not _is_all_servers_selector(server_id): normalized_server_id = server_id.strip() where_clauses.append( "(historical_servers.slug = ? OR CAST(historical_servers.server_number AS TEXT) = ?)" ) params.extend([normalized_server_id, normalized_server_id]) with _connect(db_path) as connection: row = connection.execute( f""" SELECT COUNT(DISTINCT historical_matches.id) AS matches_count FROM historical_matches INNER JOIN historical_servers ON historical_servers.id = historical_matches.historical_server_id WHERE {" AND ".join(where_clauses)} """, params, ).fetchone() return int(row["matches_count"] or 0) if row is not None else 0 def _count_valid_matches_with_stats_in_window( *, server_id: str | None, window_start: datetime, window_end: datetime, db_path: Path, ) -> int: where_clauses = [ "historical_matches.ended_at IS NOT NULL", "historical_matches.ended_at >= ?", "historical_matches.ended_at < ?", "(" "COALESCE(historical_player_match_stats.kills, 0) > 0 " "OR COALESCE(historical_player_match_stats.deaths, 0) > 0 " "OR COALESCE(historical_player_match_stats.support, 0) > 0 " "OR COALESCE(historical_player_match_stats.combat, 0) > 0 " "OR COALESCE(historical_player_match_stats.offense, 0) > 0 " "OR COALESCE(historical_player_match_stats.defense, 0) > 0 " "OR COALESCE(historical_player_match_stats.time_seconds, 0) > 0" ")", ] params: list[object] = [ window_start.isoformat().replace("+00:00", "Z"), window_end.isoformat().replace("+00:00", "Z"), ] if server_id and not _is_all_servers_selector(server_id): normalized_server_id = server_id.strip() where_clauses.append( "(historical_servers.slug = ? OR CAST(historical_servers.server_number AS TEXT) = ?)" ) params.extend([normalized_server_id, normalized_server_id]) with _connect(db_path) as connection: row = connection.execute( f""" SELECT COUNT(DISTINCT historical_matches.id) AS matches_count FROM historical_matches INNER JOIN historical_servers ON historical_servers.id = historical_matches.historical_server_id INNER JOIN historical_player_match_stats ON historical_player_match_stats.historical_match_id = historical_matches.id WHERE {" AND ".join(where_clauses)} """, params, ).fetchone() return int(row["matches_count"] or 0) if row is not None else 0 def _classify_coverage_status( matches_count: int, coverage_days: float | None, ) -> str: if matches_count <= 0: return "empty" if coverage_days is None: return "range-unknown" if coverage_days < DEFAULT_WEEKLY_WINDOW_DAYS: return "under-week" return "week-plus" def _build_all_servers_summary(*, db_path: Path) -> dict[str, object]: per_server_items = list_historical_server_summaries(db_path=db_path) imported_matches_count = sum(int(item.get("matches_count") or 0) for item in per_server_items) unique_players = _count_all_servers_unique_players(db_path=db_path) total_kills = sum(int(item.get("total_kills") or 0) for item in per_server_items) discovered_total_matches = sum( _coerce_int(item.get("backfill", {}).get("discovered_total_matches")) or 0 for item in per_server_items ) first_points = [ item.get("coverage", {}).get("first_match_at") for item in per_server_items if item.get("coverage", {}).get("first_match_at") ] last_points = [ item.get("coverage", {}).get("last_match_at") for item in per_server_items if item.get("coverage", {}).get("last_match_at") ] first_match_at = min(first_points) if first_points else None last_match_at = max(last_points) if last_points else None coverage_days = _calculate_coverage_days(first_match_at, last_match_at) return { "server": { "slug": ALL_SERVERS_SLUG, "name": ALL_SERVERS_DISPLAY_NAME, }, "matches_count": imported_matches_count, "imported_matches_count": imported_matches_count, "unique_players": unique_players, "total_kills": total_kills, "map_count": _count_all_servers_maps(db_path=db_path), "top_maps": _list_all_servers_top_maps(db_path=db_path, limit=3), "coverage": { "basis": "persisted-import-aggregate", "status": _classify_coverage_status(imported_matches_count, coverage_days), "imported_matches_count": imported_matches_count, "discovered_total_matches": discovered_total_matches or None, "first_match_at": first_match_at, "last_match_at": last_match_at, "coverage_days": coverage_days, }, "backfill": { "mode": "aggregate", "server_count": len(per_server_items), "discovered_total_matches": discovered_total_matches or None, "remaining_matches_estimate": ( max(discovered_total_matches - imported_matches_count, 0) if discovered_total_matches else None ), "archive_exhausted": all( bool(item.get("backfill", {}).get("archive_exhausted")) for item in per_server_items ), "last_run": None, }, "time_range": { "start": first_match_at, "end": last_match_at, }, } def _count_all_servers_unique_players(*, db_path: Path) -> int: with _connect(db_path) as connection: row = connection.execute( """ SELECT COUNT(DISTINCT historical_players.id) AS unique_players FROM historical_player_match_stats INNER JOIN historical_players ON historical_players.id = historical_player_match_stats.historical_player_id """ ).fetchone() return int(row["unique_players"] or 0) if row is not None else 0 def _count_all_servers_maps(*, db_path: Path) -> int: with _connect(db_path) as connection: row = connection.execute( """ SELECT COUNT(DISTINCT COALESCE(map_pretty_name, map_name)) AS map_count FROM historical_matches """ ).fetchone() return int(row["map_count"] or 0) if row is not None else 0 def _list_all_servers_top_maps(*, db_path: Path, limit: int) -> list[dict[str, object]]: with _connect(db_path) as connection: rows = connection.execute( """ SELECT COALESCE(map_pretty_name, map_name, 'Mapa no disponible') AS map_name, COUNT(*) AS matches_count FROM historical_matches GROUP BY COALESCE(map_pretty_name, map_name, 'Mapa no disponible') ORDER BY matches_count DESC, map_name ASC LIMIT ? """, (limit,), ).fetchall() return [ { "map_name": row["map_name"], "matches_count": int(row["matches_count"] or 0), } for row in rows ] def _is_all_servers_selector(value: str | None) -> bool: return isinstance(value, str) and value.strip() == ALL_SERVERS_SLUG def _resolve_safe_match_url(raw_payload_ref: object, server_slug: object) -> str | None: return resolve_trusted_scoreboard_match_url(raw_payload_ref, server_slug) def _calculate_match_duration_seconds(started_at: object, ended_at: object) -> int | None: start_text = _stringify(started_at) end_text = _stringify(ended_at) if not start_text or not end_text: return None try: duration = _parse_timestamp(end_text) - _parse_timestamp(start_text) except ValueError: return None return max(0, int(duration.total_seconds())) def _start_of_week(value: datetime) -> datetime: normalized = value.astimezone(timezone.utc) midnight = normalized.replace(hour=0, minute=0, second=0, microsecond=0) return midnight - timedelta(days=midnight.weekday()) def _start_of_month(value: datetime) -> datetime: normalized = value.astimezone(timezone.utc) return normalized.replace(day=1, hour=0, minute=0, second=0, microsecond=0) def _start_of_previous_month(value: datetime) -> datetime: previous_day = value - timedelta(days=1) return _start_of_month(previous_day) def _calculate_window_days(*, window_start: datetime, window_end: datetime) -> int: delta = window_end - window_start return max(1, int((delta.total_seconds() + 86399) // 86400)) def _get_nested(payload: Mapping[str, object], *path: str) -> object: current: object = payload for key in path: if not isinstance(current, Mapping): return None current = current.get(key) return current def _utc_now_iso() -> str: return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")