Fix legacy historical server summary fast path

This commit is contained in:
devRaGonSa
2026-06-10 20:39:08 +02:00
parent 6233ff821f
commit 0317894bed
5 changed files with 256 additions and 23 deletions

View File

@@ -0,0 +1,117 @@
---
id: TASK-230
title: Fix historical server summary single server timeout
status: done
type: backend
team: Backend Senior
supporting_teams: []
roadmap_item: foundation
priority: high
---
# TASK-230 - Fix historical server summary single server timeout
## Goal
Corregir el ultimo `CRITICAL` restante de la auditoria publica global:
- `GET /api/historical/server-summary?server=comunidad-hispana-01`
## Context
Tras `TASK-229`, los endpoints legacy agregados `all-servers` dejaron de hacer timeout. La auditoria completa posterior mostro un unico `CRITICAL`:
- `historical-server-summary-comunidad-hispana-01`
- HTTP 200
- `10120.89 ms`
- `fallback=False`
Los endpoints relacionados respondian correctamente:
- `historical-server-summary-comunidad-hispana-02`: `139.37 ms`, OK, `fallback=False`.
- `snapshot-server-summary-comunidad-hispana-01`: `42.14 ms`, WARNING, `fallback=True`.
- `snapshot-server-summary-comunidad-hispana-02`: `55.97 ms`, WARNING, `fallback=True`.
- `historical-server-summary-all-servers`: ya corregido en `TASK-229`.
## Files Read First
- `ai/architecture-index.md`
- `ai/repo-context.md`
- `ai/orchestrator/backend-senior.md`
- `backend/README.md`
- `backend/requirements.txt`
- `docs/project-overview.md`
- `docs/decisions.md`
- `scripts/audit_public_requests.py`
- `backend/app/routes.py`
- `backend/app/payloads.py`
- `backend/app/rcon_historical_read_model.py`
- `backend/tests/test_historical_snapshot_refresh.py`
## Call Chain Analysis
Cadena de auditoria:
1. `scripts/audit_public_requests.py` genera `historical-server-summary-comunidad-hispana-01` como `GET /api/historical/server-summary?server=comunidad-hispana-01`.
2. `backend/app/routes.py` mapea `/api/historical/server-summary` a `build_historical_server_summary_payload(server_slug=...)`.
3. Tras `TASK-229`, solo `server=all-servers` usaba snapshot fast-path.
4. `server=comunidad-hispana-01` y `server=comunidad-hispana-02` seguian intentando `get_rcon_historical_read_model().list_server_summaries(...)`.
5. Ese read model llama a `list_rcon_historical_server_summaries()`.
6. `_build_server_summary()` enriquece cada resumen con `list_rcon_historical_recent_activity(server_key=..., limit=1)`.
7. Esa lectura intenta primero `list_materialized_rcon_matches(target_key=..., only_ended=True, limit=1)`, que entra en la capa materializada RCON/AdminLog.
8. En produccion, CH01 tardo ~10 s en esa lectura de actividad aunque termino con `fallback=False`; CH02 uso el mismo path pero respondio rapido. La diferencia observable es el coste de la lectura RCON/materialized para CH01, no un fallback CRCON ni un error de configuracion.
9. El endpoint snapshot equivalente ya respondia rapido porque solo lee snapshots precomputados.
## Root Cause
El endpoint legacy por servidor seguia usando el read model RCON runtime para `server-summary`. Aunque la respuesta fuese exitosa y `fallback=False`, el builder hacia una lectura adicional de actividad reciente por servidor para completar `activity.latest_*`. En CH01 esa consulta sobre datos materializados RCON/AdminLog era lenta y elevaba el endpoint a `CRITICAL`.
## Changes
- `backend/app/payloads.py`
- `build_historical_server_summary_payload()` usa snapshot fast-path para cualquier `server_slug` explicito, no solo `all-servers`.
- Se extrajo `_build_historical_server_summary_legacy_snapshot_payload()` para conservar `context`, `items`, `summary_basis`, `weekly_ranking_window_days` y `legacy_endpoint_policy`.
- Si no hay snapshot, devuelve JSON controlado con `items: []` y metadata de snapshot missing, sin RCON live ni fallback runtime pesado.
- `backend/tests/test_historical_snapshot_refresh.py`
- Cubre que `comunidad-hispana-01` no entra en `get_rcon_historical_read_model()` ni `list_historical_server_summaries()`.
- Cubre que `comunidad-hispana-02` sigue usando el mismo fast-path.
- Mantiene cobertura de `all-servers` tras `TASK-229`.
- `docs/FULL_APPLICATION_REQUEST_AUDIT.md`
- Documenta el estado post-fix de `TASK-230`.
- `docs/PERFORMANCE_PUBLIC_QUERY_AUDIT.md`
- Actualiza la politica de `/api/historical/server-summary?server=<scope>` como wrapper legacy sobre snapshot read-only.
## 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, 17 tests.
- `cd backend; python -m unittest tests.test_current_match_payload tests.test_rcon_admin_log_storage tests.test_historical_snapshot_refresh`: OK, 24 tests.
Auditoria de produccion pendiente tras redeploy:
```powershell
python .\scripts\audit_public_requests.py --base-url https://comunidadhll.devzamode.es --timeout 30 --output tmp\full_audit_after_task230.json
```
## Outcome
El endpoint legacy de resumen historico por servidor queda alineado con el path rapido de snapshots ya usado por `historico.html`. CH01, CH02 y `all-servers` evitan RCON live, scoreboard externo, inicializaciones de storage y fallbacks runtime pesados en lectura publica.
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.
- Sin cambios en frontend ni configuracion.

