Add annual ranking snapshot storage

This commit is contained in:
devRaGonSa
2026-06-08 13:44:09 +02:00
parent 9b6e1645ac
commit 82d280e629
5 changed files with 159 additions and 86 deletions

View File

@@ -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).

View File

@@ -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.

View File

@@ -188,6 +188,39 @@ CREATE TABLE IF NOT EXISTS rcon_match_player_stats (
UNIQUE(target_key, match_key, player_id) 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 ( CREATE TABLE IF NOT EXISTS rcon_scoreboard_match_candidates (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
server_slug TEXT NOT NULL, 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); ON rcon_materialized_matches(target_key, ended_at DESC, ended_server_time DESC);
CREATE INDEX IF NOT EXISTS idx_rcon_match_player_stats_match CREATE INDEX IF NOT EXISTS idx_rcon_match_player_stats_match
ON rcon_match_player_stats(target_key, match_key); 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 CREATE INDEX IF NOT EXISTS idx_rcon_scoreboard_candidates_server_end
ON rcon_scoreboard_match_candidates(server_slug, ended_at DESC, started_at DESC); ON rcon_scoreboard_match_candidates(server_slug, ended_at DESC, started_at DESC);
""" """

View File

@@ -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 CREATE INDEX IF NOT EXISTS idx_rcon_match_player_stats_match
ON rcon_match_player_stats(target_key, match_key); 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 return resolved_path

View File

@@ -28,6 +28,8 @@ RCON_TABLES = (
"rcon_player_profile_snapshots", "rcon_player_profile_snapshots",
"rcon_materialized_matches", "rcon_materialized_matches",
"rcon_match_player_stats", "rcon_match_player_stats",
"rcon_annual_ranking_snapshots",
"rcon_annual_ranking_snapshot_items",
"rcon_scoreboard_match_candidates", "rcon_scoreboard_match_candidates",
) )
DISPLAY_TABLES = ( DISPLAY_TABLES = (
@@ -315,6 +317,8 @@ def _sync_sequences() -> None:
"rcon_player_profile_snapshots", "rcon_player_profile_snapshots",
"rcon_materialized_matches", "rcon_materialized_matches",
"rcon_match_player_stats", "rcon_match_player_stats",
"rcon_annual_ranking_snapshots",
"rcon_annual_ranking_snapshot_items",
"rcon_scoreboard_match_candidates", "rcon_scoreboard_match_candidates",
) )
with connect_display_postgres() as connection: with connect_display_postgres() as connection: