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