View File

@@ -1764,24 +1764,10 @@ def build_historical_server_summary_payload(
server_slug: str | None = None, server_slug: str | None = None,
) -> dict[str, object]: ) -> dict[str, object]:
"""Return aggregated historical metrics per server.""" """Return aggregated historical metrics per server."""
if server_slug == ALL_SERVERS_SLUG: if server_slug:
snapshot_payload = build_historical_server_summary_snapshot_payload( return _build_historical_server_summary_legacy_snapshot_payload(
server_slug=server_slug, 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": if get_historical_data_source_kind() == "rcon":
data_source = get_rcon_historical_read_model() data_source = get_rcon_historical_read_model()
@@ -1855,6 +1841,33 @@ def build_historical_server_summary_payload(
} }
def _build_historical_server_summary_legacy_snapshot_payload(
*,
server_slug: str,
) -> dict[str, object]:
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"
if server_slug == ALL_SERVERS_SLUG
else "Cobertura historica importada por servidor"
),
"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}
def build_historical_player_profile_payload(player_id: str) -> dict[str, object]: def build_historical_player_profile_payload(player_id: str) -> dict[str, object]:
"""Return aggregate historical metrics for one player identity.""" """Return aggregate historical metrics for one player identity."""
profile = get_historical_player_profile(player_id) profile = get_historical_player_profile(player_id)

View File

@@ -201,7 +201,61 @@ class HistoricalSnapshotRefreshTests(unittest.TestCase):
self.assertEqual(payload["data"]["legacy_endpoint_policy"], "snapshot-read-only-fast-path") self.assertEqual(payload["data"]["legacy_endpoint_policy"], "snapshot-read-only-fast-path")
self.assertEqual(payload["data"]["items"][0]["match_id"], "match-1") self.assertEqual(payload["data"]["items"][0]["match_id"], "match-1")
def test_legacy_all_servers_summary_uses_snapshot_fast_path(self) -> None: def test_legacy_server_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": "comunidad-hispana-01", "name": "Comunidad Hispana #01"},
"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="comunidad-hispana-01")
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"]["server_slug"], "comunidad-hispana-01")
self.assertEqual(payload["data"]["items"][0]["matches_count"], 12)
def test_legacy_second_server_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": "comunidad-hispana-02", "name": "Comunidad Hispana #02"},
"matches_count": 8,
},
},
}
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="comunidad-hispana-02")
rcon_loader.assert_not_called()
fallback_loader.assert_not_called()
self.assertEqual(payload["data"]["server_slug"], "comunidad-hispana-02")
self.assertEqual(payload["data"]["items"][0]["matches_count"], 8)
def test_legacy_all_servers_summary_still_uses_snapshot_fast_path(self) -> None:
snapshot = { snapshot = {
"generated_at": "2026-06-10T04:00:00Z", "generated_at": "2026-06-10T04:00:00Z",
"source_range_start": "2026-06-09T00:00:00Z", "source_range_start": "2026-06-09T00:00:00Z",
@@ -210,7 +264,7 @@ class HistoricalSnapshotRefreshTests(unittest.TestCase):
"payload": { "payload": {
"item": { "item": {
"server": {"slug": "all-servers", "name": "Todos los servidores"}, "server": {"slug": "all-servers", "name": "Todos los servidores"},
"matches_count": 12, "matches_count": 20,
}, },
}, },
} }
@@ -224,9 +278,8 @@ class HistoricalSnapshotRefreshTests(unittest.TestCase):
rcon_loader.assert_not_called() rcon_loader.assert_not_called()
fallback_loader.assert_not_called() fallback_loader.assert_not_called()
self.assertEqual(payload["data"]["context"], "historical-server-summary") self.assertEqual(payload["data"]["server_slug"], "all-servers")
self.assertEqual(payload["data"]["legacy_endpoint_policy"], "snapshot-read-only-fast-path") self.assertEqual(payload["data"]["items"][0]["matches_count"], 20)
self.assertEqual(payload["data"]["items"][0]["matches_count"], 12)
def test_rcon_coverage_accepts_postgres_datetime_values(self) -> None: def test_rcon_coverage_accepts_postgres_datetime_values(self) -> None:
start = datetime(2026, 5, 21, 10, 0, tzinfo=timezone.utc) start = datetime(2026, 5, 21, 10, 0, tzinfo=timezone.utc)

View File

