From 82d280e6295bcab0753dbc8e4a4f88cce5dffc14 Mon Sep 17 00:00:00 2001 From: devRaGonSa Date: Mon, 8 Jun 2026 13:44:09 +0200 Subject: [PATCH] Add annual ranking snapshot storage --- ...167-add-annual-ranking-snapshot-storage.md | 69 +++++++++++++++ ...167-add-annual-ranking-snapshot-storage.md | 86 ------------------- backend/app/postgres_rcon_storage.py | 41 +++++++++ backend/app/rcon_admin_log_materialization.py | 45 ++++++++++ backend/app/sqlite_to_postgres_migration.py | 4 + 5 files changed, 159 insertions(+), 86 deletions(-) create mode 100644 ai/tasks/done/TASK-167-add-annual-ranking-snapshot-storage.md delete mode 100644 ai/tasks/pending/TASK-167-add-annual-ranking-snapshot-storage.md diff --git a/ai/tasks/done/TASK-167-add-annual-ranking-snapshot-storage.md b/ai/tasks/done/TASK-167-add-annual-ranking-snapshot-storage.md new file mode 100644 index 0000000..7faf71d --- /dev/null +++ b/ai/tasks/done/TASK-167-add-annual-ranking-snapshot-storage.md @@ -0,0 +1,69 @@ +--- +id: TASK-167 +title: Add annual ranking snapshot storage +status: done +type: backend +team: Arquitecto de Base de Datos +supporting_teams: + - Arquitecto Python + - Backend Senior +roadmap_item: foundation +--- + +# TASK-167 - Add annual ranking snapshot storage + +## Goal + +Implement storage primitives to persist annual top 20 player ranking snapshots, following the schema plan in `docs/annual-ranking-snapshot-schema-plan.md`, without adding generator logic or annual ranking APIs yet. + +## Context + +`TASK-166-design-annual-ranking-snapshot-schema.md` defined the intended schema and constraints for: + +- `rcon_annual_ranking_snapshots` +- `rcon_annual_ranking_snapshot_items` + +This task creates the storage layer so future yearly ranking generators can run on top of materialized RCON data in SQLite and PostgreSQL-compatible paths. + +The implementation must stay backend-only and must not touch frontend assets. + +## Validation Scope + +The work intentionally stayed limited to backend storage schema initialization and migration compatibility. + +## Outcome + +### Archivos modificados + +- backend/app/rcon_admin_log_materialization.py +- backend/app/postgres_rcon_storage.py +- backend/app/sqlite_to_postgres_migration.py +- ai/tasks/done/TASK-167-add-annual-ranking-snapshot-storage.md + +### Tablas nuevas añadidas + +- rcon_annual_ranking_snapshots +- rcon_annual_ranking_snapshot_items + +### Compatibilidad SQLite/Postgres + +- SQLite: tablas y restricciones añadidas en `initialize_rcon_materialized_storage()` (backend/app/rcon_admin_log_materialization.py). +- PostgreSQL: tablas, índices y restricciones añadidas a `RCON_SCHEMA_SQL` (backend/app/postgres_rcon_storage.py). +- Migración: tablas añadidas a `RCON_TABLES` y a `_sync_sequences()` en `backend/app/sqlite_to_postgres_migration.py`. + +### Validaciones realizadas + +- `python -m compileall backend/app` +- `scripts/run-integration-tests.ps1` +- Validación de inicialización de storage SQLite con base temporal y consulta en `sqlite_master` para ambas tablas. + +### Limitaciones conocidas + +- No se implementó generador anual ni endpoint de ranking anual. +- No se tocó frontend. +- No hay validación de runtime PostgreSQL porque no había entorno Postgres requerido por el alcance de esta task. + +### Próximo task recomendado + +- TASK-168 (Generador de snapshot anual top 20). + diff --git a/ai/tasks/pending/TASK-167-add-annual-ranking-snapshot-storage.md b/ai/tasks/pending/TASK-167-add-annual-ranking-snapshot-storage.md deleted file mode 100644 index 93d3366..0000000 --- a/ai/tasks/pending/TASK-167-add-annual-ranking-snapshot-storage.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -id: TASK-167 -title: Add annual ranking snapshot storage -status: pending -type: backend -team: Arquitecto de Base de Datos -supporting_teams: - - Arquitecto Python - - Backend Senior -roadmap_item: foundation ---- - -# TASK-167 - Add annual ranking snapshot storage - -## Goal - -Implement storage primitives to persist annual top 20 player ranking snapshots, following the schema plan in `docs/annual-ranking-snapshot-schema-plan.md`, without adding generator logic or annual ranking APIs yet. - -## Context - -`TASK-166-design-annual-ranking-snapshot-schema.md` defined the intended schema and constraints for: -- `rcon_annual_ranking_snapshots` -- `rcon_annual_ranking_snapshot_items` - -This task creates the storage layer so future yearly ranking generators can run on top of materialized RCON data in SQLite and PostgreSQL-compatible paths. - -The implementation must stay backend-only and must not touch frontend assets. - -## Steps - -1. Read all files listed below before any edit. -2. Add snapshot table creation for both SQLite and PostgreSQL-compatible schema paths if present. -3. Include SQLite/Postgres-safe DDL in migration-like storage initializer surfaces used by the project. -4. Keep changes minimal and avoid endpoint or generator creation. -5. Validate syntax/initialization and keep changed files within expected scope. -6. Update task outcome once completed and move to `ai/tasks/done` only after scope checks pass. - -## Files to Read First - -- `AGENTS.md` -- `ai/repo-context.md` -- `ai/architecture-index.md` -- `docs/annual-ranking-snapshot-schema-plan.md` -- `backend/app/rcon_admin_log_materialization.py` -- `backend/app/postgres_rcon_storage.py` -- `backend/app/sqlite_to_postgres_migration.py` -- `backend/app/rcon_historical_leaderboards.py` -- `backend/app/rcon_historical_player_stats.py` - -## Expected Files to Modify - -- `backend/app/rcon_admin_log_materialization.py` or equivalent materialized-RCON storage init module -- `backend/app/postgres_rcon_storage.py` or equivalent PostgreSQL schema initializer -- `backend/app/sqlite_to_postgres_migration.py` if migration table lists and sequence sync require alignment -- `ai/tasks/done/TASK-167-add-annual-ranking-snapshot-storage.md` - -## Constraints - -- No generador anual de ranking en este scope. -- No endpoint anual agregado aún. -- No modificaciones de frontend. -- No tocar `frontend/assets/js/partida-actual.js`. -- No tocar `frontend/assets/img/clans/bxb.png`. -- Mantener compatibilidad SQLite/Postgres si la capa existente ya la usa. -- No reactivar Elo/MMR. -- No reintroducir Comunidad Hispana #03. -- No ampliar `historical workers` salvo cambios estrictamente necesarios en inicialización de storage. - -## Validation - -- `python -m compileall backend/app` -- `scripts/run-integration-tests.ps1` (si aplica). -- Validación de inicialización de storage para SQLite. -- Validación de compatibilidad SQL/DDL con Postgres en los módulos de compatibilidad existentes. -- `git diff --name-only` debe mostrar sólo el alcance esperado. - -## Outcome - -Documentar en el cierre de task: -- Archivos modificados. -- Tablas nuevas añadidas (`rcon_annual_ranking_snapshots`, `rcon_annual_ranking_snapshot_items`). -- Compatibilidad SQLite/Postgres. -- Validaciones ejecutadas. -- Limitaciones conocidas. -- Siguiente task recomendada: generador de snapshot anual top 20. - diff --git a/backend/app/postgres_rcon_storage.py b/backend/app/postgres_rcon_storage.py index 4fad140..e01a084 100644 --- a/backend/app/postgres_rcon_storage.py +++ b/backend/app/postgres_rcon_storage.py @@ -188,6 +188,39 @@ CREATE TABLE IF NOT EXISTS rcon_match_player_stats ( UNIQUE(target_key, match_key, player_id) ); +CREATE TABLE IF NOT EXISTS rcon_annual_ranking_snapshots ( + id BIGSERIAL PRIMARY KEY, + year INTEGER NOT NULL, + server_key TEXT NOT NULL, + metric TEXT NOT NULL, + limit_size INTEGER NOT NULL DEFAULT 20, + source_basis TEXT NOT NULL DEFAULT 'rcon-admin-log', + window_start TEXT, + window_end TEXT, + generated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + status TEXT NOT NULL DEFAULT 'ready', + source_matches_count INTEGER NOT NULL DEFAULT 0, + CHECK (limit_size > 0), + CHECK (metric IN ('kills', 'deaths', 'matches_over_100_kills', 'support')), + UNIQUE (year, server_key, metric) +); + +CREATE TABLE IF NOT EXISTS rcon_annual_ranking_snapshot_items ( + id BIGSERIAL PRIMARY KEY, + snapshot_id BIGINT NOT NULL REFERENCES rcon_annual_ranking_snapshots(id) ON DELETE CASCADE, + ranking_position INTEGER NOT NULL, + player_id TEXT NOT NULL, + player_name TEXT NOT NULL, + metric_value BIGINT NOT NULL DEFAULT 0, + matches_considered INTEGER NOT NULL DEFAULT 0, + kills BIGINT NOT NULL DEFAULT 0, + deaths BIGINT NOT NULL DEFAULT 0, + teamkills BIGINT NOT NULL DEFAULT 0, + kd_ratio DOUBLE PRECISION NOT NULL DEFAULT 0.0, + UNIQUE(snapshot_id, ranking_position), + UNIQUE(snapshot_id, player_id) +); + CREATE TABLE IF NOT EXISTS rcon_scoreboard_match_candidates ( id BIGSERIAL PRIMARY KEY, server_slug TEXT NOT NULL, @@ -219,6 +252,14 @@ CREATE INDEX IF NOT EXISTS idx_rcon_materialized_matches_recent ON rcon_materialized_matches(target_key, ended_at DESC, ended_server_time DESC); CREATE INDEX IF NOT EXISTS idx_rcon_match_player_stats_match ON rcon_match_player_stats(target_key, match_key); +CREATE INDEX IF NOT EXISTS idx_rcon_annual_ranking_snapshots_year +ON rcon_annual_ranking_snapshots(year, server_key, metric); +CREATE INDEX IF NOT EXISTS idx_rcon_annual_ranking_snapshots_status +ON rcon_annual_ranking_snapshots(status); +CREATE INDEX IF NOT EXISTS idx_rcon_annual_snapshot_items_snapshot +ON rcon_annual_ranking_snapshot_items(snapshot_id, ranking_position); +CREATE INDEX IF NOT EXISTS idx_rcon_annual_snapshot_items_player +ON rcon_annual_ranking_snapshot_items(snapshot_id, player_id); CREATE INDEX IF NOT EXISTS idx_rcon_scoreboard_candidates_server_end ON rcon_scoreboard_match_candidates(server_slug, ended_at DESC, started_at DESC); """ diff --git a/backend/app/rcon_admin_log_materialization.py b/backend/app/rcon_admin_log_materialization.py index 879983c..a2c0e82 100644 --- a/backend/app/rcon_admin_log_materialization.py +++ b/backend/app/rcon_admin_log_materialization.py @@ -83,6 +83,51 @@ def initialize_rcon_materialized_storage(*, db_path: Path | None = None) -> Path CREATE INDEX IF NOT EXISTS idx_rcon_match_player_stats_match ON rcon_match_player_stats(target_key, match_key); + + CREATE TABLE IF NOT EXISTS rcon_annual_ranking_snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + year INTEGER NOT NULL, + server_key TEXT NOT NULL, + metric TEXT NOT NULL, + limit_size INTEGER NOT NULL DEFAULT 20, + source_basis TEXT NOT NULL DEFAULT 'rcon-admin-log', + window_start TEXT, + window_end TEXT, + generated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + status TEXT NOT NULL DEFAULT 'ready', + source_matches_count INTEGER NOT NULL DEFAULT 0, + CHECK (limit_size > 0), + CHECK (metric IN ('kills', 'deaths', 'matches_over_100_kills', 'support')), + UNIQUE (year, server_key, metric) + ); + + CREATE INDEX IF NOT EXISTS idx_rcon_annual_ranking_snapshots_year + ON rcon_annual_ranking_snapshots(year, server_key, metric); + + CREATE INDEX IF NOT EXISTS idx_rcon_annual_ranking_snapshots_status + ON rcon_annual_ranking_snapshots(status); + + CREATE TABLE IF NOT EXISTS rcon_annual_ranking_snapshot_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + snapshot_id INTEGER NOT NULL REFERENCES rcon_annual_ranking_snapshots(id) ON DELETE CASCADE, + ranking_position INTEGER NOT NULL, + player_id TEXT NOT NULL, + player_name TEXT NOT NULL, + metric_value INTEGER NOT NULL DEFAULT 0, + matches_considered INTEGER NOT NULL DEFAULT 0, + kills INTEGER NOT NULL DEFAULT 0, + deaths INTEGER NOT NULL DEFAULT 0, + teamkills INTEGER NOT NULL DEFAULT 0, + kd_ratio REAL NOT NULL DEFAULT 0.0, + UNIQUE(snapshot_id, ranking_position), + UNIQUE(snapshot_id, player_id) + ); + + CREATE INDEX IF NOT EXISTS idx_rcon_annual_snapshot_items_snapshot + ON rcon_annual_ranking_snapshot_items(snapshot_id, ranking_position); + + CREATE INDEX IF NOT EXISTS idx_rcon_annual_snapshot_items_player + ON rcon_annual_ranking_snapshot_items(snapshot_id, player_id); """ ) return resolved_path diff --git a/backend/app/sqlite_to_postgres_migration.py b/backend/app/sqlite_to_postgres_migration.py index a28d102..84d0b01 100644 --- a/backend/app/sqlite_to_postgres_migration.py +++ b/backend/app/sqlite_to_postgres_migration.py @@ -28,6 +28,8 @@ RCON_TABLES = ( "rcon_player_profile_snapshots", "rcon_materialized_matches", "rcon_match_player_stats", + "rcon_annual_ranking_snapshots", + "rcon_annual_ranking_snapshot_items", "rcon_scoreboard_match_candidates", ) DISPLAY_TABLES = ( @@ -315,6 +317,8 @@ def _sync_sequences() -> None: "rcon_player_profile_snapshots", "rcon_materialized_matches", "rcon_match_player_stats", + "rcon_annual_ranking_snapshots", + "rcon_annual_ranking_snapshot_items", "rcon_scoreboard_match_candidates", ) with connect_display_postgres() as connection: