Add real KPM active seconds design
This commit is contained in:
@@ -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.
|
||||
603
docs/REAL_KPM_PLAYER_ACTIVE_SECONDS_DESIGN.md
Normal file
603
docs/REAL_KPM_PLAYER_ACTIVE_SECONDS_DESIGN.md
Normal file
@@ -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.
|
||||
Reference in New Issue
Block a user