Implement real player active time intervals and historical KPM
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
# TASK-247 - Implement real player active time and historical KPM
|
||||
|
||||
## Summary
|
||||
|
||||
This task implements the first real KPM base for historical match detail using reconstructed connection intervals from the RCON AdminLog materialization flow.
|
||||
|
||||
It does not use total match duration as a substitute for player time.
|
||||
|
||||
## Files Read First
|
||||
|
||||
- `backend/app/rcon_admin_log_materialization.py`
|
||||
- `backend/app/rcon_admin_log_storage.py`
|
||||
- `backend/app/rcon_historical_read_model.py`
|
||||
- `backend/app/payloads.py`
|
||||
- `backend/app/postgres_rcon_storage.py`
|
||||
- `backend/app/config.py`
|
||||
- `backend/tests/test_rcon_materialization_pipeline.py`
|
||||
- `backend/tests/test_current_match_payload.py`
|
||||
- `backend/tests/test_rcon_admin_log_storage.py`
|
||||
- `frontend/assets/js/historico-partida.js`
|
||||
- `frontend/historico-partida.html`
|
||||
- `docs/REAL_KPM_PLAYER_ACTIVE_SECONDS_DESIGN.md`
|
||||
- `docs/HISTORICAL_MATCH_KILLS_PER_MINUTE_ANALYSIS.md`
|
||||
|
||||
## Problem
|
||||
|
||||
Historical match detail could not show a real player KPM because the backend had no player-level active time persisted in the RCON materialized match facts.
|
||||
|
||||
Using total match duration would have reintroduced a false KPM.
|
||||
|
||||
## Decision
|
||||
|
||||
Use the existing RCON AdminLog materialization path as the source of truth.
|
||||
|
||||
For each player inside one materialized match:
|
||||
|
||||
- keep `first_seen_server_time`
|
||||
- keep `last_seen_server_time`
|
||||
- reconstruct connected intervals inside the match window
|
||||
- persist `player_active_seconds` from those intervals when they are reliable
|
||||
- mark `active_time_source` according to the interval quality
|
||||
|
||||
Reliable interval sources:
|
||||
|
||||
- `connection_intervals`
|
||||
- `connection_intervals_carryover`
|
||||
|
||||
Fallback only, not valid for real KPM:
|
||||
|
||||
- `event_span_fallback`
|
||||
- `unavailable`
|
||||
|
||||
Then expose KPM in the historical match detail payload only when active time is present and reaches a minimum threshold.
|
||||
|
||||
## Persistence
|
||||
|
||||
Added to `rcon_match_player_stats`:
|
||||
|
||||
- `player_active_seconds`
|
||||
- `active_time_source`
|
||||
|
||||
This is applied for both SQLite and PostgreSQL schema initialization/migration.
|
||||
|
||||
Legacy rows remain compatible. They keep `player_active_seconds = null` until they are rematerialized or replaced by newer matches.
|
||||
|
||||
## KPM Rules
|
||||
|
||||
- Formula:
|
||||
- `kills / (player_active_seconds / 60)`
|
||||
- Minimum threshold:
|
||||
- `HLL_KPM_MIN_ACTIVE_SECONDS`
|
||||
- default `60`
|
||||
- If `player_active_seconds` is missing:
|
||||
- `kpm = null`
|
||||
- `kpm_status = missing_active_time`
|
||||
- If active time comes only from fallback event span:
|
||||
- `kpm = null`
|
||||
- `kpm_status = missing_connection_intervals`
|
||||
- If `player_active_seconds < 60`:
|
||||
- `kpm = null`
|
||||
- `kpm_status = insufficient_active_time`
|
||||
- If active time is valid:
|
||||
- `kpm` is rounded to 2 decimals
|
||||
- `kpm_status = ready`
|
||||
- only when `active_time_source` is `connection_intervals` or `connection_intervals_carryover`
|
||||
|
||||
## Interval Rules
|
||||
|
||||
- connect inside match:
|
||||
- open interval at `connected.server_time`
|
||||
- disconnect inside match:
|
||||
- close interval at `disconnected.server_time`
|
||||
- connected before match start and no pre-start disconnect after that:
|
||||
- carry interval from `match_start`
|
||||
- still connected at match end:
|
||||
- close interval at `match_end`
|
||||
- reconnections:
|
||||
- sum all intervals
|
||||
- inconsistent or incomplete connection evidence:
|
||||
- do not mark KPM as ready
|
||||
- keep `event_span_fallback` only as auxiliary observed time
|
||||
|
||||
## Squad Time
|
||||
|
||||
Not implemented.
|
||||
|
||||
This task reviewed the possibility of discounting time without squad/unit/role, but the current historical source is not reliable enough to compute `squad_active_seconds` safely. That remains future work.
|
||||
|
||||
## Frontend
|
||||
|
||||
`historico-partida.js` now shows a `KPM` stat chip in the expanded player panel only when `kpm_status == "ready"`.
|
||||
|
||||
Old matches without active time do not show a fake `0.00`.
|
||||
|
||||
## Validation
|
||||
|
||||
Executed:
|
||||
|
||||
- `python -m compileall backend/app`
|
||||
- `cd backend; python -m unittest tests.test_current_match_payload`
|
||||
- `cd backend; python -m unittest tests.test_rcon_admin_log_storage`
|
||||
- `cd backend; python -m unittest tests.test_historical_snapshot_refresh`
|
||||
- `cd backend; python -m unittest tests.test_rcon_materialization_pipeline.RconMaterializationPipelineTests.test_materialization_migrates_existing_player_stats_schema_with_active_time_columns`
|
||||
- `cd backend; python -m unittest tests.test_rcon_materialization_pipeline.RconMaterializationPipelineTests.test_match_detail_keeps_kpm_missing_for_legacy_rows_without_active_time`
|
||||
- `cd backend; python -m unittest tests.test_rcon_materialization_pipeline.RconMaterializationPipelineTests.test_match_detail_read_model_hides_raw_player_ids`
|
||||
- `cd backend; python -m unittest tests.test_rcon_materialization_pipeline.RconMaterializationPipelineTests.test_active_time_counts_full_match_for_player_connected_before_start`
|
||||
- `cd backend; python -m unittest tests.test_rcon_materialization_pipeline.RconMaterializationPipelineTests.test_active_time_counts_until_disconnect_for_player_connected_before_start`
|
||||
- `cd backend; python -m unittest tests.test_rcon_materialization_pipeline.RconMaterializationPipelineTests.test_active_time_counts_from_connect_until_match_end`
|
||||
- `cd backend; python -m unittest tests.test_rcon_materialization_pipeline.RconMaterializationPipelineTests.test_active_time_sums_multiple_connection_intervals`
|
||||
- `cd backend; python -m unittest tests.test_rcon_materialization_pipeline.RconMaterializationPipelineTests.test_active_time_uses_event_span_fallback_without_ready_kpm_when_connection_intervals_missing`
|
||||
- `cd backend; python -m unittest tests.test_rcon_materialization_pipeline.RconMaterializationPipelineTests.test_kpm_is_null_when_active_time_is_below_threshold`
|
||||
- `cd backend; python -m unittest tests.test_rcon_materialization_pipeline.RconMaterializationPipelineTests.test_kpm_payload_helper_returns_ready_for_ten_kills_in_ten_minutes`
|
||||
- `cd backend; python -m unittest tests.test_rcon_materialization_pipeline.RconMaterializationPipelineTests.test_kpm_payload_helper_returns_zero_for_zero_kills_with_valid_active_time`
|
||||
- `cd backend; python -m unittest tests.test_rcon_materialization_pipeline.RconMaterializationPipelineTests.test_kpm_payload_helper_returns_missing_when_active_time_is_null`
|
||||
- `node --check frontend/assets/js/historico-partida.js`
|
||||
|
||||
## Notes
|
||||
|
||||
- No scheduler changes were required.
|
||||
- No RCON host/port/server config changes were made.
|
||||
- No weapon/icon assets were touched.
|
||||
- Historical KPM remains forward-valid only. Old rows stay null until real interval evidence exists.
|
||||
Reference in New Issue
Block a user