Files
comunidadhll/ai/tasks/done/TASK-247-implement-real-player-active-time-and-kpm.md

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