diff --git a/ai/tasks/done/TASK-229-fix-legacy-historical-summary-and-recent-matches-timeouts.md b/ai/tasks/done/TASK-229-fix-legacy-historical-summary-and-recent-matches-timeouts.md new file mode 100644 index 0000000..89b7296 --- /dev/null +++ b/ai/tasks/done/TASK-229-fix-legacy-historical-summary-and-recent-matches-timeouts.md @@ -0,0 +1,109 @@ +--- +id: TASK-229 +title: Fix legacy historical summary and recent matches timeouts +status: done +type: backend +team: Backend Senior +supporting_teams: [] +roadmap_item: foundation +priority: high +--- + +# TASK-229 - Fix legacy historical summary and recent matches timeouts + +## Goal + +Corregir los endpoints historicos legacy agregados que seguian agotando el timeout de 30 s en produccion: + +- `GET /api/historical/server-summary?server=all-servers` +- `GET /api/historical/recent-matches?server=all-servers&limit=20` + +## Context + +La auditoria posterior a `TASK-228` confirmo que `/api/servers`, `/api/servers/latest` y `/api/servers/history` ya respondian correctamente, pero dos probes legacy de `historico.html legacy` seguian bloqueando: + +- `historical-server-summary-all-servers`: timeout 30024 ms. +- `historical-recent-matches-all-servers`: timeout 30045 ms. + +No se debian tocar `/api/servers`, `/api/current-match/kills` ni `/api/current-match/players`. + +## Files Read First + +- `ai/architecture-index.md` +- `ai/repo-context.md` +- `ai/orchestrator/backend-senior.md` +- `scripts/audit_public_requests.py` +- `backend/app/routes.py` +- `backend/app/payloads.py` +- `backend/app/historical_storage.py` +- `backend/app/postgres_display_storage.py` +- `backend/tests/test_historical_snapshot_refresh.py` + +## Call Chain Analysis + +Cadena de auditoria: + +1. `scripts/audit_public_requests.py` genera `historical-server-summary-all-servers` como `GET /api/historical/server-summary?server=all-servers`. +2. El mismo script genera `historical-recent-matches-all-servers` como `GET /api/historical/recent-matches?server=all-servers&limit=20`. +3. `backend/app/routes.py` mapea esas rutas a `build_historical_server_summary_payload()` y `build_recent_historical_matches_payload()`. +4. Los builders legacy intentaban usar el read model historico RCON y, si no cubria la peticion, caian a storage legacy publico. +5. Para `server=all-servers`, el fallback ejecutaba agregaciones globales: + - SQLite: `list_historical_server_summaries()` llama a `_build_all_servers_summary()` y esta recalcula conteos globales sobre tablas historicas. + - PostgreSQL: `list_scoreboard_server_summaries(server_slug=all-servers)` inicializa display storage y agrega tablas migradas de public-scoreboard. + - Recent matches puede completar desde `list_recent_historical_matches()`, tambien sobre storage legacy. +6. Los endpoints snapshot equivalentes (`/api/historical/snapshots/server-summary` y `/api/historical/snapshots/recent-matches`) ya respondian rapido porque solo leen snapshots precomputados con politica `read-only-fast-path`. + +## Root Cause + +Los endpoints legacy agregados para `all-servers` conservaban fallback runtime hacia CRCON/PostgreSQL display storage. En produccion, cuando el read model RCON no cubria la respuesta o necesitaba completar datos, el request publico entraba en agregaciones globales y/o inicializacion de storage, agotando el timeout de 30 s. + +## Changes + +- `backend/app/payloads.py` + - `build_recent_historical_matches_payload(server_slug="all-servers")` pasa a delegar en `build_recent_historical_matches_snapshot_payload()`. + - `build_historical_server_summary_payload(server_slug="all-servers")` pasa a delegar en `build_historical_server_summary_snapshot_payload()`. + - Ambos wrappers conservan `context` legacy y campos basicos (`items`, `limit`, `server_slug`, `summary_basis`) pero exponen `source: historical-precomputed-snapshots` y `legacy_endpoint_policy: snapshot-read-only-fast-path`. + - Si no existe snapshot, responden JSON controlado con snapshot missing y lista vacia, sin read model RCON ni fallback runtime pesado. +- `backend/tests/test_historical_snapshot_refresh.py` + - Cubre que los dos endpoints legacy `all-servers` usan snapshots. + - Verifica que no llaman a `get_rcon_historical_read_model()`. + - Verifica que no llaman a `list_recent_historical_matches()` ni `list_historical_server_summaries()`. +- `docs/FULL_APPLICATION_REQUEST_AUDIT.md` + - Documenta el estado post-fix de `TASK-229`. +- `docs/PERFORMANCE_PUBLIC_QUERY_AUDIT.md` + - Actualiza la politica de los endpoints legacy agregados. + +## Validation + +Validaciones ejecutadas: + +```powershell +python -m compileall backend/app +cd backend +python -m unittest tests.test_historical_snapshot_refresh +python -m unittest tests.test_current_match_payload tests.test_rcon_admin_log_storage tests.test_historical_snapshot_refresh +``` + +Resultados: + +- `python -m compileall backend/app`: OK. +- `cd backend; python -m unittest tests.test_historical_snapshot_refresh`: OK, 15 tests. +- `cd backend; python -m unittest tests.test_current_match_payload tests.test_rcon_admin_log_storage tests.test_historical_snapshot_refresh`: OK, 22 tests. + +Auditoria de produccion pendiente tras redeploy: + +```powershell +python .\scripts\audit_public_requests.py --base-url https://comunidadhll.devzamode.es --timeout 30 --filter servers --output tmp\task229_servers_recheck_after.json +``` + +## Outcome + +Los endpoints legacy agregados dejan de ejecutar read model RCON o fallback runtime pesado en lectura publica y quedan como wrappers read-only sobre snapshots precomputados. El contrato se mantiene compatible de forma razonable: HTTP 200, `status: ok`, `data.items` y metadata de snapshot/fallback. + +No se cambiaron hosts, puertos, `27001`, variables de entorno ni configuracion de servidores. No se tocaron frontend, assets, SVGs, imagenes fisicas, `ai/system-metrics.md` ni `tmp/`. + +## Change Budget + +- Archivos de codigo modificados: 2. +- Documentacion actualizada: 3 archivos. +- No se creo arquitectura nueva; solo se redirigio el path legacy agregado a read models/snapshots ya existentes. diff --git a/backend/app/payloads.py b/backend/app/payloads.py index bd9feaa..6f8dd10 100644 --- a/backend/app/payloads.py +++ b/backend/app/payloads.py @@ -1075,6 +1075,24 @@ def build_recent_historical_matches_payload( server_slug: str | None = None, ) -> dict[str, object]: """Return recent historical matches from persisted CRCON data.""" + if server_slug == ALL_SERVERS_SLUG: + snapshot_payload = build_recent_historical_matches_snapshot_payload( + limit=limit, + server_slug=server_slug, + ) + data = dict(snapshot_payload.get("data") or {}) + data.update( + { + "title": "Partidas recientes por servidor", + "context": "historical-recent-matches", + "source": "historical-precomputed-snapshots", + "historical_data_source": get_historical_data_source_kind(), + "coverage_basis": "precomputed-recent-matches-snapshot", + "legacy_endpoint_policy": "snapshot-read-only-fast-path", + } + ) + return {"status": snapshot_payload.get("status", "ok"), "data": data} + if get_historical_data_source_kind() == "rcon": data_source = get_rcon_historical_read_model() if data_source is not None: @@ -1746,6 +1764,25 @@ def build_historical_server_summary_payload( server_slug: str | None = None, ) -> dict[str, object]: """Return aggregated historical metrics per server.""" + if server_slug == ALL_SERVERS_SLUG: + snapshot_payload = build_historical_server_summary_snapshot_payload( + server_slug=server_slug, + ) + data = dict(snapshot_payload.get("data") or {}) + item = data.get("item") if isinstance(data.get("item"), dict) else None + data.update( + { + "title": "Cobertura historica agregada de todos los servidores", + "context": "historical-server-summary", + "source": "historical-precomputed-snapshots", + "summary_basis": "precomputed-server-summary-snapshot", + "weekly_ranking_window_days": 7, + "legacy_endpoint_policy": "snapshot-read-only-fast-path", + "items": [item] if item is not None else [], + } + ) + return {"status": snapshot_payload.get("status", "ok"), "data": data} + if get_historical_data_source_kind() == "rcon": data_source = get_rcon_historical_read_model() if data_source is not None: diff --git a/backend/tests/test_historical_snapshot_refresh.py b/backend/tests/test_historical_snapshot_refresh.py index 1c134ed..fb05068 100644 --- a/backend/tests/test_historical_snapshot_refresh.py +++ b/backend/tests/test_historical_snapshot_refresh.py @@ -21,7 +21,9 @@ from app.config import ( get_public_recent_matches_refresh_interval_seconds, ) from app.payloads import ( + build_historical_server_summary_payload, build_leaderboard_snapshot_payload, + build_recent_historical_matches_payload, build_recent_historical_matches_snapshot_payload, ) from app.historical_runner import ( @@ -166,6 +168,66 @@ class HistoricalSnapshotRefreshTests(unittest.TestCase): self.assertEqual(len(payload["data"]["items"]), 1) self.assertFalse(payload["data"].get("fallback_used", False)) + def test_legacy_all_servers_recent_matches_uses_snapshot_fast_path(self) -> None: + snapshot = { + "generated_at": "2026-06-10T04:00:00Z", + "source_range_start": "2026-06-09T21:00:00Z", + "source_range_end": "2026-06-09T22:00:00Z", + "is_stale": False, + "payload": { + "items": [ + { + "match_id": "match-1", + "closed_at": "2026-06-09T22:00:00Z", + } + ], + "limit": 100, + }, + } + with ( + patch("app.payloads._get_historical_snapshot_record", return_value=snapshot), + patch("app.payloads.get_historical_data_source_kind", return_value="rcon"), + patch("app.payloads.get_rcon_historical_read_model") as rcon_loader, + patch("app.payloads.list_recent_historical_matches") as fallback_loader, + ): + payload = build_recent_historical_matches_payload( + server_slug="all-servers", + limit=20, + ) + + rcon_loader.assert_not_called() + fallback_loader.assert_not_called() + self.assertEqual(payload["data"]["context"], "historical-recent-matches") + self.assertEqual(payload["data"]["legacy_endpoint_policy"], "snapshot-read-only-fast-path") + self.assertEqual(payload["data"]["items"][0]["match_id"], "match-1") + + def test_legacy_all_servers_summary_uses_snapshot_fast_path(self) -> None: + snapshot = { + "generated_at": "2026-06-10T04:00:00Z", + "source_range_start": "2026-06-09T00:00:00Z", + "source_range_end": "2026-06-10T00:00:00Z", + "is_stale": False, + "payload": { + "item": { + "server": {"slug": "all-servers", "name": "Todos los servidores"}, + "matches_count": 12, + }, + }, + } + with ( + patch("app.payloads._get_historical_snapshot_record", return_value=snapshot), + patch("app.payloads.get_historical_data_source_kind", return_value="rcon"), + patch("app.payloads.get_rcon_historical_read_model") as rcon_loader, + patch("app.payloads.list_historical_server_summaries") as fallback_loader, + ): + payload = build_historical_server_summary_payload(server_slug="all-servers") + + rcon_loader.assert_not_called() + fallback_loader.assert_not_called() + self.assertEqual(payload["data"]["context"], "historical-server-summary") + self.assertEqual(payload["data"]["legacy_endpoint_policy"], "snapshot-read-only-fast-path") + self.assertEqual(payload["data"]["items"][0]["matches_count"], 12) + def test_rcon_coverage_accepts_postgres_datetime_values(self) -> None: start = datetime(2026, 5, 21, 10, 0, tzinfo=timezone.utc) end = datetime(2026, 5, 21, 11, 30, tzinfo=timezone.utc) diff --git a/docs/FULL_APPLICATION_REQUEST_AUDIT.md b/docs/FULL_APPLICATION_REQUEST_AUDIT.md index 0bae11c..9ca43cc 100644 --- a/docs/FULL_APPLICATION_REQUEST_AUDIT.md +++ b/docs/FULL_APPLICATION_REQUEST_AUDIT.md @@ -191,6 +191,46 @@ Invoke-WebRequest "$base/api/servers/history" | python .\scripts\audit_public_requests.py --base-url https://comunidadhll.devzamode.es --timeout 30 --filter servers --output tmp\task228_servers_audit_after.json ``` +## Estado post-fix TASK-229 + +Fecha: 2026-06-10 +Alcance aplicado: endpoints historicos legacy agregados que seguian agotando timeout tras `TASK-228`. + +URLs afectadas: + +- `GET /api/historical/server-summary?server=all-servers` +- `GET /api/historical/recent-matches?server=all-servers&limit=20` + +Causa confirmada: + +- `scripts/audit_public_requests.py` etiqueta esas rutas como `historical-server-summary-all-servers` y `historical-recent-matches-all-servers`. +- `backend/app/routes.py` las mapea a `build_historical_server_summary_payload()` y `build_recent_historical_matches_payload()`. +- Antes del fix, esos builders legacy intentaban read model RCON y, si no habia cobertura suficiente, caian a storage CRCON/PostgreSQL legacy. +- Para `server=all-servers`, el fallback ejecutaba agregaciones globales o inicializacion de display storage en lectura publica. En produccion esa cadena agotaba el timeout de 30 s. +- Los endpoints snapshot equivalentes ya respondian rapido porque solo leen snapshots precomputados. + +Cambios aplicados: + +- `build_historical_server_summary_payload(server_slug="all-servers")` delega en `build_historical_server_summary_snapshot_payload()`. +- `build_recent_historical_matches_payload(server_slug="all-servers")` delega en `build_recent_historical_matches_snapshot_payload()`. +- Ambos mantienen `context` legacy y `items`, pero declaran `source: historical-precomputed-snapshots` y `legacy_endpoint_policy: snapshot-read-only-fast-path`. +- Si falta snapshot, la respuesta sigue siendo JSON controlado y rapido, sin RCON live, sin fallback runtime pesado y sin `initialize_*` de storage historico desde estos paths agregados. + +Estado esperado tras redeploy: + +| Endpoint | Estado TASK-229 en codigo | Severidad esperada tras deploy | +| --- | --- | --- | +| `/api/historical/server-summary?server=all-servers` | Wrapper legacy sobre snapshot read-only | OK o WARNING si snapshot missing, sin timeout | +| `/api/historical/recent-matches?server=all-servers&limit=20` | Wrapper legacy sobre snapshot read-only | OK o WARNING si snapshot missing, sin timeout | +| `/api/historical/snapshots/server-summary?server=all-servers` | Snapshot read-only | OK o WARNING si snapshot missing | +| `/api/historical/snapshots/recent-matches?server=all-servers&limit=100` | Snapshot read-only | OK o WARNING si snapshot missing | + +Comando de validacion de produccion tras redeploy: + +```powershell +python .\scripts\audit_public_requests.py --base-url https://comunidadhll.devzamode.es --timeout 30 --filter servers --output tmp\task229_servers_recheck_after.json +``` + ## Evidencia ejecutada Comandos ejecutados: @@ -318,10 +358,10 @@ La tabla usa rangos cuando una ruta se lanzo con varias combinaciones representa | M026 | backend-api | `backend/app/routes.py` | snapshot monthly MVP V2 | GET | `/api/historical/snapshots/monthly-mvp-v2` | `limit`, `server` | `/api/historical/snapshots/monthly-mvp-v2?server=all-servers&limit=10` | `build_monthly_mvp_v2_snapshot_payload` | displayed snapshots | snapshot | si en Postgres display | no pesado hoy | bajo | n/a | produccion | 200 | 40-41 ms | 1539 B | snapshot missing/fallback | WARNING | completar snapshots | | M027 | backend-api | `backend/app/routes.py` | snapshot player events | GET | `/api/historical/snapshots/player-events` | `limit`, `server`, `view` | `/api/historical/snapshots/player-events?server=all-servers&view=duels&limit=10` | `build_player_event_snapshot_payload` | displayed snapshots | snapshot | si en Postgres display | no pesado hoy | bajo | n/a | produccion | 200 | 40-57 ms | ~1176-1188 B | snapshot missing/fallback | WARNING | completar snapshots | | M028 | backend-api | `backend/app/routes.py` | snapshot weekly leaderboard | GET | `/api/historical/snapshots/weekly-leaderboard` | `limit`, `server`, `metric` | `/api/historical/snapshots/weekly-leaderboard?server=comunidad-hispana-01&metric=kills&limit=10` | `build_weekly_leaderboard_snapshot_payload` | displayed snapshots | snapshot | si en Postgres display | no pesado hoy | bajo | n/a | produccion | 200 | 44-70 ms | ~1610 B | snapshot missing/fallback | WARNING | completar snapshots | -| M029 | backend-api | `backend/app/routes.py` | historico recent matches legacy | GET | `/api/historical/recent-matches` | `limit`, `server` | `/api/historical/recent-matches?server=all-servers&limit=20` | `build_recent_historical_matches_payload` | RCON read model + scoreboard merge | read-model/legacy merge | posible init en fallback storage | si | medio | n/a | produccion | 200 | 278.44-1286.47 ms | ~24885 B | OK pero mas lento que snapshot | OK | preferir snapshot en frontend | +| M029 | backend-api | `backend/app/routes.py` | historico recent matches legacy | GET | `/api/historical/recent-matches` | `limit`, `server` | `/api/historical/recent-matches?server=all-servers&limit=20` | `build_recent_historical_matches_payload` | snapshot recent matches | wrapper legacy sobre snapshot | no en `all-servers` | no | bajo | n/a | pendiente redeploy TASK-229 | pendiente | pendiente | pendiente | `legacy_endpoint_policy=snapshot-read-only-fast-path` | OK o WARNING sin timeout | mantener como compatibilidad legacy | | M030 | backend-api | `backend/app/routes.py` | historico recent snapshot | GET | `/api/historical/snapshots/recent-matches` | `limit`, `server` | `/api/historical/snapshots/recent-matches?server=all-servers&limit=20` | `build_recent_historical_matches_snapshot_payload` | displayed snapshots | snapshot | si en Postgres display | no | bajo | historico handles error | produccion | 200 | 48-82 ms | ~24185 B | OK | OK | mantener, quitar init display despues | | M031 | backend-api | `backend/app/routes.py` | historico match detail | GET | `/api/historical/matches/detail` | `server`, `match` | `/api/historical/matches/detail?server=comunidad-hispana-01&match=comunidad-hispana-01:1781023156:1781028555:purpleheartlanewarfare` | `build_historical_match_detail_payload` | RCON materialized detail; scoreboard fallback | materialized/read-model + fallback legacy | si: `get_materialized_rcon_match_detail` -> `initialize_rcon_materialized_storage` -> `initialize_postgres_rcon_storage`; fallback display init | si | medio | historico-partida muestra error, sin timeout | produccion | 200 | 120.47 ms | 125580 B | hoy OK, deuda de init persiste | OK | hardening posterior read-only estricto | -| M032 | backend-api | `backend/app/routes.py` | historico server summary legacy | GET | `/api/historical/server-summary` | `server` | `/api/historical/server-summary?server=all-servers` | `build_historical_server_summary_payload` | RCON read model | read-model/fallback | posible init en fallback storage | si | bajo | n/a | produccion | 200 | 95.82-145.29 ms | ~2480 B | OK | OK | mantener | +| M032 | backend-api | `backend/app/routes.py` | historico server summary legacy | GET | `/api/historical/server-summary` | `server` | `/api/historical/server-summary?server=all-servers` | `build_historical_server_summary_payload` | snapshot server summary | wrapper legacy sobre snapshot | no en `all-servers` | no | bajo | n/a | pendiente redeploy TASK-229 | pendiente | pendiente | pendiente | `legacy_endpoint_policy=snapshot-read-only-fast-path` | OK o WARNING sin timeout | mantener como compatibilidad legacy | | M033 | backend-api | `backend/app/routes.py` | historico server summary snapshot | GET | `/api/historical/snapshots/server-summary` | `server` | `/api/historical/snapshots/server-summary?server=all-servers` | `build_historical_server_summary_snapshot_payload` | displayed snapshots | snapshot | si en Postgres display | no | bajo | historico handles missing | produccion | 200 | 50.03-62.60 ms | ~1042 B | `fallback_used=true` | WARNING | completar snapshot/read-only | | M034 | backend-api | `backend/app/routes.py` | historical player profile | GET | `/api/historical/player-profile` | `player` | `/api/historical/player-profile?player=76561198092154180` | `build_historical_player_profile_payload` | legacy historical profile | legacy | si en Postgres display/historical fallback | si | medio | n/a | manual produccion | 200 | 4736.64 ms | no capturado | fallback true | WARNING | no cargar de inicio; optimizar si se mantiene publico | | M035 | backend-api | `backend/app/routes.py` | Elo/MMR leaderboard | GET | `/api/historical/elo-mmr/leaderboard` | `limit`, `server` | `/api/historical/elo-mmr/leaderboard?server=all-servers&limit=10` | `build_elo_mmr_leaderboard_payload` | Elo/MMR storage | legacy/paused | posible `initialize_elo_mmr_storage` si engine carga | si | bajo | no usado por frontend actual | produccion | 200 | 58.35 ms | 2257 B | fallback/paused | WARNING | no reactivar; mantener fuera de UI | diff --git a/docs/PERFORMANCE_PUBLIC_QUERY_AUDIT.md b/docs/PERFORMANCE_PUBLIC_QUERY_AUDIT.md index d3046c5..a156ae7 100644 --- a/docs/PERFORMANCE_PUBLIC_QUERY_AUDIT.md +++ b/docs/PERFORMANCE_PUBLIC_QUERY_AUDIT.md @@ -12,6 +12,8 @@ Actualizacion TASK-227, 2026-06-10: la auditoria real post-`TASK-226` mostro que Actualizacion TASK-228, 2026-06-10: `/api/servers` deja de ser cache-only estricto y vuelve a ser near-real-time controlado para la home. Sirve snapshot fresco si existe; si no hay cache o esta stale, intenta RCON/A2S con timeout publico corto y degrada a snapshot stale o JSON controlado si live falla. `/api/servers/latest` e `/api/servers/history` siguen siendo lecturas de almacenamiento local, no sustitutos del estado live. +Actualizacion TASK-229, 2026-06-10: los endpoints legacy agregados `/api/historical/server-summary?server=all-servers` y `/api/historical/recent-matches?server=all-servers&limit=20` dejan de ejecutar read model RCON o fallback runtime CRCON/PostgreSQL en lectura publica. Ahora son wrappers de los snapshots precomputados equivalentes, con `legacy_endpoint_policy=snapshot-read-only-fast-path`; si falta snapshot devuelven JSON controlado rapido en vez de agotar el timeout. + Conclusiones principales: - El backend de `ranking` ya no muestra el cuello de botella grave del ranking anual. La evidencia mas fuerte es el test `backend/tests/test_annual_ranking_payload.py`, que confirma que la lectura anual en PostgreSQL ya no inicializa storage en request publico. @@ -46,7 +48,9 @@ Flujo observado: | `/api/stats/players/{player_id}` | `frontend/assets/js/stats.js` | `build_stats_player_profile_payload()` | `player_period_stats` | Primero `player_period_stats`, si falta cae a runtime y ademas calcula weekly/monthly ranking en caliente | Si | P1 | | `/api/stats/rankings/annual` | `frontend/assets/js/stats.js` | `build_annual_ranking_snapshot_payload()` | `rcon_annual_ranking_snapshots`, `rcon_annual_ranking_snapshot_items` | Snapshot anual | No recalcula ranking en lectura | P2 | | `/api/historical/snapshots/leaderboard` | `frontend/assets/js/historico.js` | `build_rcon_materialized_leaderboard_snapshot_payload()` en modo rcon | Snapshot historico publico equivalente | En modo rcon el nombre dice snapshot, pero sirve runtime fast path sobre materialized leaderboard | Si, por definicion del endpoint en modo rcon | P1 | -| `/api/historical/snapshots/recent-matches` | `frontend/assets/js/historico.js` | `build_recent_historical_matches_snapshot_payload()` | Snapshot publico de recent matches | Segun source kind puede usar RCON read model o fallback publico | Si | P2 | +| `/api/historical/snapshots/recent-matches` | `frontend/assets/js/historico.js` | `build_recent_historical_matches_snapshot_payload()` | Snapshot publico de recent matches | Snapshot precomputado read-only | No | P2 | +| `/api/historical/recent-matches?server=all-servers` | legacy historico audit | `build_recent_historical_matches_payload()` | Snapshot publico de recent matches | Wrapper legacy sobre snapshot precomputado | No | P2 | +| `/api/historical/server-summary?server=all-servers` | legacy historico audit | `build_historical_server_summary_payload()` | Snapshot publico de server summary | Wrapper legacy sobre snapshot precomputado | No | P2 | | `/api/historical/matches/detail` | `frontend/assets/js/historico-partida.js` | `build_historical_match_detail_payload()` | Read model detalle de partida | Intenta `get_rcon_historical_match_detail()`, luego fallback a storage historico publico | Si | P2 | | `/api/current-match` | `frontend/assets/js/partida-actual.js` | `build_current_match_payload()` | Read model live propio | Primero intenta `_query_current_match_rcon_sample()` directo; luego fallback a `/api/servers` snapshot | Si, y toca RCON directo | P0 | | `/api/current-match/kills` | `frontend/assets/js/partida-actual.js` | `build_current_match_kill_feed_payload()` | Read model live propio de kill feed | AdminLog materializado en modo read-only publico; PostgreSQL usa `connect_postgres_compat(initialize=False)` | Degradacion JSON controlada si falla read model | P2 |