@@ -231,6 +231,54 @@ Comando de validacion de produccion tras redeploy:
python .\scripts\audit_public_requests.py --base-url https://comunidadhll.devzamode.es --timeout 30 --filter servers --output tmp\task229_servers_recheck_after.json python .\scripts\audit_public_requests.py --base-url https://comunidadhll.devzamode.es --timeout 30 --filter servers --output tmp\task229_servers_recheck_after.json
``` ```
## Estado post-fix TASK-230
Fecha: 2026-06-10
Alcance aplicado: ultimo `CRITICAL` de la auditoria global posterior a `TASK-229`.
URL afectada:
- `GET /api/historical/server-summary?server=comunidad-hispana-01`
Evidencia de produccion previa al fix:
- Probe: `historical-server-summary-comunidad-hispana-01`.
- HTTP 200.
- `10120.89 ms`.
- `fallback=False`.
- `historical-server-summary-comunidad-hispana-02` respondia en `139.37 ms`.
- El snapshot equivalente de CH01 respondia en `42.14 ms`.
Causa confirmada:
- `scripts/audit_public_requests.py` etiqueta la ruta como `historical-server-summary-comunidad-hispana-01`.
- `backend/app/routes.py` la mapea a `build_historical_server_summary_payload(server_slug="comunidad-hispana-01")`.
- Tras `TASK-229`, solo `server=all-servers` tenia snapshot fast-path.
- CH01 y CH02 seguian entrando en `get_rcon_historical_read_model().list_server_summaries(...)`.
- El read model RCON construye cada resumen con `_build_server_summary()`, que llama a `list_rcon_historical_recent_activity(server_key=..., limit=1)` para enriquecer actividad reciente.
- Esa lectura intenta materialized RCON/AdminLog (`list_materialized_rcon_matches`) antes del fallback de ventanas. En CH01 ese camino fue lento aunque exitoso (`fallback=False`); CH02 uso el mismo flujo pero con coste bajo.
Cambios aplicados:
- `build_historical_server_summary_payload()` usa snapshot fast-path para cualquier `server_slug` explicito.
- CH01, CH02 y `all-servers` quedan como wrappers legacy sobre `build_historical_server_summary_snapshot_payload()`.
- El contrato conserva `context: historical-server-summary`, `items`, `summary_basis`, `weekly_ranking_window_days` y `legacy_endpoint_policy: snapshot-read-only-fast-path`.
- Si falta snapshot, responde JSON controlado con `items: []`, sin RCON live, sin scoreboard externo, sin `initialize_*` y sin fallback runtime pesado.
Estado esperado tras redeploy:
| Endpoint | Estado TASK-230 en codigo | Severidad esperada tras deploy |
| --- | --- | --- |
| `/api/historical/server-summary?server=comunidad-hispana-01` | Wrapper legacy sobre snapshot read-only | OK o WARNING si snapshot missing, sin timeout |
| `/api/historical/server-summary?server=comunidad-hispana-02` | Wrapper legacy sobre snapshot read-only | OK o WARNING si snapshot missing, sin empeorar |
| `/api/historical/server-summary?server=all-servers` | Wrapper legacy sobre snapshot read-only | OK o WARNING si snapshot missing, sin empeorar |
Comando de validacion de produccion tras redeploy:
```powershell
python .\scripts\audit_public_requests.py --base-url https://comunidadhll.devzamode.es --timeout 30 --output tmp\full_audit_after_task230.json
```
## Evidencia ejecutada ## Evidencia ejecutada
Comandos ejecutados: Comandos ejecutados:
@@ -361,7 +409,7 @@ La tabla usa rangos cuando una ruta se lanzo con varias combinaciones representa
| 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 | | 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 | | 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 | | 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` | 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 | | M032 | backend-api | `backend/app/routes.py` | historico server summary legacy | GET | `/api/historical/server-summary` | `server` | `/api/historical/server-summary?server=comunidad-hispana-01` | `build_historical_server_summary_payload` | snapshot server summary | wrapper legacy sobre snapshot para cualquier `server=` explicito | no | no | bajo | n/a | pendiente redeploy TASK-230 | 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 | | 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 | | 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 | | 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 |

View File

@@ -14,6 +14,8 @@ Actualizacion TASK-228, 2026-06-10: `/api/servers` deja de ser cache-only estric
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. 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.
Actualizacion TASK-230, 2026-06-10: el ultimo `CRITICAL` de la auditoria global fue `/api/historical/server-summary?server=comunidad-hispana-01`, con HTTP 200, `fallback=False` y ~10.1 s. La causa estaba en el path legacy por servidor, que seguia entrando en el read model RCON y enriquecia el resumen con actividad materializada reciente. `server-summary` legacy pasa a usar snapshot fast-path para cualquier `server=` explicito (`all-servers`, `comunidad-hispana-01`, `comunidad-hispana-02`), manteniendo contrato compatible con `items` y metadata de snapshot.
Conclusiones principales: 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. - 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.
@@ -50,7 +52,7 @@ Flujo observado:
| `/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/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 | Snapshot precomputado read-only | No | 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/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/server-summary?server=<scope>` | legacy historico audit | `build_historical_server_summary_payload()` | Snapshot publico de server summary | Wrapper legacy sobre snapshot precomputado para cualquier `server=` explicito | 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/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` | `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 | | `/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 |