--- id: TASK-148 title: Relink existing RCON matches to scoreboard candidates status: done type: backend team: Backend Senior supporting_teams: - Arquitecto Python roadmap_item: rcon-full-data priority: high --- # TASK-148 - Relink existing RCON matches to scoreboard candidates ## Goal Add a deterministic relink flow that can update or resolve scoreboard URLs for already-materialized RCON matches after new scoreboard candidates are inserted. ## Background After manually inserting the Foy `1562115` candidate, the match detail page showed the expected scoreboard button. Future backfills may insert candidates after RCON matches already exist, so the system must relink existing RCON matches with null or missing `match_url` dynamically or via materialization. Known Foy regression target: - RCON match key: `comunidad-hispana-02:1779310451:1779315851:foywarfare` - candidate external match id: `1562115` - expected URL: `https://scoreboard.comunidadhll.es:5443/games/1562115` ## Constraints / DO NOT BREAK - Do not relax trusted scoreboard URL allowlist. - Do not link to unsafe origins. - Do not choose ambiguous candidates silently. - Do not break old Carentan match: `comunidad-hispana-02:1779178461:1779183861:carentanwarfare` - Do not break Kharkov: `comunidad-hispana-02:1779315955:1779319098:kharkovwarfare` - Do not re-add public scoreboard buttons to recent match cards. - Do not expose correlation debug text in normal UI. ## Allowed Changes - Existing RCON materialization, historical read-model, and RCON storage modules needed for deterministic candidate relinking. - `backend/app/scoreboard_candidate_backfill.py` only when needed to trigger or document the relink sequence. - A focused backend relink command module or a narrowly scoped command option on existing materialization CLI. - Focused backend scoreboard correlation tests and fixtures/mocks. - This task file when it moves through the task workflow. ## Implementation Requirements 1. Inspect current correlation logic: - `backend/app/rcon_admin_log_materialization.py` - `backend/app/rcon_historical_read_model.py` - `backend/app/postgres_rcon_storage.py` - `backend/app/scoreboard_candidate_backfill.py` 2. Add or improve a function that matches RCON materialized matches to `rcon_scoreboard_match_candidates` using: - same server slug / external server id - normalized map identity - overlapping or close time window - score allied/axis where present - winner where present 3. Tolerances: - allow reasonable time drift between RCON and scoreboard, at least +/- 15 minutes, preferably configurable or centralized - for matches with null `started_at` / `ended_at` but `closed_at` plus `duration_seconds`, derive a correlation window 4. When a best safe candidate is found, make `match_url` available in detail payload. 5. Prefer deterministic scoring: - exact map match - close end time - score match - same server - reject ambiguous ties unless one candidate is clearly better 6. Add a command or option to relink existing matches without needing new ingestion: - acceptable command module: `python -m app.rcon_scoreboard_relink` - acceptable alternative: an option inside `app.rcon_admin_log_materialization` 7. The relink command must print JSON summary: - `matches_scanned` - `candidates_scanned` - `matches_linked` - `matches_skipped_no_candidate` - `matches_skipped_ambiguous` - `errors` 8. Ensure `scoreboard_candidate_backfill` can optionally trigger relink at the end, or document the command sequence. 9. Add regression coverage for the Foy case: - RCON match key `comunidad-hispana-02:1779310451:1779315851:foywarfare` - candidate external match id `1562115` - expected `match_url` `https://scoreboard.comunidadhll.es:5443/games/1562115` 10. Ensure the detail endpoint returns `match_url` after relink. ## Files to Read First - `AGENTS.md` - `ai/architecture-index.md` - `ai/repo-context.md` - `ai/orchestrator/backend-senior.md` - `backend/app/rcon_admin_log_materialization.py` - `backend/app/rcon_historical_read_model.py` ## Expected Files to Modify - Existing RCON correlation/materialization/storage code needed for relinking. - One relink CLI entry point or one existing CLI option. - Focused scoreboard correlation regression tests. - This task file after moving it through the workflow. Keep correlation diagnostics out of this task except the JSON relink summary required here. Use TASK-149 for detailed diagnostic output and docs. ## Validation - `python -m compileall backend/app` - `python -m unittest discover -s tests -p "*scoreboard*"` - `powershell -ExecutionPolicy Bypass -File scripts/run-integration-tests.ps1` - `docker compose --profile advanced up -d --build backend frontend postgres` - `docker compose exec backend python -m app.scoreboard_candidate_backfill --server comunidad-hispana-02 --from 2026-05-20T00:00:00Z --to 2026-05-21T23:59:59Z --max-pages 5 --page-size 100` - Run the new relink command. - Use `Invoke-WebRequest` for the Foy detail payload and verify `found` is true and `match_url` is present. - Use `Invoke-WebRequest` for the Carentan detail payload and verify the existing `match_url` is still present. - Review `git diff --name-only` and confirm the changed files match this task. ## Manual Verification 1. Foy detail page shows `Ver en Scoreboard`. 2. Scoreboard button opens the `1562115` public URL. 3. Recent match cards still do not show public scoreboard links. 4. No `Detalle no disponible` state appears for the validated detail page. 5. Player filters and external profile links still work. ## Commit Message `fix: relink rcon matches to scoreboard candidates` ## Outcome Document correlation scoring choices, time-window derivation, ambiguity handling, relink command output, validation, and any follow-up task instead of expanding scope. - Added `python -m app.rcon_scoreboard_relink` to scan already-materialized ended RCON matches against the same trusted candidate resolution used by the detail read model. The command reports `matches_scanned`, `candidates_scanned`, `matches_linked`, `matches_skipped_no_candidate`, `matches_skipped_ambiguous` and `errors` as JSON. - Kept URL availability dynamic in the read model instead of adding a second persisted URL column to RCON matches. The new correlation resolution summary preserves the existing deterministic map/time/duration/score scoring path, returns explicit low-confidence/no-candidate/ambiguous reasons, and exposes the selected external candidate only to command callers. - Reused the existing materialized window derivation for closed-at plus server-time duration cases so relink and detail payloads evaluate the same effective correlation window. - Added a Foy regression for materialized match key `comunidad-hispana-02:1779310451:1779315851:foywarfare`: relink reports a safe link and detail returns `https://scoreboard.comunidadhll.es:5443/games/1562115`. - Validation: - `python -m compileall backend/app` - `python -m unittest discover -s tests -p "*scoreboard*"` from `backend/` passed with existing SQLite `ResourceWarning` output in the scoreboard regression file. - `powershell -ExecutionPolicy Bypass -File scripts/run-integration-tests.ps1` passed; the script reports no product integration tests configured for its platform-only scope. - `docker compose --profile advanced up -d --build backend frontend postgres` - Re-ran `app.scoreboard_candidate_backfill` for the requested May 20-21 window. Candidate refresh succeeded (`list_candidates_updated: 11`), while detail enrichment returned a partial status because SQLite reported `database is locked` for detail match `1562104`. - `docker compose exec backend python -m app.rcon_scoreboard_relink --server comunidad-hispana-02` reported 19 scanned matches, 13 linked matches, 6 no-candidate skips, 0 ambiguous skips and no errors. - HTTP detail verification returned `found: true` and the expected Foy URL. Existing Carentan detail key `comunidad-hispana-02:1779178461:1779183861:carentanwarfare` also kept a trusted `match_url`. - Scope review used `git diff --name-only`; relink changes stay within the correlation/read-model command path, focused scoreboard tests and this task file. ## Change Budget - Prefer fewer than 5 modified files. - Prefer changes under 200 lines when feasible. - Split follow-up work into a new task if the scope grows.