6.3 KiB
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.pybackend/app/rcon_admin_log_storage.pybackend/app/rcon_historical_read_model.pybackend/app/payloads.pybackend/app/postgres_rcon_storage.pybackend/app/config.pybackend/tests/test_rcon_materialization_pipeline.pybackend/tests/test_current_match_payload.pybackend/tests/test_rcon_admin_log_storage.pyfrontend/assets/js/historico-partida.jsfrontend/historico-partida.htmldocs/REAL_KPM_PLAYER_ACTIVE_SECONDS_DESIGN.mddocs/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_secondsfrom those intervals when they are reliable - mark
active_time_sourceaccording to the interval quality
Reliable interval sources:
connection_intervalsconnection_intervals_carryover
Fallback only, not valid for real KPM:
event_span_fallbackunavailable
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_secondsactive_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_secondsis missing:kpm = nullkpm_status = missing_active_time
- If active time comes only from fallback event span:
kpm = nullkpm_status = missing_connection_intervals
- If
player_active_seconds < 60:kpm = nullkpm_status = insufficient_active_time
- If active time is valid:
kpmis rounded to 2 decimalskpm_status = ready- only when
active_time_sourceisconnection_intervalsorconnection_intervals_carryover
Interval Rules
- connect inside match:
- open interval at
connected.server_time
- open interval at
- disconnect inside match:
- close interval at
disconnected.server_time
- close interval at
- connected before match start and no pre-start disconnect after that:
- carry interval from
match_start
- carry interval from
- still connected at match end:
- close interval at
match_end
- close interval at
- reconnections:
- sum all intervals
- inconsistent or incomplete connection evidence:
- do not mark KPM as ready
- keep
event_span_fallbackonly 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/appcd backend; python -m unittest tests.test_current_match_payloadcd backend; python -m unittest tests.test_rcon_admin_log_storagecd backend; python -m unittest tests.test_historical_snapshot_refreshcd backend; python -m unittest tests.test_rcon_materialization_pipeline.RconMaterializationPipelineTests.test_materialization_migrates_existing_player_stats_schema_with_active_time_columnscd backend; python -m unittest tests.test_rcon_materialization_pipeline.RconMaterializationPipelineTests.test_match_detail_keeps_kpm_missing_for_legacy_rows_without_active_timecd backend; python -m unittest tests.test_rcon_materialization_pipeline.RconMaterializationPipelineTests.test_match_detail_read_model_hides_raw_player_idscd backend; python -m unittest tests.test_rcon_materialization_pipeline.RconMaterializationPipelineTests.test_active_time_counts_full_match_for_player_connected_before_startcd backend; python -m unittest tests.test_rcon_materialization_pipeline.RconMaterializationPipelineTests.test_active_time_counts_until_disconnect_for_player_connected_before_startcd backend; python -m unittest tests.test_rcon_materialization_pipeline.RconMaterializationPipelineTests.test_active_time_counts_from_connect_until_match_endcd backend; python -m unittest tests.test_rcon_materialization_pipeline.RconMaterializationPipelineTests.test_active_time_sums_multiple_connection_intervalscd backend; python -m unittest tests.test_rcon_materialization_pipeline.RconMaterializationPipelineTests.test_active_time_uses_event_span_fallback_without_ready_kpm_when_connection_intervals_missingcd backend; python -m unittest tests.test_rcon_materialization_pipeline.RconMaterializationPipelineTests.test_kpm_is_null_when_active_time_is_below_thresholdcd backend; python -m unittest tests.test_rcon_materialization_pipeline.RconMaterializationPipelineTests.test_kpm_payload_helper_returns_ready_for_ten_kills_in_ten_minutescd backend; python -m unittest tests.test_rcon_materialization_pipeline.RconMaterializationPipelineTests.test_kpm_payload_helper_returns_zero_for_zero_kills_with_valid_active_timecd backend; python -m unittest tests.test_rcon_materialization_pipeline.RconMaterializationPipelineTests.test_kpm_payload_helper_returns_missing_when_active_time_is_nullnode --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.