diff --git a/ai/tasks/done/TASK-218-design-player-active-seconds-for-real-kpm.md b/ai/tasks/done/TASK-218-design-player-active-seconds-for-real-kpm.md new file mode 100644 index 0000000..74fffc6 --- /dev/null +++ b/ai/tasks/done/TASK-218-design-player-active-seconds-for-real-kpm.md @@ -0,0 +1,170 @@ +--- +id: TASK-218 +title: Design player active seconds for real KPM +status: done +type: documentation +team: Arquitecto de Base de Datos +supporting_teams: + - Backend Senior + - Arquitecto Python +roadmap_item: foundation +priority: high +--- + +# TASK-218 - Design player active seconds for real KPM + +## Goal + +Analizar y disenar como calcular KPM real en HLL Vietnam sin implementar cambios funcionales todavia. + +KPM real se define como: + +```text +kills / minutos_jugados_reales +``` + +No debe confundirse con `kills_per_match`, `Kills/partida` ni kills divididas por duracion completa de partida cuando el jugador no estuvo toda la partida. + +## Context + +`TASK-216` confirmo que hoy no existe tiempo jugado real por jugador materializado para ranking publico. + +Campos encontrados: + +- `rcon_player_profile_snapshots.play_time`: texto de perfil, no normalizado y no por jugador/partida. +- `rcon_match_player_stats.first_seen_server_time` y `last_seen_server_time`: presencia observada por eventos dentro de partida. +- `rcon_materialized_matches.started_at`, `ended_at`, `started_server_time` y `ended_server_time`: duracion de partida, no tiempo por jugador. + +`TASK-217` dejo `kills_per_match` como `Kills/partida` y no implemento KPM real. + +Esta task produce un diseno tecnico accionable para crear `player_active_seconds` o `rcon_match_player_presence` en tasks futuras, sin modificar codigo funcional ni esquema real. + +Preserve the current product identity: Spanish-speaking HLL Vietnam community, military/Vietnam/tactical/sober visual direction and controlled repository evolution. + +## Steps + +1. Revisar fuentes de AdminLog, materializacion, read models y snapshots. +2. Evaluar eventos disponibles para presencia real o observada. +3. Clasificar calidad de datos: `exact`, `observed`, `estimated`, `unknown`. +4. Proponer opciones de modelo de datos. +5. Recomendar una estrategia de backfill y propagacion a read models. +6. Documentar plan de implementacion dividido en tasks. +7. Validar que solo se crearon documentos. + +## Files to Read First + +- `AGENTS.md` +- `ai/repo-context.md` +- `ai/architecture-index.md` +- `backend/app/rcon_admin_log_storage.py` +- `backend/app/rcon_admin_log_materialization.py` +- `backend/app/rcon_admin_log_parser.py` + +Tambien se revisaron: + +- `backend/app/postgres_rcon_storage.py` +- `backend/app/rcon_historical_player_stats.py` +- `backend/app/rcon_historical_leaderboards.py` +- `backend/app/rcon_annual_rankings.py` +- `backend/app/historical_runner.py` +- `backend/app/payloads.py` +- `docs/PERFORMANCE_PUBLIC_QUERY_AUDIT.md` +- `ai/tasks/done/TASK-216-clean-ranking-ui-and-fix-kpm-metric.md` +- `ai/tasks/done/TASK-217-add-annual-ranking-snapshots-for-supported-metrics.md` + +## Expected Files to Modify + +- `docs/REAL_KPM_PLAYER_ACTIVE_SECONDS_DESIGN.md` +- `ai/tasks/done/TASK-218-design-player-active-seconds-for-real-kpm.md` + +## Constraints + +- Keep the change minimal. +- Preserve HLL Vietnam project identity. +- Do not introduce unnecessary frameworks or dependencies. +- Do not implement backend functionality. +- Do not change endpoints. +- Do not change frontend. +- Do not change backend functional code. +- Do not change real schema yet. +- No ejecutar `ai-platform run`. +- No hacer push. +- No tocar `frontend/assets/img/weapons/`. +- No tocar SVGs. +- No modificar imagenes fisicas. +- No tocar `ai/system-metrics.md`. +- No reactivar Elo/MMR. +- No reintroducir Comunidad Hispana #03. +- No incluir cambios previos no relacionados. + +## Validation + +Before completing the task ensure: + +- `git status --short --untracked-files=all` +- `git diff --name-only` +- Confirmar que solo se crean/modifican: + - `docs/REAL_KPM_PLAYER_ACTIVE_SECONDS_DESIGN.md` + - `ai/tasks/done/TASK-218-design-player-active-seconds-for-real-kpm.md` + +## Outcome + +Archivos creados: + +- `docs/REAL_KPM_PLAYER_ACTIVE_SECONDS_DESIGN.md` +- `ai/tasks/done/TASK-218-design-player-active-seconds-for-real-kpm.md` + +Conclusion: + +- No se puede calcular KPM real publico hoy. +- La fuente recomendada para `player_active_seconds` es AdminLog con `connected`/`disconnected` cuando sea posible y ventana observada por eventos como fallback etiquetado. +- El modelo recomendado es Opcion C: + - crear `rcon_match_player_presence` como detalle auditable por jugador/partida; + - denormalizar `player_active_seconds` y `playtime_quality` en `rcon_match_player_stats` para agregaciones rapidas. +- KPM futuro debe calcularse en generacion de read models/snapshots como `kills / (player_active_seconds / 60)`, nunca en frontend y nunca usando `kills_per_match`. + +Plan de implementacion propuesto: + +1. `TASK-219-create-rcon-match-player-presence-schema` +2. `TASK-220-materialize-player-presence-from-adminlog` +3. `TASK-221-backfill-player-active-seconds` +4. `TASK-222-denormalize-player-active-seconds-into-match-stats` +5. `TASK-223-extend-player-period-stats-with-kpm` +6. `TASK-224-add-kpm-ranking-snapshots` +7. `TASK-225-expose-real-kpm-in-api-payloads` +8. `TASK-226-show-real-kpm-in-ranking-ui` +9. `TASK-227-validate-kpm-quality-and-performance` + +Riesgos principales: + +- Cobertura incompleta de eventos `connected`/`disconnected`. +- Presencia observada por eventos puede subestimar tiempo activo. +- Duracion completa de partida no representa tiempo por jugador y debe quedar `estimated`. +- Mezclar calidades sin metadata haria KPM enganoso. +- Backfill masivo puede competir con lecturas publicas si no se agenda bien. + +Validacion ejecutada: + +- `git status --short --untracked-files=all` +- `git diff --name-only` + +Confirmacion de exclusiones: + +- No se ejecuto `ai-platform run`. +- No se hizo push. +- No se tocaron assets de armas. +- No se tocaron SVGs. +- No se modificaron imagenes fisicas. +- No se toco `ai/system-metrics.md`. +- No se reactivo Elo/MMR. +- No se reintrodujo Comunidad Hispana #03. +- No se modifico frontend. +- No se modifico backend funcional. +- No se modificaron tests. +- No se modifico esquema real. +- No se incluyeron cambios previos no relacionados. + +## Change Budget + +- Archivos modificados por esta task: 2. +- Sin cambios funcionales. diff --git a/docs/REAL_KPM_PLAYER_ACTIVE_SECONDS_DESIGN.md b/docs/REAL_KPM_PLAYER_ACTIVE_SECONDS_DESIGN.md new file mode 100644 index 0000000..e61e73c --- /dev/null +++ b/docs/REAL_KPM_PLAYER_ACTIVE_SECONDS_DESIGN.md @@ -0,0 +1,603 @@ +# Real KPM Player Active Seconds Design + +## Resumen ejecutivo + +KPM real debe significar `kills / minutos_jugados_reales`. HLL Vietnam no puede calcularlo de forma publica y fiable hoy porque ningun read model materializa tiempo activo real por jugador y partida. Lo que existe actualmente permite dos lecturas parciales: + +- Presencia observada por eventos AdminLog dentro de una partida: `rcon_match_player_stats.first_seen_server_time` y `last_seen_server_time`. +- Duracion de partida: `rcon_materialized_matches.started_server_time`, `ended_server_time`, `started_at`, `ended_at`. + +La primera puede servir como base `observed` si se etiqueta claramente. La segunda no debe usarse como KPM real por jugador salvo como calidad `estimated`, y no deberia publicarse como KPM competitivo sin decision explicita futura. + +Recomendacion: crear `rcon_match_player_presence` como tabla de detalle por jugador y partida, y derivar `player_active_seconds` hacia `rcon_match_player_stats` y read models publicos. Este enfoque conserva trazabilidad, permite backfill por calidad y evita mezclar datos exactos, observados y estimados. + +## Definicion de KPM real + +Formula: + +```text +KPM = kills / (player_active_seconds / 60) +``` + +Reglas: + +- El numerador debe ser kills agregadas del mismo scope que el tiempo. +- El denominador debe ser minutos reales jugados por el jugador, no minutos de partida. +- KPM debe calcularse en generacion de read models/snapshots, nunca en frontend salvo formateo. +- `kills_per_match` debe seguir llamandose `Kills/partida` o `KPP`; nunca KPM. + +No es KPM: + +- `kills / partidas` +- `kills_per_match` +- `kills / duracion completa de partida` si el jugador no estuvo toda la partida +- `kills / ventana observada` sin marcar calidad + +## Estado actual + +`TASK-216` confirmo que no existe hoy un campo fiable de tiempo jugado real por jugador materializado para ranking publico. + +Campos existentes: + +- `rcon_player_profile_snapshots.play_time`: texto de perfil, no normalizado, no por jugador/partida y no apto para ranking publico sin parser y reconciliacion. +- `rcon_match_player_stats.first_seen_server_time`: primer `server_time` observado para el jugador dentro de una partida materializada. +- `rcon_match_player_stats.last_seen_server_time`: ultimo `server_time` observado para el jugador dentro de una partida materializada. +- `rcon_materialized_matches.started_server_time` y `ended_server_time`: limites de partida, no presencia real por jugador. +- `rcon_materialized_matches.started_at` y `ended_at`: timestamps de partida, no presencia real por jugador. + +`TASK-217` dejo `kills_per_match` como `Kills/partida` y no implemento KPM real. + +## Por que no sirve kills_per_match + +`kills_per_match` responde a otra pregunta: rendimiento medio por partida contabilizada. + +Ejemplo: + +```text +1222 kills / 32 partidas = 38.19 kills/partida +``` + +Eso no dice si el jugador estuvo 20 minutos, 45 minutos o 90 minutos por partida. Mostrar ese valor como KPM exagera o distorsiona la metrica porque el denominador no es tiempo real jugado. + +## Fuentes de datos disponibles + +### AdminLog raw + +Tabla: + +- `rcon_admin_log_events` + +Campos relevantes: + +- `target_key` +- `external_server_id` +- `event_timestamp` +- `server_time` +- `event_type` +- `parsed_payload_json` +- `raw_message` + +Eventos parseados: + +- `connected` +- `disconnected` +- `kill` +- `team_switch` +- `chat` +- `message` +- `match_start` +- `match_end` + +Calidad: + +- `connected` + `disconnected` dentro de limites de match pueden dar calidad `exact` si ambos eventos existen y se pueden emparejar de forma consistente. +- `kill`, `team_switch`, `chat` y `message` dan presencia `observed`, no presencia continua exacta. +- `server_time` es la unidad operativa mas util para duraciones intra-partida. + +### rcon_match_player_stats + +Tabla: + +- `rcon_match_player_stats` + +Campos relevantes: + +- `target_key` +- `match_key` +- `player_id` +- `player_name` +- `kills` +- `deaths` +- `teamkills` +- `first_seen_server_time` +- `last_seen_server_time` + +Calidad: + +- Ya agrupa por jugador y partida. +- Sus `first_seen_server_time` y `last_seen_server_time` son presencia observada por eventos, no tiempo activo real garantizado. +- Es buen destino de agregado, pero no conserva suficiente detalle para auditar intervalos de conexion/desconexion. + +### rcon_materialized_matches + +Tabla: + +- `rcon_materialized_matches` + +Campos relevantes: + +- `target_key` +- `external_server_id` +- `match_key` +- `started_server_time` +- `ended_server_time` +- `started_at` +- `ended_at` +- `source_basis` + +Calidad: + +- Define limites de partida. +- No debe usarse como tiempo por jugador sin presencia individual. +- Puede acotar intervalos de presencia para evitar duraciones negativas o fuera de partida. + +### rcon_player_profile_snapshots + +Tabla: + +- `rcon_player_profile_snapshots` + +Campos relevantes: + +- `player_id` +- `source_server_time` +- `first_seen` +- `sessions` +- `matches_played` +- `play_time` + +Calidad: + +- `play_time` es texto de perfil y puede ser acumulado global del servidor. +- No esta normalizado por partida. +- No puede distribuirse con precision por weekly/monthly/annual. +- Puede servir como comparacion diagnostica, no como fuente primaria para KPM publico. + +### player_period_stats y snapshots + +Tablas: + +- `player_period_stats` +- `ranking_snapshot_items` +- `rcon_annual_ranking_snapshot_items` + +Estado: + +- Tienen kills, deaths, teamkills, matches, K/D y Kills/partida. +- No tienen `player_active_seconds`, `playtime_quality` ni `kills_per_minute`. + +## Evaluacion de calidad de datos + +| Fuente | Calidad | Uso recomendado | +|---|---|---| +| `connected` + `disconnected` emparejados dentro de match | `exact` | Calcular intervalos reales acotados por partida | +| `connected` sin `disconnected` pero con match end | `observed` o `estimated` segun politica | Acotar hasta `ended_server_time`, no publicar como exacto | +| `disconnected` sin `connected` pero con match start | `observed` | Acotar desde primer evento observado o match start solo si se etiqueta | +| `kill`, `team_switch`, `chat`, `message` | `observed` | Inferir ventana `first_seen`/`last_seen` | +| Duracion completa de partida | `estimated` | Solo diagnostico o fallback no publico | +| Sin eventos suficientes | `unknown` | No calcular KPM | +| `rcon_player_profile_snapshots.play_time` | `unknown` para KPM por periodo | No usar como fuente primaria | + +Valores recomendados para `playtime_quality`: + +- `exact`: intervalos conectados/desconectados suficientemente cerrados y acotados por partida. +- `observed`: ventana inferida por eventos del jugador dentro de la partida. +- `estimated`: duracion completa o parcial inferida sin evidencia individual suficiente. +- `unknown`: no hay base util. + +Regla publica recomendada: + +- Publicar KPM solo con `exact` u `observed`. +- No publicar KPM para `estimated` o `unknown` salvo decision explicita futura y etiqueta visible. + +## Modelo de datos propuesto + +### Opcion A: anadir player_active_seconds a rcon_match_player_stats + +Columnas nuevas: + +- `player_active_seconds INTEGER` +- `playtime_quality TEXT NOT NULL DEFAULT 'unknown'` +- `playtime_source TEXT` +- `playtime_first_server_time BIGINT` +- `playtime_last_server_time BIGINT` +- `playtime_interval_count INTEGER NOT NULL DEFAULT 0` + +Clave existente: + +- `UNIQUE(target_key, match_key, player_id)` + +Indices recomendados: + +- `idx_rcon_match_player_stats_playtime_quality` sobre `(playtime_quality)` +- `idx_rcon_match_player_stats_player_playtime` sobre `(player_id, target_key, match_key, player_active_seconds)` + +Como se rellena: + +- Durante `materialize_rcon_admin_log`, despues de derivar stats por jugador. +- Se calcula a partir de eventos en la ventana del match. +- Si solo hay `first_seen_server_time` y `last_seen_server_time`, usar `observed`. + +Ventajas: + +- Menor cambio para read models existentes. +- Agregaciones weekly/monthly/annual simples. + +Riesgos: + +- Pierde detalle de intervalos exactos. +- Mezcla metrica agregada con evidencia. +- Es mas dificil auditar reconexiones multiples. + +### Opcion B: crear rcon_match_player_presence + +Tabla nueva propuesta: + +```sql +CREATE TABLE rcon_match_player_presence ( + id BIGSERIAL PRIMARY KEY, + target_key TEXT NOT NULL, + external_server_id TEXT, + match_key TEXT NOT NULL, + player_id TEXT NOT NULL, + player_name TEXT NOT NULL, + first_seen_server_time BIGINT, + last_seen_server_time BIGINT, + active_seconds INTEGER NOT NULL DEFAULT 0, + playtime_quality TEXT NOT NULL DEFAULT 'unknown', + interval_count INTEGER NOT NULL DEFAULT 0, + evidence_event_count INTEGER NOT NULL DEFAULT 0, + evidence_event_types TEXT NOT NULL DEFAULT '[]', + source_basis TEXT NOT NULL DEFAULT 'rcon-admin-log', + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(target_key, match_key, player_id) +); +``` + +Indices recomendados: + +- `(target_key, match_key)` +- `(player_id, target_key, match_key)` +- `(playtime_quality, active_seconds)` +- `(external_server_id, match_key)` + +Como se rellena: + +- Leer `rcon_admin_log_events` acotados por `rcon_materialized_matches.started_server_time` y `ended_server_time`. +- Construir intervalos por jugador: + - abrir intervalo en `connected` + - cerrar intervalo en `disconnected` + - acotar cada intervalo por match start/end + - sumar intervalos no solapados +- Si no hay intervalos completos, usar ventana de eventos observados: + - `first_seen_server_time = MIN(server_time)` de eventos del jugador + - `last_seen_server_time = MAX(server_time)` de eventos del jugador + - `active_seconds = max(0, last_seen - first_seen)` + - `playtime_quality = observed` +- Si solo existe una observacion puntual, `active_seconds = 0` o minimo configurable, calidad `observed-low-confidence`. + Para mantener enum simple, guardar `observed` y exponer `evidence_event_count`. +- No usar duracion completa de partida salvo como `estimated`. + +Ventajas: + +- Auditable. +- Permite recalculo/backfill independiente. +- No contamina stats base hasta tener calidad suficiente. + +Riesgos: + +- Requiere nueva tabla, backfill y runner. +- La exactitud depende de cobertura real de eventos `connected`/`disconnected`. + +### Opcion C: ambas + +Recomendacion final: + +- Crear `rcon_match_player_presence` como fuente de verdad de presencia por jugador/partida. +- Denormalizar hacia `rcon_match_player_stats.player_active_seconds` y `playtime_quality` para agregaciones rapidas. + +Por que: + +- La tabla de detalle permite auditoria y recalculo. +- El campo denormalizado evita joins costosos en generacion de snapshots. +- La ruta publica sigue leyendo snapshots/read models, no calculando KPM en request. + +## Estrategia de calculo + +### Intervalos exactos + +Entrada: + +- Eventos `connected` y `disconnected` con `player_id`, `server_time`. +- Limites de match `started_server_time`, `ended_server_time`. + +Algoritmo: + +1. Ordenar eventos por `server_time`, `id`. +2. Por jugador, abrir intervalo en `connected`. +3. Cerrar intervalo en `disconnected`. +4. Acotar inicio/fin al rango de la partida. +5. Fusionar intervalos solapados. +6. Sumar segundos. +7. Marcar `playtime_quality = exact` solo si la evidencia permite explicar inicio y fin. + +### Ventana observada + +Entrada: + +- Eventos `kill`, `team_switch`, `chat`, `message`, `connected`, `disconnected`. + +Algoritmo: + +1. Tomar min/max `server_time` por jugador dentro del match. +2. Calcular `active_seconds = max(0, last_seen - first_seen)`. +3. Guardar `evidence_event_count` y tipos. +4. Marcar `playtime_quality = observed`. + +### Estimacion por duracion de partida + +Entrada: + +- `started_server_time`, `ended_server_time`. + +Politica: + +- No usar para KPM publico por defecto. +- Si se guarda, marcar `playtime_quality = estimated`. +- Requiere flag futuro para mostrarse. + +## Estrategia de backfill + +Fase 1: + +- Crear tabla `rcon_match_player_presence`. +- Backfill desde `rcon_admin_log_events` y `rcon_materialized_matches`. +- No cambiar API publica. + +Fase 2: + +- Denormalizar `player_active_seconds` y `playtime_quality` en `rcon_match_player_stats`. +- Backfill stats desde presence. + +Fase 3: + +- Extender `player_period_stats` con: + - `player_active_seconds` + - `playtime_quality` + - `kills_per_minute` +- Regenerar weekly/monthly/yearly period stats. + +Fase 4: + +- Extender `ranking_snapshot_items` y `rcon_annual_ranking_snapshot_items` con: + - `player_active_seconds` + - `playtime_quality` + - `kills_per_minute` +- Generar snapshots KPM solo para filas con calidad permitida. + +Reglas de backfill: + +- Si hay intervalos exactos, usar `exact`. +- Si solo hay `first_seen`/`last_seen` por eventos, usar `observed`. +- Si solo hay duracion completa de partida, usar `estimated` y excluir de KPM publico. +- Si no hay evidencia, usar `unknown` y excluir de KPM. +- No mezclar `exact` y `observed` sin conservar calidad agregada. + +Calidad agregada por periodo: + +- `exact` si todos los segundos agregados vienen de exact. +- `observed` si hay mezcla exact + observed o solo observed. +- `estimated` si incluye estimated. +- `unknown` si no hay segundos validos. + +## Cambios necesarios en read models + +### rcon_match_player_stats + +Campos recomendados: + +- `player_active_seconds INTEGER` +- `playtime_quality TEXT` +- `kills_per_minute REAL` + +Nota: `kills_per_minute` puede no guardarse en stats base si se prefiere derivarlo en read models. Si se guarda, debe recalcularse cada vez que cambien kills o segundos. + +### player_period_stats + +Campos recomendados: + +- `player_active_seconds INTEGER NOT NULL DEFAULT 0` +- `playtime_quality TEXT NOT NULL DEFAULT 'unknown'` +- `kills_per_minute REAL` + +Agregacion: + +```text +SUM(kills) / (SUM(player_active_seconds) / 60) +``` + +Filtro publico: + +- Incluir KPM solo si `SUM(player_active_seconds) > 0` y calidad agregada no es `estimated` ni `unknown`. + +### ranking_snapshot_items + +Campos recomendados: + +- `player_active_seconds` +- `playtime_quality` +- `kills_per_minute` + +Nueva metrica snapshot: + +- `kills_per_minute` + +Orden: + +- `metric_value DESC` +- `player_active_seconds DESC` +- `kills DESC` +- `matches_considered DESC` +- `player_name ASC` + +### rcon_annual_ranking_snapshot_items + +Campos recomendados: + +- `player_active_seconds` +- `playtime_quality` +- `kills_per_minute` + +Regla: + +- Annual KPM debe tener snapshot propio `metric = kills_per_minute`. +- No representar KPM anual con `kills_per_match`. + +### Perfil de jugador + +Exponer: + +- `player_active_seconds` +- `player_active_minutes` +- `kills_per_minute` +- `playtime_quality` + +Evitar: + +- Calcular KPM runtime en request publico. +- Mezclar periodo semanal/mensual/anual sin metadata. + +## Cambios necesarios en snapshots weekly/monthly/annual + +Weekly/monthly: + +- Extender generacion en `rcon_historical_leaderboards`. +- Agregar metrica `kills_per_minute` solo si existe `player_active_seconds`. +- Persistir `kills_per_minute` en `ranking_snapshot_items`. + +Annual: + +- Extender `rcon_annual_rankings` cuando exista presence/read model. +- Agregar `kills_per_minute` a metricas soportadas solo despues del backfill. +- Persistir snapshot independiente por anio, servidor y metrica. + +General: + +- Public read path debe seguir leyendo solo snapshots. +- Missing snapshot debe devolver `snapshot_status=missing`, sin fallback runtime. + +## Cambios necesarios en API y frontend + +API: + +- Incluir `kills_per_minute` solo cuando venga de read model/snapshot. +- Incluir `playtime_quality`. +- Mantener `kills_per_match` como `Kills/partida`. +- Rechazar o marcar missing si se pide KPM sin snapshot real. + +Frontend: + +- Mostrar columna `KPM` solo si backend expone `kills_per_minute`. +- Mostrar tooltip o metadata de calidad si se decide publicar `observed`. +- No calcular KPM en JS. +- No mostrar KPM para `estimated` o `unknown`. + +## Impacto en refresh runner + +El runner actual refresca: + +- RCON capture +- materializaciones +- player search index +- player period stats +- ranking snapshots + +Orden futuro recomendado: + +1. RCON capture/AdminLog ingestion. +2. Materializar matches y player stats. +3. Materializar `rcon_match_player_presence`. +4. Denormalizar `player_active_seconds` a `rcon_match_player_stats`. +5. Refrescar `player_search_index`. +6. Refrescar `player_period_stats`. +7. Refrescar ranking snapshots weekly/monthly. +8. Refrescar annual snapshots cuando aplique. + +## Comandos futuros de produccion + +Nombres propuestos; no existen todavia: + +```bash +docker compose exec backend python -m app.rcon_player_presence migrate +docker compose exec backend python -m app.rcon_player_presence backfill --year 2026 --server-key all-servers +docker compose exec backend python -m app.rcon_player_presence backfill --year 2026 --server-key comunidad-hispana-01 +docker compose exec backend python -m app.rcon_player_presence backfill --year 2026 --server-key comunidad-hispana-02 +docker compose exec backend python -m app.rcon_historical_player_stats refresh-player-period-stats +docker compose exec backend python -m app.rcon_historical_leaderboards refresh-ranking-snapshots --limit 30 +docker compose exec backend python -m app.rcon_annual_rankings generate --year 2026 --server-key all-servers --metric kills_per_minute --limit 30 --replace-existing +docker compose exec backend python -m app.rcon_annual_rankings generate --year 2026 --server-key comunidad-hispana-01 --metric kills_per_minute --limit 30 --replace-existing +docker compose exec backend python -m app.rcon_annual_rankings generate --year 2026 --server-key comunidad-hispana-02 --metric kills_per_minute --limit 30 --replace-existing +``` + +## Plan de implementacion por tasks + +1. `TASK-219-create-rcon-match-player-presence-schema` + Crear schema SQLite/Postgres para `rcon_match_player_presence`, sin cambiar API publica. + +2. `TASK-220-materialize-player-presence-from-adminlog` + Implementar materializacion exact/observed desde `rcon_admin_log_events` y `rcon_materialized_matches`. + +3. `TASK-221-backfill-player-active-seconds` + Backfill historico por servidor/anio y reporte de cobertura por calidad. + +4. `TASK-222-denormalize-player-active-seconds-into-match-stats` + Agregar `player_active_seconds` y `playtime_quality` a `rcon_match_player_stats`. + +5. `TASK-223-extend-player-period-stats-with-kpm` + Extender `player_period_stats` con segundos activos y KPM real. + +6. `TASK-224-add-kpm-ranking-snapshots` + Agregar `kills_per_minute` a weekly/monthly snapshots y annual snapshots independientes. + +7. `TASK-225-expose-real-kpm-in-api-payloads` + Exponer `kills_per_minute` y `playtime_quality` desde snapshots/read models, sin runtime publico. + +8. `TASK-226-show-real-kpm-in-ranking-ui` + Mostrar KPM solo cuando backend entregue `kills_per_minute`; mantener Kills/partida separado. + +9. `TASK-227-validate-kpm-quality-and-performance` + Medir cobertura, latencia, `fallback_used=false` y comparar contra muestras manuales. + +## Riesgos principales + +- Cobertura incompleta de `connected`/`disconnected`. +- `server_time` puede reiniciarse o tener discontinuidades entre partidas; siempre debe estar acotado por `match_key`. +- Jugadores con eventos escasos pueden tener `observed` subestimado. +- Usar duracion completa de partida como tiempo de jugador inflaria o distorsionaria KPM. +- Mezclar calidades sin metadata haria la metrica poco confiable. +- Backfill masivo puede competir con lecturas publicas si se ejecuta fuera de ventana controlada. +- El frontend podria volver a confundir `Kills/partida` con KPM si no se separan nombres y payloads. + +## Criterios de aceptacion futuros + +- Existe `player_active_seconds` por jugador/partida con `playtime_quality`. +- Cada fila puede explicar su fuente: exact, observed, estimated o unknown. +- KPM se calcula como `kills / (player_active_seconds / 60)`. +- No se calcula KPM en frontend. +- `kills_per_match` sigue separado como `Kills/partida`. +- Weekly/monthly/annual KPM tienen snapshots propios. +- Request publico de ranking no consulta `rcon_match_player_stats` para calcular KPM. +- `fallback_used=false` en rutas publicas normales. +- Si falta snapshot KPM, el endpoint devuelve missing sin fallback runtime. +- Tests cubren exact, observed, estimated/unknown excluded, division por cero y orden de ranking.