Add full public request audit

This commit is contained in:
devRaGonSa
2026-06-10 19:04:29 +02:00
parent a2d6951bee
commit 97234179c8
3 changed files with 1229 additions and 0 deletions

View File

@@ -0,0 +1,167 @@
---
id: TASK-224
title: Full application request audit
status: done
type: research
team: Analista
supporting_teams:
- Backend Senior
- Frontend Senior
- Arquitecto Python
- Arquitecto de Base de Datos
roadmap_item: foundation
priority: high
---
# TASK-224 - Full application request audit
## Goal
Discover, classify, measure and document all relevant public application requests across frontend and backend/API without applying functional fixes.
## Context
Recent public ranking/snapshot optimizations were followed by a regression in `historico-partida.html` and `/api/historical/matches/detail`. Production profiling showed the real bottleneck was storage initialization during a public read path:
- `get_rcon_historical_match_detail()`: 14.193s
- `get_materialized_rcon_match_detail()`: 14.167s
- `initialize_postgres_rcon_storage()`: 14.147s
- `initialize_rcon_materialized_storage()`: 14.133s
- storage read after initialization: 0.224s
- payload build: 0.014s
This task audits the complete public request surface to find other endpoints with similar cold-read initialization, DDL/bootstrap, direct live network calls or heavyweight fallback behavior.
## Steps
1. Inspect repository context, architecture notes and relevant orchestrator role guidance.
2. Review `backend/app/routes.py` and extract all public GET routes.
3. Review `backend/app/payloads.py` and storage/read-model modules reached by payload builders.
4. Search backend for `initialize_*`, `ensure_*`, DDL, migration, bootstrap, fallback, direct RCON and external HTTP paths.
5. Review public frontend HTML and JavaScript request construction.
6. Create an executable public request audit script using stdlib only.
7. Run the audit against production when available and document measured results.
8. Write the full request matrix and prioritized follow-up tasks.
## Files to Read First
- `AGENTS.md`
- `ai/architecture-index.md`
- `ai/repo-context.md`
- `ai/orchestrator/analyst.md`
- `ai/orchestrator/backend-senior.md`
- `ai/orchestrator/frontend-senior.md`
- `backend/app/routes.py`
- `backend/app/payloads.py`
- `frontend/*.html`
- `frontend/assets/js/*.js`
## Expected Files to Modify
- `ai/tasks/done/TASK-224-full-application-request-audit.md`
- `docs/FULL_APPLICATION_REQUEST_AUDIT.md`
- `scripts/audit_public_requests.py`
- `tmp/public_request_audit.json`
- `tmp/audit_prod_stdout.txt`
- `tmp/audit_prod_stderr.txt`
## Constraints
- Do not run `ai-platform run`.
- Do not commit.
- Do not push.
- Do not apply functional fixes.
- Do not optimize production code except for audit tooling.
- Do not touch weapon assets, clan assets, SVGs or physical images.
- Do not modify `ai/system-metrics.md`.
- Do not reactivate Elo/MMR.
- Do not add unrelated prior changes.
## Validation
Executed:
```powershell
python -m compileall backend\app
python -m py_compile scripts\audit_public_requests.py
cd backend
python -m unittest tests.test_rcon_materialization_pipeline
```
Results:
- `python -m compileall backend\app`: passed.
- `python -m py_compile scripts\audit_public_requests.py`: passed.
- `python -m unittest tests.test_rcon_materialization_pipeline`: passed, 9 tests in 0.588s. Existing tests emitted `ResourceWarning` for unclosed SQLite connections but did not fail.
Runtime audit:
- Local backend health check against `http://127.0.0.1:8000/health`: unavailable from host, so local endpoint matrix was not launched.
- Production audit against `https://comunidadhll.devzamode.es`: launched 191 probes and wrote `tmp/public_request_audit.json`.
- Manual player-dependent probes launched after deriving `player_id=76561198092154180` from ranking data.
Production automatic summary:
- OK: 77
- WARNING: 110
- CRITICAL: 4
- HTTP 200: 187
- HTTP 500: 1
- timeout/error without HTTP status: 3
Manual player-dependent summary:
- OK: 0
- WARNING: 2
- CRITICAL: 2
Combined measured summary:
- OK: 77
- WARNING: 112
- CRITICAL: 6
## Outcome
Created `docs/FULL_APPLICATION_REQUEST_AUDIT.md` with:
- Executive summary.
- Backend route inventory.
- Frontend fetch inventory.
- Full request matrix with public backend routes, frontend fetches, static assets and internal runners.
- Endpoint severity tables.
- Initialization-in-read detection.
- Heavy fallback detection.
- Frontend loading/error/timeout audit.
- Top risks and prioritized recommendations.
- Proposed follow-up tasks.
- Exact commands to rerun the audit locally, in production and from the backend container.
Created `scripts/audit_public_requests.py` with:
- Configurable base URL and timeout.
- Representative public GET probes.
- HTTP status, elapsed time, response size and JSON metadata extraction.
- Non-fatal endpoint failures.
- JSON output to `tmp/public_request_audit.json` by default.
- Static discovery for backend routes, frontend API literals, `fetch(` occurrences and localhost references.
Key findings:
1. `/api/stats/players/search` times out at 30s in production.
2. `/api/stats/players/{player_id}` weekly/monthly times out at 30s in production.
3. `/api/current-match/kills` times out or returns 500.
4. `/api/current-match/players` times out for `comunidad-hispana-01`.
5. `/api/servers` can spend 4.3s refreshing RCON live data during a public GET.
6. `/api/historical/matches/detail` responded OK for the known real match in this audit, but still has a static initialization-in-read chain and needs later hardening.
7. 13 frontend localhost/127.0.0.1 references remain in public HTML/JS, mitigated by `config.js` but still worth hardening.
Next recommended fix:
1. Fix `/api/stats/players/search` and `/api/stats/players/{player_id}` first by making the public read path strict read-only, without `initialize_*` or runtime fallback.
2. Fix `/api/current-match/kills` and `/api/current-match/players` next.
3. Harden `/api/historical/matches/detail` after those, unless new profiling shows it regressed again.
## Change Budget
Files changed by this task are audit/documentation/support files only. No product logic files were modified.

View File

@@ -0,0 +1,386 @@
# Full Application Request Audit
Fecha: 2026-06-10
Task: `TASK-224-full-application-request-audit`
Alcance: auditoria de peticiones publicas frontend/backend/API sin aplicar fixes funcionales.
## Resumen ejecutivo
La auditoria encontro 36 patrones HTTP publicos en `backend/app/routes.py`, 12 ocurrencias `fetch(` en JavaScript publico, 15 literales `/api/...` en frontend y 13 referencias `localhost`/`127.0.0.1` en HTML/JS publico. No se encontro `XMLHttpRequest`.
Se ejecuto la auditoria contra `https://comunidadhll.devzamode.es` con 191 probes automaticos y 4 probes manuales dependientes de `player_id`. Resultado combinado:
| Severidad | Cantidad |
| --- | ---: |
| OK | 77 |
| WARNING | 112 |
| CRITICAL | 6 |
Los fallos criticos actuales no estan en rankings/snapshots ni en el match detail historico probado. Estan en:
- `/api/stats/players/search?q=Medu&server_id=all&limit=10`: timeout a 30s.
- `/api/stats/players/{player_id}?timeframe=weekly|monthly&server_id=all`: timeout a 30s.
- `/api/current-match/kills?server=comunidad-hispana-01&limit=30`: timeout a 30s.
- `/api/current-match/players?server=comunidad-hispana-01`: timeout a 30s.
- `/api/current-match/kills?server=comunidad-hispana-02&limit=30`: 500 en una ejecucion y timeout en una repeticion manual.
`/api/historical/matches/detail` con el match real `comunidad-hispana-01:1781023156:1781028555:purpleheartlanewarfare` respondio `200` en `120.47 ms` y `125580 B`. Sigue teniendo deuda de inicializacion en lectura en la cadena estatica, pero ya no es el endpoint publico mas critico segun la medicion de produccion de esta auditoria.
Siguiente fix prioritario recomendado: aislar o eliminar inicializacion/fallback runtime de `/api/stats/players/search` y `/api/stats/players/{player_id}`. Despues, corregir `/api/current-match/kills` y `/api/current-match/players`. `/api/historical/matches/detail` debe quedar en cola de hardening para hacerlo estrictamente read-only, pero no aparece como el primer incendio operativo en esta medicion.
## Evidencia ejecutada
Comandos ejecutados:
```powershell
python -m compileall backend\app
python -m py_compile scripts\audit_public_requests.py
cd backend
python -m unittest tests.test_rcon_materialization_pipeline
```
Resultado:
- `compileall`: OK.
- `py_compile`: OK.
- `unittest`: OK, 9 tests en 0.588s. El test existente emitio `ResourceWarning` por conexiones SQLite sin cerrar, sin fallo de test.
Auditoria local:
- `http://127.0.0.1:8000/health` no estaba disponible desde el host, por lo que no se lanzo la matriz completa local.
Auditoria produccion:
```powershell
python scripts\audit_public_requests.py --base-url https://comunidadhll.devzamode.es --timeout 30 --output tmp\public_request_audit.json
```
Resultado automatico:
- Probes lanzados: 191.
- `OK`: 77.
- `WARNING`: 110.
- `CRITICAL`: 4.
- HTTP 200: 187.
- HTTP 500: 1.
- Timeout/error sin status: 3.
- Warnings con `fallback_used=true`: 109.
- Snapshots con `snapshot_status=missing`: 75.
Probes manuales adicionales dependientes de `player_id`:
| Endpoint | Resultado | Tiempo | Severidad |
| --- | ---: | ---: | --- |
| `/api/stats/players/76561198092154180?timeframe=weekly&server_id=all` | timeout | 30094.15 ms | CRITICAL |
| `/api/stats/players/76561198092154180?timeframe=monthly&server_id=all` | timeout | 30046.21 ms | CRITICAL |
| `/api/historical/player-profile?player=76561198092154180` | 200 | 4736.64 ms | WARNING |
| `/api/historical/elo-mmr/player?server=all-servers&player=76561198092154180` | 200 | 46.87 ms | WARNING |
El `player_id` se obtuvo desde `/api/ranking?timeframe=weekly&server_id=all&metric=kills&limit=1`.
## Inventario backend
Rutas HTTP publicas extraidas de `backend/app/routes.py`: 36 patrones.
| ID | Metodo | Path | Handler/payload builder | Parametros | Fuente/modelo esperado |
| --- | --- | --- | --- | --- | --- |
| B001 | GET | `/health` | `build_health_payload` | ninguno | config/runtime status |
| B002 | GET | `/api/community` | `build_community_payload` | ninguno | payload estatico |
| B003 | GET | `/api/trailer` | `build_trailer_payload` | ninguno | payload estatico |
| B004 | GET | `/api/discord` | `build_discord_payload` | ninguno | payload estatico |
| B005 | GET | `/api/servers` | `build_servers_payload` | ninguno | latest snapshots; si stale refresca RCON/A2S |
| B006 | GET | `/api/servers/latest` | `build_server_latest_payload` | ninguno | `list_latest_snapshots` |
| B007 | GET | `/api/servers/history` | `build_server_history_payload` | `limit` | `list_snapshot_history` |
| B008 | GET | `/api/servers/{id}/history` | `build_server_detail_history_payload` | `id`, `limit` | `list_server_history` |
| B009 | GET | `/api/stats/players/search` | `build_stats_player_search_payload` | `q`, `server_id/server`, `limit` | player search read model; fallback runtime RCON materialized |
| B010 | GET | `/api/stats/rankings/annual` | `build_annual_ranking_snapshot_payload` | `year`, `metric=kills`, `server_id/server`, `limit` | annual ranking snapshot |
| B011 | GET | `/api/ranking` | `build_global_ranking_payload` | `timeframe`, `server_id/server`, `metric`, `limit`, `year` for annual | ranking snapshots; annual snapshots |
| B012 | GET | `/api/current-match` | `build_current_match_payload` | `server` | direct RCON sample; fallback `/api/servers` path |
| B013 | GET | `/api/current-match/kills` | `build_current_match_kill_feed_payload` | `server`, `limit`, `since_event_id` | AdminLog storage |
| B014 | GET | `/api/current-match/players` | `build_current_match_player_stats_payload` | `server` | AdminLog storage |
| B015 | GET | `/api/stats/players/{player_id}` | `build_stats_player_profile_payload` | `player_id`, `timeframe`, `server_id/server` | player period stats read model; fallback runtime |
| B016 | GET | `/api/historical/weekly-top-kills` | `build_weekly_top_kills_payload` | `limit`, `server` | legacy historical storage |
| B017 | GET | `/api/historical/leaderboard` | `build_historical_leaderboard_payload` | `limit`, `server`, `metric`, `timeframe` | RCON read model plus public-scoreboard fallback |
| B018 | GET | `/api/historical/weekly-leaderboard` | `build_weekly_leaderboard_payload` | `limit`, `server`, `metric` | legacy weekly storage |
| B019 | GET | `/api/historical/monthly-leaderboard` | `build_monthly_leaderboard_payload` | `limit`, `server`, `metric` | legacy monthly storage |
| B020 | GET | `/api/historical/monthly-mvp` | `build_monthly_mvp_payload` | `limit`, `server` | legacy monthly storage |
| B021 | GET | `/api/historical/monthly-mvp-v2` | `build_monthly_mvp_v2_payload` | `limit`, `server` | legacy monthly storage |
| B022 | GET | `/api/historical/player-events` | `build_player_event_payload` | `limit`, `server`, `view` | legacy player event storage |
| B023 | GET | `/api/historical/snapshots/leaderboard` | `build_leaderboard_snapshot_payload` | `limit`, `server`, `metric`, `timeframe` | displayed historical snapshots |
| B024 | GET | `/api/historical/snapshots/monthly-leaderboard` | `build_monthly_leaderboard_snapshot_payload` | `limit`, `server`, `metric` | displayed historical snapshots |
| B025 | GET | `/api/historical/snapshots/monthly-mvp` | `build_monthly_mvp_snapshot_payload` | `limit`, `server` | displayed historical snapshots |
| B026 | GET | `/api/historical/snapshots/monthly-mvp-v2` | `build_monthly_mvp_v2_snapshot_payload` | `limit`, `server` | displayed historical snapshots |
| B027 | GET | `/api/historical/snapshots/player-events` | `build_player_event_snapshot_payload` | `limit`, `server`, `view` | displayed historical snapshots |
| B028 | GET | `/api/historical/snapshots/weekly-leaderboard` | `build_weekly_leaderboard_snapshot_payload` | `limit`, `server`, `metric` | displayed historical snapshots |
| B029 | GET | `/api/historical/recent-matches` | `build_recent_historical_matches_payload` | `limit`, `server` | RCON read model plus scoreboard merge |
| B030 | GET | `/api/historical/snapshots/recent-matches` | `build_recent_historical_matches_snapshot_payload` | `limit`, `server` | displayed historical snapshots |
| B031 | GET | `/api/historical/matches/detail` | `build_historical_match_detail_payload` | `server`, `match` | RCON materialized detail; fallback scoreboard detail |
| B032 | GET | `/api/historical/server-summary` | `build_historical_server_summary_payload` | `server` | RCON read model plus fallback |
| B033 | GET | `/api/historical/snapshots/server-summary` | `build_historical_server_summary_snapshot_payload` | `server` | displayed historical snapshots |
| B034 | GET | `/api/historical/player-profile` | `build_historical_player_profile_payload` | `player` | legacy historical profile |
| B035 | GET | `/api/historical/elo-mmr/leaderboard` | `build_elo_mmr_leaderboard_payload` | `limit`, `server` | Elo/MMR engine/read storage, currently fallback/paused in public payload |
| B036 | GET | `/api/historical/elo-mmr/player` | `build_elo_mmr_player_payload` | `player`, `server` | Elo/MMR engine/read storage, currently fallback/paused in public payload |
## Matriz completa de peticiones
La tabla usa rangos cuando una ruta se lanzo con varias combinaciones representativas. El JSON probe-a-probe esta en `tmp/public_request_audit.json`.
| ID | Tipo | Archivo origen | Pagina/contexto | Metodo | Endpoint/path | Parametros requeridos | Ejemplo de URL real | Handler/backend function | Fuente de datos | Usa snapshot/read-model/materialized/legacy/RCON/directo | Ejecuta initialize/ensure/bootstrap en lectura publica | Riesgo fallback pesado | Riesgo timeout | Loading/error frontend | Validacion ejecutada | Resultado HTTP | Tiempo medido | Tamano respuesta | Observaciones | Severidad | Recomendacion |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| M001 | backend-api | `backend/app/routes.py` | health | GET | `/health` | ninguno | `/health` | `build_health_payload` | config | status directo | no | no | bajo | n/a | produccion | 200 | 210.37 ms | 264 B | OK | OK | mantener |
| M002 | backend-api | `backend/app/routes.py` | landing metadata | GET | `/api/community` | ninguno | `/api/community` | `build_community_payload` | estatico | directo | no | no | bajo | n/a | produccion | 200 | 22.33 ms | 203 B | OK | OK | mantener |
| M003 | backend-api | `backend/app/routes.py` | trailer | GET | `/api/trailer` | ninguno | `/api/trailer` | `build_trailer_payload` | estatico | directo | no | no | bajo | main usa error controlado | produccion | 200 | 33.08 ms | 139 B | OK | OK | mantener |
| M004 | backend-api | `backend/app/routes.py` | discord | GET | `/api/discord` | ninguno | `/api/discord` | `build_discord_payload` | estatico | directo | no | no | bajo | n/a | produccion | 200 | 47.26 ms | 137 B | OK | OK | mantener |
| M005 | backend-api | `backend/app/routes.py` | server cards | GET | `/api/servers` | ninguno | `/api/servers` | `build_servers_payload` | latest snapshots + live refresh | snapshot/RCON/A2S | no DDL, pero red externa en lectura si stale | si | medio | main tiene fallback visual | produccion | 200 | 4311.38 ms | 1751 B | `source=real-time-rcon-refresh` | WARNING | servir snapshot estricto y mover refresh fuera de request |
| M006 | backend-api | `backend/app/routes.py` | server history | GET | `/api/servers/latest` | ninguno | `/api/servers/latest` | `build_server_latest_payload` | local snapshot storage | snapshot legacy | posible `initialize_storage` por storage local | no | bajo | n/a | produccion | 200 | 42.17 ms | 184 B | OK | OK | mantener |
| M007 | backend-api | `backend/app/routes.py` | server history | GET | `/api/servers/history` | `limit` | `/api/servers/history?limit=20` | `build_server_history_payload` | local snapshot storage | snapshot legacy | posible `initialize_storage` por storage local | no | bajo | n/a | produccion | 200 | 40.59 ms | 167 B | OK | OK | mantener |
| M008 | backend-api | `backend/app/routes.py` | server history detail | GET | `/api/servers/{id}/history` | `id`, `limit` | `/api/servers/comunidad-hispana-01/history?limit=20` | `build_server_detail_history_payload` | local snapshot storage | snapshot legacy | posible `initialize_storage` por storage local | no | bajo | n/a | produccion | 200 | 46.53-78.34 ms | 194 B | OK | OK | mantener |
| M009 | backend-api | `backend/app/routes.py` | stats search | GET | `/api/stats/players/search` | `q`, `server_id/server`, `limit` | `/api/stats/players/search?q=Medu&server_id=all&limit=10` | `build_stats_player_search_payload` | player search index | read-model + runtime materialized fallback | si: `initialize_player_search_index_storage` -> `initialize_rcon_materialized_storage` -> `initialize_postgres_rcon_storage` | si | alto | stats puede quedar esperando hasta timeout navegador | produccion | timeout | 30019.46 ms | 0 B | probe CRITICAL | CRITICAL | primer fix: read-only estricto, sin inicializar ni fallback runtime |
| M010 | backend-api | `backend/app/routes.py` | stats annual ranking | GET | `/api/stats/rankings/annual` | `year`, `metric=kills`, `server_id/server`, `limit` | `/api/stats/rankings/annual?year=2026&metric=kills&server_id=all&limit=10` | `build_annual_ranking_snapshot_payload` | annual ranking snapshot | snapshot | no en lectura normal | no | bajo | stats tiene error controlado | produccion | 200 | 60.07 ms | 5236 B | OK | OK | mantener |
| M011 | backend-api | `backend/app/routes.py` | ranking | GET | `/api/ranking` | `timeframe`, `server_id/server`, `metric`, `limit`, `year` anual | `/api/ranking?timeframe=weekly&server_id=all&metric=kills&limit=20` | `build_global_ranking_payload` | ranking snapshots | snapshot/read-model | no en lectura snapshot; fallback runtime opcional por env | si si env fallback esta activo | bajo | ranking usa AbortController/requestId | produccion | 200 | 31.40-129.56 ms | 755-5668 B | 55 probes OK | OK | mantener fallback runtime desactivado en publico |
| M012 | backend-api | `backend/app/routes.py` | partida actual resumen | GET | `/api/current-match` | `server` | `/api/current-match?server=comunidad-hispana-01` | `build_current_match_payload` | RCON live | directo RCON + fallback `/api/servers` | no DDL | si, por fallback live | medio | partida actual tiene in-flight guard, sin timeout | produccion | 200 | 1242.08-2155.20 ms | 808-820 B | OK pero red externa en request | OK | mover a snapshot/read-model publico |
| M013 | backend-api | `backend/app/routes.py` | partida actual kills | GET | `/api/current-match/kills` | `server`, `limit`, `since_event_id` | `/api/current-match/kills?server=comunidad-hispana-01&limit=30` | `build_current_match_kill_feed_payload` | AdminLog | materialized AdminLog | si: `initialize_rcon_admin_log_storage` -> `initialize_postgres_rcon_storage` | desconocido | alto | polling 1.5s con in-flight guard, sin timeout | produccion | timeout/500 | 6268.78-30024.86 ms | 0-58 B | CH01 timeout, CH02 500 y luego timeout manual | CRITICAL | segundo fix: read-only connection y query/index audit |
| M014 | backend-api | `backend/app/routes.py` | partida actual jugadores | GET | `/api/current-match/players` | `server` | `/api/current-match/players?server=comunidad-hispana-01` | `build_current_match_player_stats_payload` | AdminLog | materialized AdminLog | si: `initialize_rcon_admin_log_storage` -> `initialize_postgres_rcon_storage` | desconocido | alto | polling 3s con in-flight guard, sin timeout | produccion | timeout/200 | 1090.32-30096.62 ms | 0-72280 B | CH01 timeout; CH02 OK | CRITICAL | segundo fix junto con kills |
| M015 | backend-api | `backend/app/routes.py` | stats player detail | GET | `/api/stats/players/{player_id}` | `player_id`, `timeframe`, `server_id/server` | `/api/stats/players/76561198092154180?timeframe=weekly&server_id=all` | `build_stats_player_profile_payload` | player period stats | read-model + runtime fallback | si: `initialize_player_period_stats_storage` -> `initialize_rcon_materialized_storage` -> `initialize_postgres_rcon_storage` | si | alto | stats sin AbortController/timeout | manual produccion | timeout | 30046.21-30094.15 ms | 0 B | weekly/monthly CRITICAL | CRITICAL | primer fix junto con player search |
| M016 | backend-api | `backend/app/routes.py` | legacy historical top kills | GET | `/api/historical/weekly-top-kills` | `limit`, `server` | `/api/historical/weekly-top-kills?server=all-servers&limit=10` | `build_weekly_top_kills_payload` | historical storage | legacy/fallback | si en Postgres display fallback paths | si | bajo | n/a | produccion | 200 | 53.72-75.73 ms | 806 B | `fallback_used=true` | WARNING | convertir a snapshot estricto o marcar legacy |
| M017 | backend-api | `backend/app/routes.py` | legacy historical leaderboard | GET | `/api/historical/leaderboard` | `limit`, `server`, `metric`, `timeframe` | `/api/historical/leaderboard?server=all-servers&timeframe=weekly&metric=kills&limit=10` | `build_historical_leaderboard_payload` | RCON/historical storage | read-model + legacy fallback | si en legacy display paths | si | bajo | n/a | produccion | 200 | 38-132 ms | ~1375-1414 B | `fallback_used=true` en muchos probes | WARNING | documentar deprecacion o hacer snapshot-only |
| M018 | backend-api | `backend/app/routes.py` | legacy weekly leaderboard | GET | `/api/historical/weekly-leaderboard` | `limit`, `server`, `metric` | `/api/historical/weekly-leaderboard?server=comunidad-hispana-01&metric=kills&limit=10` | `build_weekly_leaderboard_payload` | historical storage | legacy | si en Postgres display paths | si | bajo | n/a | produccion | 200 | 55-58 ms | ~1380 B | fallback | WARNING | snapshot-only o mantener como legacy controlado |
| M019 | backend-api | `backend/app/routes.py` | legacy monthly leaderboard | GET | `/api/historical/monthly-leaderboard` | `limit`, `server`, `metric` | `/api/historical/monthly-leaderboard?server=comunidad-hispana-02&metric=kills&limit=10` | `build_monthly_leaderboard_payload` | historical storage | legacy | si en Postgres display paths | si | bajo | n/a | produccion | 200 | 57-116 ms | ~1360 B | fallback | WARNING | snapshot-only o mantener como legacy controlado |
| M020 | backend-api | `backend/app/routes.py` | monthly MVP | GET | `/api/historical/monthly-mvp` | `limit`, `server` | `/api/historical/monthly-mvp?server=all-servers&limit=10` | `build_monthly_mvp_payload` | historical storage | legacy | si en Postgres display paths | si | bajo | n/a | produccion | 200 | 43-58 ms | 1470 B | fallback | WARNING | snapshot-only |
| M021 | backend-api | `backend/app/routes.py` | monthly MVP V2 | GET | `/api/historical/monthly-mvp-v2` | `limit`, `server` | `/api/historical/monthly-mvp-v2?server=all-servers&limit=10` | `build_monthly_mvp_v2_payload` | historical storage | legacy | si en Postgres display paths | si | bajo | n/a | produccion | 200 | 40-72 ms | 1509 B | fallback | WARNING | snapshot-only |
| M022 | backend-api | `backend/app/routes.py` | player events | GET | `/api/historical/player-events` | `limit`, `server`, `view` | `/api/historical/player-events?server=comunidad-hispana-01&view=duels&limit=10` | `build_player_event_payload` | player event storage | legacy | posible initialize legacy storage | si | bajo | n/a | produccion | 200 | 38-67 ms | ~1144-1156 B | fallback | WARNING | snapshot-only |
| M023 | backend-api | `backend/app/routes.py` | snapshot leaderboard | GET | `/api/historical/snapshots/leaderboard` | `limit`, `server`, `metric`, `timeframe` | `/api/historical/snapshots/leaderboard?server=all-servers&timeframe=weekly&metric=kills&limit=10` | `build_leaderboard_snapshot_payload` | displayed snapshots | snapshot | si en Postgres: `get_snapshot` -> `initialize_postgres_display_storage` | no pesado hoy | bajo | historico muestra fallback visual | produccion | 200 | 40-110 ms | ~1596-1646 B | muchos `snapshot_status=missing` | WARNING | quitar DDL/init de `get_snapshot`; completar snapshots |
| M024 | backend-api | `backend/app/routes.py` | snapshot monthly leaderboard | GET | `/api/historical/snapshots/monthly-leaderboard` | `limit`, `server`, `metric` | `/api/historical/snapshots/monthly-leaderboard?server=comunidad-hispana-02&metric=kills&limit=10` | `build_monthly_leaderboard_snapshot_payload` | displayed snapshots | snapshot | si en Postgres display | no pesado hoy | bajo | n/a | produccion | 200 | 41-116 ms | ~1612 B | snapshot missing/fallback | WARNING | completar snapshots y read-only |
| M025 | backend-api | `backend/app/routes.py` | snapshot monthly MVP | GET | `/api/historical/snapshots/monthly-mvp` | `limit`, `server` | `/api/historical/snapshots/monthly-mvp?server=all-servers&limit=10` | `build_monthly_mvp_snapshot_payload` | displayed snapshots | snapshot | si en Postgres display | no pesado hoy | bajo | n/a | produccion | 200 | 42-78 ms | 1506 B | snapshot missing/fallback | WARNING | completar snapshots |
| 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 |
| 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 |
| 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 |
| M036 | backend-api | `backend/app/routes.py` | Elo/MMR player | GET | `/api/historical/elo-mmr/player` | `player`, `server` | `/api/historical/elo-mmr/player?server=all-servers&player=76561198092154180` | `build_elo_mmr_player_payload` | Elo/MMR storage | legacy/paused | posible `initialize_elo_mmr_storage` si engine carga | si | bajo | no usado por frontend actual | manual produccion | 200 | 46.87 ms | no capturado | fallback true | WARNING | no reactivar; mantener fuera de UI |
| M037 | frontend-fetch | `frontend/assets/js/main.js` | `index.html` | GET | `/health` | ninguno | `${backendBaseUrl}/health` | backend B001 | config status | directo | no | no | bajo | usa `try/catch`; no bloquea pagina completa | codigo + produccion | 200 | 210.37 ms | 264 B | usado para estado backend | OK | mantener |
| M038 | frontend-fetch | `frontend/assets/js/main.js` | `index.html` | GET | `/api/trailer` | ninguno | `${backendBaseUrl}/api/trailer` | backend B003 | estatico | directo | no | no | bajo | error controlado | codigo + produccion | 200 | 33.08 ms | 139 B | OK | OK | mantener |
| M039 | frontend-fetch | `frontend/assets/js/main.js` | `index.html` | GET | `/api/servers` | ninguno | `${backendBaseUrl}/api/servers` | backend B005 | snapshots/live | RCON refresh posible | no DDL, si red live | si | medio | fallback visual; `Promise.allSettled` | codigo + produccion | 200 | 4311.38 ms | 1751 B | puede ralentizar landing | WARNING | no refrescar live en request publica |
| M040 | frontend-fetch | `frontend/assets/js/historico.js` | `historico.html` | GET | `/api/historical/snapshots/server-summary` | `server` | `/api/historical/snapshots/server-summary?server=comunidad-hispana-01` | backend B033 | snapshot | snapshot | si display init | no | bajo | requestId/cache/error | codigo + produccion | 200 | 50-63 ms | ~1042 B | snapshot missing/fallback | WARNING | completar snapshot y read-only |
| M041 | frontend-fetch | `frontend/assets/js/historico.js` | `historico.html` | GET | `/api/historical/snapshots/recent-matches` | `server`, `limit` | `/api/historical/snapshots/recent-matches?server=comunidad-hispana-01&limit=20` | backend B030 | snapshot | snapshot | si display init | no | bajo | requestId/cache/error | codigo + produccion | 200 | 48-82 ms | ~24 KB | OK | OK | mantener |
| M042 | frontend-fetch | `frontend/assets/js/historico.js` | `historico.html` | GET | `/api/historical/snapshots/leaderboard` | `server`, `timeframe`, `metric`, `limit` | `/api/historical/snapshots/leaderboard?server=comunidad-hispana-01&timeframe=weekly&metric=kills&limit=10` | backend B023 | snapshot | snapshot | si display init | no | bajo | requestId/cache/error | codigo + produccion | 200 | 40-110 ms | ~1.6 KB | snapshot missing/fallback | WARNING | completar snapshots |
| M043 | frontend-fetch | `frontend/assets/js/historico-recent-live.js` | `historico.html` | GET | `/api/historical/snapshots/recent-matches` | `server`, `limit` | `/api/historical/snapshots/recent-matches?server=comunidad-hispana-02&limit=20` | backend B030 | snapshot | snapshot | si display init | no | bajo | `try/catch`; cache no-store | codigo + produccion | 200 | 48-82 ms | ~24 KB | duplicable con historico.js | OK | revisar si ambos scripts piden lo mismo |
| M044 | frontend-fetch | `frontend/assets/js/historico-partida.js` | `historico-partida.html` | GET | `/api/historical/matches/detail` | `server`, `match` | `/api/historical/matches/detail?server=...&match=...` | backend B031 | materialized/fallback | materialized RCON | si | si | medio | error visual; sin timeout/abort | codigo + produccion | 200 | 120.47 ms | 125580 B | fallback localhost corregido previamente | OK | hardening read-only despues |
| M045 | frontend-fetch | `frontend/assets/js/partida-actual.js` | `partida-actual.html` | GET | `/api/current-match` | `server` | `/api/current-match?server=comunidad-hispana-01` | backend B012 | RCON live | directo RCON | no | si | medio | in-flight guard; sin timeout | codigo + produccion | 200 | 1242-2155 ms | ~820 B | polling | OK | mover a snapshot |
| M046 | frontend-fetch | `frontend/assets/js/partida-actual.js` | `partida-actual.html` | GET | `/api/current-match/kills` | `server`, `limit`, `since_event_id` | `/api/current-match/kills?server=comunidad-hispana-01&limit=30` | backend B013 | AdminLog | materialized | si | desconocido | alto | in-flight guard; sin timeout | codigo + produccion | timeout/500 | 6269-30025 ms | 0-58 B | polling frecuente | CRITICAL | corregir backend y agregar timeout frontend |
| M047 | frontend-fetch | `frontend/assets/js/partida-actual.js` | `partida-actual.html` | GET | `/api/current-match/players` | `server` | `/api/current-match/players?server=comunidad-hispana-01` | backend B014 | AdminLog | materialized | si | desconocido | alto | in-flight guard; sin timeout | codigo + produccion | timeout/200 | 1090-30097 ms | 0-72 KB | polling frecuente | CRITICAL | corregir backend y agregar timeout frontend |
| M048 | frontend-fetch | `frontend/assets/js/ranking.js` | `ranking.html` | GET | `/api/ranking` | `timeframe`, `server_id`, `metric`, `limit`, `year` | `/api/ranking?timeframe=weekly&server_id=all&metric=kills&limit=100` | backend B011 | ranking snapshots | snapshot | no en fast path | no | bajo | AbortController + requestId + error | codigo + produccion | 200 | 31-130 ms | variable | OK | OK | mantener |
| M049 | frontend-fetch | `frontend/assets/js/stats.js` | `stats.html` | GET | `/health` | ninguno | `${backendBaseUrl}/health` | backend B001 | config status | directo | no | no | bajo | error controlado | codigo + produccion | 200 | 210 ms | 264 B | OK | OK | mantener |
| M050 | frontend-fetch | `frontend/assets/js/stats.js` | `stats.html` | GET | `/api/stats/players/search` | `q`, opcional `server_id`, `limit` | `/api/stats/players/search?q=Medu&server_id=all&limit=10` | backend B009 | player read model | read-model/fallback | si | si | alto | sin abort/timeout; loading puede durar mucho | codigo + produccion | timeout | 30019 ms | 0 B | CRITICAL | CRITICAL | primer fix |
| M051 | frontend-fetch | `frontend/assets/js/stats.js` | `stats.html` | GET | `/api/stats/rankings/annual` | `year`, `metric`, `server_id`, `limit` | `/api/stats/rankings/annual?year=2026&metric=kills&server_id=all&limit=10` | backend B010 | annual snapshot | snapshot | no | no | bajo | error controlado | codigo + produccion | 200 | 60 ms | 5236 B | OK | OK | mantener |
| M052 | frontend-fetch | `frontend/assets/js/stats.js` | `stats.html` | GET | `/api/stats/players/{player_id}` | `player_id`, `timeframe`, `server_id` | `/api/stats/players/76561198092154180?timeframe=weekly&server_id=all` | backend B015 | player period stats | read-model/fallback | si | si | alto | sin abort/timeout; puede quedarse cargando hasta timeout navegador | codigo + manual produccion | timeout | 30046-30094 ms | 0 B | CRITICAL | CRITICAL | primer fix |
| M053 | static-asset | `frontend/*.html`, `frontend/assets/css`, `frontend/assets/js` | paginas publicas | GET | assets estaticos | path estatico | `/assets/js/ranking.js` | servidor estatico | filesystem/web server | n/a | no | no | bajo | navegador gestiona error de asset | no medida HTTP individual | n/a | n/a | n/a | no se tocaron assets | OK | mantener cache headers fuera de esta task |
| M054 | static-asset | `frontend/assets/img/**` | imagenes publicas | GET | assets imagen | path estatico | `/assets/img/...` | servidor estatico | filesystem/web server | n/a | no | no | bajo | navegador gestiona error de asset | no medida HTTP individual | n/a | n/a | n/a | no se tocaron imagenes, SVGs, weapons ni clans | OK | sin cambios |
| M055 | internal-runner | `backend/app/historical_runner.py` | worker historico | n/a | no publico | args CLI/env | n/a | runner | RCON/public scoreboard/storage | ingestion/fallback | si, por diseno de worker | si | n/a | n/a | lectura estatica | n/a | n/a | n/a | fuera de superficie publica | OK | no mezclar con fixes publicos |
| M056 | internal-runner | `backend/app/historical_ingestion.py` | ingestion historica | n/a | no publico | args CLI/env | n/a | runner | public scoreboard/RCON | ingestion/fallback | si, por diseno de worker | si | n/a | n/a | lectura estatica | n/a | n/a | n/a | fuera de superficie publica | OK | no ejecutar desde request publica |
| M057 | internal-runner | `backend/app/database_maintenance.py` | mantenimiento DB | n/a | no publico | args CLI/env | n/a | runner | DB | maintenance | si, por diseno de worker | n/a | n/a | n/a | lectura estatica | n/a | n/a | n/a | fuera de superficie publica | OK | mantener separado de GET |
| M058 | internal-runner | `scripts/audit_public_requests.py` | auditoria publica | GET | endpoints publicos | `--base-url`, `--timeout` | produccion/local | script stdlib | HTTP publico | n/a | no modifica app | no | configurable | n/a | ejecutado | 191 probes | ver JSON | ver JSON | artefacto de auditoria | OK | usar antes/despues de fixes |
## Endpoints OK
| Endpoint/familia | Evidencia |
| --- | --- |
| `/health`, `/api/community`, `/api/trailer`, `/api/discord` | 200, menos de 211 ms |
| `/api/servers/latest`, `/api/servers/history`, `/api/servers/{id}/history` | 200, 40-78 ms |
| `/api/ranking` semanal/mensual/anual, metricas `kills`, `deaths`, `teamkills`, `matches_considered`, `kd_ratio`, `kills_per_match`, scopes `all`, `comunidad-hispana-01`, `comunidad-hispana-02` | 55 probes OK, 31-130 ms |
| `/api/stats/rankings/annual` | 200, 60.07 ms |
| `/api/current-match` | 200, 1.2-2.2 s; OK funcional, riesgo arquitectonico por RCON directo |
| `/api/historical/recent-matches` | 200, 278-1286 ms |
| `/api/historical/snapshots/recent-matches` | 200, 48-82 ms |
| `/api/historical/matches/detail` con match conocido | 200, 120.47 ms |
| `/api/historical/server-summary` | 200, 95-145 ms |
## Endpoints WARNING
| Endpoint/familia | Motivo |
| --- | --- |
| `/api/servers` | 200 pero 4311.38 ms y `source=real-time-rcon-refresh`: red live durante lectura publica |
| `/api/historical/weekly-top-kills` | `fallback_used=true` |
| `/api/historical/leaderboard`, `/weekly-leaderboard`, `/monthly-leaderboard` | `fallback_used=true` y legacy fallback |
| `/api/historical/monthly-mvp`, `/monthly-mvp-v2` | `fallback_used=true` |
| `/api/historical/player-events` | `fallback_used=true` |
| `/api/historical/snapshots/*` salvo recent-matches | `snapshot_status=missing` frecuente, `fallback_used=true` |
| `/api/historical/player-profile` | 200 pero 4736.64 ms manual y fallback |
| `/api/historical/elo-mmr/*` | endpoints expuestos pero fallback/pausados; no reactivar ni poner en UI |
| frontend `data-backend-base-url`/fallback local | 13 referencias a localhost/127.0.0.1 en HTML/JS publico; `config.js` mitiga en produccion pero el residuo existe |
## Endpoints CRITICAL
| Endpoint | Evidencia | Recomendacion |
| --- | --- | --- |
| `/api/stats/players/search?q=Medu&server_id=all&limit=10` | timeout 30019.46 ms | read-only estricto del player search index; no inicializar storage ni fallback runtime en request publica |
| `/api/stats/players/{player_id}?timeframe=weekly&server_id=all` | timeout 30094.15 ms | igual que search; revisar `player_period_stats` |
| `/api/stats/players/{player_id}?timeframe=monthly&server_id=all` | timeout 30046.21 ms | igual que search; revisar `player_period_stats` |
| `/api/current-match/kills?server=comunidad-hispana-01&limit=30` | timeout 30024.86 ms | quitar init de AdminLog en lectura; revisar indices/query |
| `/api/current-match/players?server=comunidad-hispana-01` | timeout 30096.62 ms | quitar init de AdminLog en lectura; revisar ventana actual e indices |
| `/api/current-match/kills?server=comunidad-hispana-02&limit=30` | 500 en 6268.78 ms; repeticion manual hizo timeout | capturar error backend y estabilizar query |
## Inicializacion en lectura publica
Rutas donde una peticion publica puede acabar ejecutando inicializacion, DDL o bootstrap de storage:
| Ruta publica | Cadena | Tiempo observado/potencial | Estado | Recomendacion |
| --- | --- | --- | --- | --- |
| `/api/historical/matches/detail` | `build_historical_match_detail_payload` -> `get_rcon_historical_match_detail` -> `get_materialized_rcon_match_detail` -> `initialize_rcon_materialized_storage` -> `initialize_postgres_rcon_storage` | historico reciente midio 14s antes; esta auditoria 120 ms para match conocido | funcional pero no limpio | mantener como fix posterior: conexion read-only sin init y fallback estricto |
| `/api/stats/players/search` | `build_stats_player_search_payload` -> `search_rcon_materialized_players` -> `_search_player_search_index` -> `initialize_player_search_index_storage` -> `initialize_rcon_materialized_storage` -> `initialize_postgres_rcon_storage`; fallback `_search_rcon_materialized_players_runtime` | 30s timeout | CRITICAL | primer fix |
| `/api/stats/players/{player_id}` | `build_stats_player_profile_payload` -> `get_rcon_materialized_player_stats` -> `_get_player_period_stats_read_model` -> `initialize_player_period_stats_storage` -> `initialize_rcon_materialized_storage` -> `initialize_postgres_rcon_storage`; fallback runtime | 30s timeout | CRITICAL | primer fix junto a search |
| `/api/current-match/kills` | `build_current_match_kill_feed_payload` -> `list_current_match_kill_feed` -> `initialize_rcon_admin_log_storage` -> `initialize_postgres_rcon_storage` | 30s timeout / 500 | CRITICAL | segundo fix |
| `/api/current-match/players` | `build_current_match_player_stats_payload` -> `list_current_match_player_stats` -> `initialize_rcon_admin_log_storage` -> `initialize_postgres_rcon_storage` | 30s timeout en CH01 | CRITICAL | segundo fix |
| `/api/historical/snapshots/*` | snapshot builder -> `get_historical_snapshot` -> Postgres `get_snapshot` -> `initialize_postgres_display_storage` | rapido hoy, 40-116 ms | WARNING | read-only snapshot connection |
| legacy `/api/historical/*` | payloads -> `historical_storage` -> Postgres display fallback functions | rapido hoy salvo player-profile 4.7s | WARNING | snapshot-only o legacy explicit |
| `/api/ranking` | weekly/monthly fast path uses read-only snapshot connection; `initialize_ranking_snapshot_storage` solo deberia ocurrir en generation/fallback. Runtime fallback existe por env | 31-130 ms | OK con condicion | mantener fallback runtime apagado en publico |
| annual ranking | `get_annual_ranking_snapshot` usa read connection | 31-88 ms | OK | mantener |
## Fallbacks pesados detectados
| Endpoint | Fallback | Activacion | Coste estimado | Lo muestra frontend | Recomendacion |
| --- | --- | --- | --- | --- | --- |
| `/api/stats/players/search` | runtime search en `rcon_match_player_stats` | indice vacio/no disponible/error | alto, timeout 30s | no claramente | eliminar fallback runtime en publico o devolver estado `snapshot_missing` rapido |
| `/api/stats/players/{player_id}` | runtime player stats desde materialized matches | `player_period_stats` vacio/no disponible | alto, timeout 30s | no claramente | eliminar fallback runtime publico |
| `/api/current-match/kills/players` | AdminLog init + queries sobre ventana actual | siempre entra por helper actual | alto en CH01 | UI queda esperando | read-only + indices; respuesta vacia rapida si no hay ventana |
| `/api/servers` | RCON live refresh y A2S fallback si snapshots stale | snapshots ausentes/stale | medio, 4.3s medido | no como fallback tecnico | snapshot estricto en publico; refresh asincrono |
| `/api/historical/recent-matches` | merge con public-scoreboard persisted fallback | RCON insuficiente | medio, 1.3s max | no | frontend ya debe preferir snapshot |
| legacy `/api/historical/*` | public-scoreboard/display fallback | RCON no soporta o snapshot missing | bajo hoy pero acoplado | parcialmente por metadata | deprecar o snapshot-only |
| `/api/historical/matches/detail` | public-scoreboard detail fallback | RCON detail no encontrado | potencial alto si display init se repite | no destacado | fallback estricto y rapido |
## Auditoria frontend
Paginas publicas revisadas:
- `frontend/index.html`
- `frontend/historico.html`
- `frontend/historico-partida.html`
- `frontend/partida-actual.html`
- `frontend/ranking.html`
- `frontend/stats.html`
Scripts publicos revisados:
- `frontend/assets/js/config.js`
- `frontend/assets/js/main.js`
- `frontend/assets/js/historico.js`
- `frontend/assets/js/historico-recent-live.js`
- `frontend/assets/js/historico-partida.js`
- `frontend/assets/js/partida-actual.js`
- `frontend/assets/js/ranking.js`
- `frontend/assets/js/stats.js`
Hallazgos frontend:
| Pagina | Fetchs | Loading/error | Timeout/abort/requestId | Riesgo |
| --- | --- | --- | --- | --- |
| `index.html` | `/health`, `/api/trailer`, `/api/servers` | error controlado; no bloquea todo por `Promise.allSettled` | sin timeout | WARNING por `/api/servers` lento |
| `historico.html` | snapshots server-summary/recent/leaderboard y recent-live | requestId/cache/error visual | requestId; sin timeout generico | WARNING por snapshots missing/fallback |
| `historico-partida.html` | `/api/historical/matches/detail` | error visual si falla | sin timeout/abort | OK hoy, deuda si endpoint vuelve lento |
| `partida-actual.html` | current match, kills, players | in-flight guards; no abort | sin timeout; polling frecuente | CRITICAL por endpoints kills/players |
| `ranking.html` | `/api/ranking` | loading/error correcto | AbortController + requestId | OK |
| `stats.html` | `/health`, player search, annual ranking, player profile | errores visibles pero search/profile pueden esperar demasiado | sin AbortController/requestId robusto para search/profile; sin timeout | CRITICAL por search/profile |
Referencias localhost/127.0.0.1 en frontend publico:
- `frontend/assets/js/config.js`: default dev backend y deteccion localhost.
- `frontend/assets/js/historico-recent-live.js`
- `frontend/assets/js/historico.js`
- `frontend/assets/js/main.js`
- `frontend/assets/js/partida-actual.js`
- `frontend/assets/js/ranking.js`
- `frontend/assets/js/stats.js`
- `frontend/historico.html`
- `frontend/index.html`
- `frontend/partida-actual.html`
- `frontend/ranking.html`
- `frontend/stats.html`
`frontend/historico-partida.html` ya no conserva `data-backend-base-url` local. La mitigacion en `config.js` evita que un host no local use el default dev, pero la deuda sigue existiendo porque hay HTML/JS publico con fallback local.
## Top 10 riesgos actuales
1. `/api/stats/players/search` hace inicializacion/read-model/fallback runtime en lectura publica y vence a 30s.
2. `/api/stats/players/{player_id}` hace inicializacion/read-model/fallback runtime en lectura publica y vence a 30s.
3. `/api/current-match/kills` vence a 30s o devuelve 500 segun servidor.
4. `/api/current-match/players` vence a 30s en `comunidad-hispana-01`.
5. `/api/servers` puede hacer refresh RCON en request publica y tarda 4.3s.
6. `get_snapshot` de snapshots historicos ejecuta `initialize_postgres_display_storage` en lectura.
7. Muchos snapshots historicos estan `missing`, generando `fallback_used=true` en 75+ probes.
8. Hay residuos de fallback `127.0.0.1`/`localhost` en HTML/JS publico.
9. `stats.js`, `partida-actual.js` e `historico-partida.js` no tienen timeout HTTP propio.
10. Endpoints legacy historicos siguen expuestos con fallback dinamico aunque el frontend ya usa snapshots para lo principal.
## Top 10 recomendaciones priorizadas
1. Crear task backend para `/api/stats/players/search` y `/api/stats/players/{player_id}`: read-only estricto, sin `initialize_*` ni fallback runtime en GET publico.
2. Crear task backend para `/api/current-match/kills` y `/api/current-match/players`: quitar init de AdminLog en lectura, revisar indices y devolver payload vacio rapido si no hay ventana actual.
3. Crear task backend para `/api/servers`: no hacer refresh RCON/A2S desde GET publico; responder snapshot y dejar refresh a worker.
4. Crear task frontend para `stats.js`: AbortController/requestId/timeout y estado de error rapido para search/profile.
5. Crear task frontend para `partida-actual.js`: timeout/abort por request y backoff si kills/players fallan.
6. Crear task backend para `historical_snapshot_storage`/`postgres_display_storage.get_snapshot`: conexion read-only sin DDL/init en lectura.
7. Crear task de generacion/validacion snapshots historicos faltantes: reducir `snapshot_status=missing`.
8. Crear task de limpieza de fallbacks locales en frontend publico, manteniendo soporte dev explicito sin contaminar produccion.
9. Crear task backend para hardening de `/api/historical/matches/detail`: read-only estricto y fallback rapido aunque hoy responda OK.
10. Crear task de deprecacion/control de endpoints legacy `/api/historical/*` no usados por frontend o exponerlos solo como legacy con limites claros.
## Fixes propuestos como tasks pequenas
1. `TASK-225-stats-player-read-model-public-fast-path`: `/api/stats/players/search` y `/api/stats/players/{player_id}` sin inicializacion ni fallback runtime en lectura publica.
2. `TASK-226-current-match-adminlog-public-fast-path`: `/api/current-match/kills` y `/api/current-match/players` read-only, indices y errores controlados.
3. `TASK-227-servers-public-snapshot-only`: `/api/servers` no refresca RCON/A2S durante request publica.
4. `TASK-228-frontend-stats-request-timeouts`: timeout/abort/requestId para `stats.js`.
5. `TASK-229-frontend-current-match-request-timeouts`: timeout/backoff para `partida-actual.js`.
6. `TASK-230-historical-snapshot-storage-read-only`: quitar `initialize_postgres_display_storage` del path de lectura de snapshots.
7. `TASK-231-historical-snapshot-coverage-refresh`: completar snapshots faltantes usados por `historico.html`.
8. `TASK-232-frontend-public-backend-url-hardening`: eliminar fallbacks locales de HTML/JS publico de produccion.
9. `TASK-233-historical-match-detail-read-only-hardening`: hardening de `/api/historical/matches/detail`.
10. `TASK-234-legacy-historical-endpoint-policy`: inventario/limites/deprecacion de legacy endpoints publicos.
## Comandos de auditoria
Desde host contra produccion:
```powershell
python scripts\audit_public_requests.py --base-url https://comunidadhll.devzamode.es --timeout 30 --output tmp\public_request_audit.json
```
Desde host contra backend local:
```powershell
python scripts\audit_public_requests.py --base-url http://127.0.0.1:8000 --timeout 30 --output tmp\public_request_audit.json
```
Dentro del contenedor backend desde PowerShell, sin copiar archivos al contenedor:
```powershell
Get-Content scripts\audit_public_requests.py | docker compose exec -T backend python - --base-url http://127.0.0.1:8000 --timeout 30 --output /app/data/public_request_audit.json
```
Equivalente shell:
```bash
cat scripts/audit_public_requests.py | docker compose exec -T backend python - --base-url http://127.0.0.1:8000 --timeout 30 --output /app/data/public_request_audit.json
```
Compilacion del script:
```powershell
python -m py_compile scripts\audit_public_requests.py
```
## Notas de alcance
- No se ejecuto `ai-platform run`.
- No se hizo commit.
- No se hizo push.
- No se aplicaron fixes funcionales.
- No se modifico logica productiva backend ni frontend.
- No se tocaron assets, SVGs, imagenes fisicas, `frontend/assets/img/weapons/`, `frontend/assets/img/clans/` ni `ai/system-metrics.md`.
- No se reactivo Elo/MMR.
- No se agrego ningun servidor nuevo.

View File

@@ -0,0 +1,676 @@
#!/usr/bin/env python3
"""Audit public HTTP requests exposed by the HLL Vietnam application."""
from __future__ import annotations
import argparse
import json
import re
import sys
import time
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.parse import quote
from urllib.request import Request, urlopen
DEFAULT_BASE_URL = "http://127.0.0.1:8000"
DEFAULT_TIMEOUT_SECONDS = 25.0
KNOWN_MATCH_ID = "comunidad-hispana-01:1781023156:1781028555:purpleheartlanewarfare"
KNOWN_PLAYER_SEARCH = "Medu"
RANKING_METRICS = (
"kills",
"deaths",
"teamkills",
"matches_considered",
"kd_ratio",
"kills_per_match",
)
RANKING_SCOPES = ("all", "comunidad-hispana-01", "comunidad-hispana-02")
HISTORICAL_SCOPES = ("all-servers", "comunidad-hispana-01", "comunidad-hispana-02")
HISTORICAL_LEGACY_METRICS = ("kills", "deaths", "support", "matches_over_100_kills")
PLAYER_EVENT_VIEWS = ("most-killed", "death-by", "duels", "weapon-kills", "teamkills")
@dataclass(frozen=True)
class ProbeSpec:
id: str
kind: str
context: str
method: str
path: str
parameters: str
def main() -> int:
parser = argparse.ArgumentParser(
description="Measure public HLL Vietnam frontend/backend request surfaces."
)
parser.add_argument(
"--base-url",
default=DEFAULT_BASE_URL,
help=f"Base URL to audit. Default: {DEFAULT_BASE_URL}",
)
parser.add_argument(
"--timeout",
type=float,
default=DEFAULT_TIMEOUT_SECONDS,
help=f"Per-request timeout in seconds. Default: {DEFAULT_TIMEOUT_SECONDS}",
)
parser.add_argument(
"--output",
default=None,
help="JSON output path. Default: tmp/public_request_audit.json.",
)
parser.add_argument(
"--repo-root",
default=None,
help="Repository root for static discovery metadata. Default: auto/cwd.",
)
parser.add_argument(
"--max-probes",
type=int,
default=0,
help="Optional cap for smoke runs. 0 means all probes.",
)
args = parser.parse_args()
base_url = normalize_base_url(args.base_url)
repo_root = resolve_repo_root(args.repo_root)
output_path = resolve_output_path(args.output)
specs = build_probe_specs()
if args.max_probes and args.max_probes > 0:
specs = specs[: args.max_probes]
results: list[dict[str, Any]] = []
dynamic_player_id: str | None = None
index = 0
while index < len(specs):
spec = specs[index]
result = run_probe(base_url=base_url, spec=spec, timeout_seconds=args.timeout)
results.append(result)
print_result_row(result)
if spec.id == "stats-player-search-medu" and dynamic_player_id is None:
dynamic_player_id = extract_first_player_id(result.get("json"))
if dynamic_player_id:
specs.extend(build_player_dependent_specs(dynamic_player_id))
index += 1
payload = {
"audit": {
"base_url": base_url,
"timeout_seconds": args.timeout,
"generated_at_epoch": time.time(),
"probe_count": len(results),
},
"summary": summarize_results(results),
"discovery": discover_static_requests(repo_root),
"results": strip_json_payloads(results),
}
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(payload, ensure_ascii=True, indent=2) + "\n", encoding="utf-8")
print()
print_summary(payload["summary"])
print(f"Saved JSON results to {output_path}")
return 0
def build_probe_specs() -> list[ProbeSpec]:
specs: list[ProbeSpec] = []
seen: set[str] = set()
def add(
id_: str,
path: str,
*,
kind: str = "backend-api",
context: str = "backend",
parameters: str = "",
) -> None:
if id_ in seen:
return
seen.add(id_)
specs.append(
ProbeSpec(
id=id_,
kind=kind,
context=context,
method="GET",
path=path,
parameters=parameters,
)
)
add("health", "/health", context="health", parameters="none")
add("api-community", "/api/community", context="home", parameters="none")
add("api-trailer", "/api/trailer", context="home", parameters="none")
add("api-discord", "/api/discord", context="home", parameters="none")
add("api-servers", "/api/servers", context="home", parameters="none")
add("api-servers-latest", "/api/servers/latest", context="home", parameters="none")
add("api-servers-history", "/api/servers/history?limit=20", context="server-history", parameters="limit=20")
for server in ("comunidad-hispana-01", "comunidad-hispana-02"):
add(
f"api-servers-{server}-history",
f"/api/servers/{server}/history?limit=20",
context="server-history",
parameters="server path, limit=20",
)
for timeframe in ("weekly", "monthly", "annual"):
for server in RANKING_SCOPES:
for metric in RANKING_METRICS:
params = {
"timeframe": timeframe,
"server_id": server,
"metric": metric,
"limit": "20",
}
if timeframe == "annual":
params["year"] = "2026"
query = "&".join(f"{key}={quote(value)}" for key, value in params.items())
add(
f"ranking-{timeframe}-{server}-{metric}",
f"/api/ranking?{query}",
context="ranking.html",
parameters="timeframe, server_id, metric, limit, year for annual",
)
add(
"ranking-weekly-all-servers-alias",
"/api/ranking?timeframe=weekly&server_id=all-servers&metric=kills&limit=20",
context="ranking.html",
parameters="server_id=all-servers alias",
)
add(
"stats-player-search-medu",
f"/api/stats/players/search?q={quote(KNOWN_PLAYER_SEARCH)}&server_id=all&limit=10",
context="stats.html",
parameters="q=Medu, server_id=all, limit=10",
)
add(
"stats-annual-ranking",
"/api/stats/rankings/annual?year=2026&server_id=all&metric=kills&limit=20",
context="stats.html",
parameters="year=2026, server_id=all, metric=kills, limit=20",
)
for server in ("comunidad-hispana-01", "comunidad-hispana-02"):
add(
f"current-match-{server}",
f"/api/current-match?server={server}",
context="partida-actual.html",
parameters="server",
)
add(
f"current-match-kills-{server}",
f"/api/current-match/kills?server={server}&limit=30",
context="partida-actual.html",
parameters="server, limit=30",
)
add(
f"current-match-players-{server}",
f"/api/current-match/players?server={server}",
context="partida-actual.html",
parameters="server",
)
for server in HISTORICAL_SCOPES:
add(
f"historical-server-summary-{server}",
f"/api/historical/server-summary?server={server}",
context="historico.html legacy",
parameters="server",
)
add(
f"historical-recent-matches-{server}",
f"/api/historical/recent-matches?server={server}&limit=20",
context="historico.html legacy",
parameters="server, limit=20",
)
add(
f"snapshot-server-summary-{server}",
f"/api/historical/snapshots/server-summary?server={server}",
context="historico.html",
parameters="server",
)
add(
f"snapshot-recent-matches-{server}",
f"/api/historical/snapshots/recent-matches?server={server}&limit=100",
context="historico.html",
parameters="server, limit=100",
)
add(
f"weekly-top-kills-{server}",
f"/api/historical/weekly-top-kills?server={server}&limit=10",
context="historical legacy API",
parameters="server, limit=10",
)
for timeframe in ("weekly", "monthly"):
for metric in HISTORICAL_LEGACY_METRICS:
add(
f"historical-leaderboard-{timeframe}-{server}-{metric}",
(
"/api/historical/leaderboard?"
f"server={server}&timeframe={timeframe}&metric={metric}&limit=10"
),
context="historical legacy API",
parameters="server, timeframe, metric, limit=10",
)
add(
f"snapshot-leaderboard-{timeframe}-{server}-{metric}",
(
"/api/historical/snapshots/leaderboard?"
f"server={server}&timeframe={timeframe}&metric={metric}&limit=10"
),
context="historico.html",
parameters="server, timeframe, metric, limit=10",
)
add(
f"historical-weekly-leaderboard-{server}",
f"/api/historical/weekly-leaderboard?server={server}&metric=kills&limit=10",
context="historical legacy API",
parameters="server, metric=kills, limit=10",
)
add(
f"historical-monthly-leaderboard-{server}",
f"/api/historical/monthly-leaderboard?server={server}&metric=kills&limit=10",
context="historical legacy API",
parameters="server, metric=kills, limit=10",
)
add(
f"snapshot-weekly-leaderboard-{server}",
f"/api/historical/snapshots/weekly-leaderboard?server={server}&metric=kills&limit=10",
context="historico.html",
parameters="server, metric=kills, limit=10",
)
add(
f"snapshot-monthly-leaderboard-{server}",
f"/api/historical/snapshots/monthly-leaderboard?server={server}&metric=kills&limit=10",
context="historico.html",
parameters="server, metric=kills, limit=10",
)
add(
f"monthly-mvp-{server}",
f"/api/historical/monthly-mvp?server={server}&limit=10",
context="historical public API",
parameters="server, limit=10",
)
add(
f"monthly-mvp-v2-{server}",
f"/api/historical/monthly-mvp-v2?server={server}&limit=10",
context="historical public API",
parameters="server, limit=10",
)
add(
f"snapshot-monthly-mvp-{server}",
f"/api/historical/snapshots/monthly-mvp?server={server}&limit=10",
context="historical snapshots",
parameters="server, limit=10",
)
add(
f"snapshot-monthly-mvp-v2-{server}",
f"/api/historical/snapshots/monthly-mvp-v2?server={server}&limit=10",
context="historical snapshots",
parameters="server, limit=10",
)
for view in PLAYER_EVENT_VIEWS:
add(
f"player-events-{server}-{view}",
f"/api/historical/player-events?server={server}&view={view}&limit=10",
context="historical public API",
parameters="server, view, limit=10",
)
add(
f"snapshot-player-events-{server}-{view}",
f"/api/historical/snapshots/player-events?server={server}&view={view}&limit=10",
context="historical snapshots",
parameters="server, view, limit=10",
)
add(
"historical-match-detail-known",
(
"/api/historical/matches/detail?"
"server=comunidad-hispana-01&match="
f"{quote(KNOWN_MATCH_ID, safe='')}"
),
context="historico-partida.html",
parameters="server, match",
)
add(
"elo-mmr-leaderboard-paused",
"/api/historical/elo-mmr/leaderboard?server=all-servers&limit=10",
context="paused public API",
parameters="server, limit=10",
)
return specs
def build_player_dependent_specs(player_id: str) -> list[ProbeSpec]:
quoted_player = quote(player_id, safe="")
return [
ProbeSpec(
id="stats-player-profile-weekly",
kind="backend-api",
context="stats.html",
method="GET",
path=f"/api/stats/players/{quoted_player}?timeframe=weekly&server_id=all",
parameters="player_id from search, timeframe=weekly, server_id=all",
),
ProbeSpec(
id="stats-player-profile-monthly",
kind="backend-api",
context="stats.html",
method="GET",
path=f"/api/stats/players/{quoted_player}?timeframe=monthly&server_id=all",
parameters="player_id from search, timeframe=monthly, server_id=all",
),
ProbeSpec(
id="historical-player-profile",
kind="backend-api",
context="historical public API",
method="GET",
path=f"/api/historical/player-profile?player={quoted_player}",
parameters="player from search",
),
ProbeSpec(
id="elo-mmr-player-paused",
kind="backend-api",
context="paused public API",
method="GET",
path=f"/api/historical/elo-mmr/player?server=all-servers&player={quoted_player}",
parameters="player from search, server=all-servers",
),
]
def run_probe(*, base_url: str, spec: ProbeSpec, timeout_seconds: float) -> dict[str, Any]:
url = f"{base_url}{spec.path}"
started = time.perf_counter()
status_code: int | None = None
body = b""
error: str | None = None
headers: dict[str, str] = {}
try:
request = Request(
url,
headers={
"Accept": "application/json",
"User-Agent": "HLL-Vietnam-Public-Request-Audit/1.0",
},
method=spec.method,
)
with urlopen(request, timeout=timeout_seconds) as response:
status_code = int(response.status)
headers = {key.lower(): value for key, value in response.headers.items()}
body = response.read()
except HTTPError as exc:
status_code = int(exc.code)
headers = {key.lower(): value for key, value in exc.headers.items()}
body = exc.read()
error = f"HTTPError: {exc.code}"
except URLError as exc:
error = f"URLError: {exc.reason}"
except TimeoutError:
error = "TimeoutError"
except Exception as exc: # noqa: BLE001 - audit must continue
error = f"{type(exc).__name__}: {exc}"
elapsed_ms = round((time.perf_counter() - started) * 1000, 2)
parsed_json = parse_json_body(body)
summary = summarize_json(parsed_json)
severity = classify_result(
status_code=status_code,
elapsed_ms=elapsed_ms,
error=error,
fallback_used=summary.get("fallback_used"),
snapshot_status=summary.get("snapshot_status"),
)
return {
**asdict(spec),
"url": url,
"status_code": status_code,
"elapsed_ms": elapsed_ms,
"response_size_bytes": len(body),
"content_type": headers.get("content-type"),
"json_status": summary.get("json_status"),
"data_source": summary.get("data_source"),
"fallback_used": summary.get("fallback_used"),
"fallback_reason": summary.get("fallback_reason"),
"snapshot_status": summary.get("snapshot_status"),
"item_count": summary.get("item_count"),
"found": summary.get("found"),
"severity": severity,
"error": error,
"json": parsed_json,
}
def parse_json_body(body: bytes) -> Any:
if not body:
return None
try:
return json.loads(body.decode("utf-8"))
except Exception:
return None
def summarize_json(payload: Any) -> dict[str, Any]:
if not isinstance(payload, dict):
return {}
data = payload.get("data") if isinstance(payload.get("data"), dict) else {}
source_payload = data.get("source") if isinstance(data.get("source"), dict) else {}
items = data.get("items")
if isinstance(items, list):
item_count = len(items)
elif isinstance(data.get("item"), dict):
item_count = 1
elif isinstance(data.get("profile"), dict):
item_count = 1
else:
item_count = None
return {
"json_status": payload.get("status"),
"data_source": data.get("source")
if isinstance(data.get("source"), str)
else source_payload.get("read_model") or source_payload.get("primary_source"),
"fallback_used": data.get("fallback_used")
if "fallback_used" in data
else source_payload.get("fallback_used"),
"fallback_reason": data.get("fallback_reason")
if "fallback_reason" in data
else source_payload.get("fallback_reason"),
"snapshot_status": data.get("snapshot_status"),
"item_count": item_count,
"found": data.get("found"),
}
def classify_result(
*,
status_code: int | None,
elapsed_ms: float,
error: str | None,
fallback_used: Any,
snapshot_status: Any,
) -> str:
if error or status_code is None or status_code >= 500 or elapsed_ms >= 10000:
return "CRITICAL"
if status_code >= 400 or elapsed_ms >= 2500 or fallback_used is True:
return "WARNING"
if str(snapshot_status or "").lower() == "missing":
return "WARNING"
return "OK"
def extract_first_player_id(payload: Any) -> str | None:
if not isinstance(payload, dict):
return None
data = payload.get("data")
if not isinstance(data, dict):
return None
items = data.get("items")
if not isinstance(items, list):
return None
for item in items:
if isinstance(item, dict) and str(item.get("player_id") or "").strip():
return str(item["player_id"]).strip()
return None
def discover_static_requests(repo_root: Path) -> dict[str, Any]:
backend_paths: set[str] = set()
frontend_literals: set[str] = set()
fetch_occurrences: list[dict[str, Any]] = []
localhost_refs: list[dict[str, Any]] = []
routes_path = repo_root / "backend" / "app" / "routes.py"
if routes_path.exists():
text = routes_path.read_text(encoding="utf-8")
for value in re.findall(r"['\"](/(?:api/[^'\"]+|health))['\"]", text):
backend_paths.add(value)
frontend_root = repo_root / "frontend"
if frontend_root.exists():
for path in sorted(frontend_root.rglob("*")):
if path.suffix.lower() not in {".html", ".js"}:
continue
text = path.read_text(encoding="utf-8", errors="replace")
for value in re.findall(r"/api/[A-Za-z0-9_./?=&%${}()+:,\\-]+|/health", text):
frontend_literals.add(value)
for line_number, line in enumerate(text.splitlines(), start=1):
if "fetch(" in line:
fetch_occurrences.append(
{
"file": str(path.relative_to(repo_root)),
"line": line_number,
"text": line.strip(),
}
)
if "127.0.0.1" in line or "localhost" in line:
localhost_refs.append(
{
"file": str(path.relative_to(repo_root)),
"line": line_number,
"text": line.strip(),
}
)
return {
"repo_root": str(repo_root),
"backend_route_paths": sorted(backend_paths),
"frontend_api_literals": sorted(frontend_literals),
"frontend_fetch_occurrences": fetch_occurrences,
"frontend_localhost_references": localhost_refs,
}
def summarize_results(results: list[dict[str, Any]]) -> dict[str, Any]:
severity_counts = {"OK": 0, "WARNING": 0, "CRITICAL": 0}
status_counts: dict[str, int] = {}
launched = 0
for result in results:
launched += 1
severity = str(result.get("severity") or "WARNING")
severity_counts[severity] = severity_counts.get(severity, 0) + 1
status_key = str(result.get("status_code"))
status_counts[status_key] = status_counts.get(status_key, 0) + 1
slowest = sorted(
(
{
"id": result.get("id"),
"path": result.get("path"),
"elapsed_ms": result.get("elapsed_ms"),
"status_code": result.get("status_code"),
"severity": result.get("severity"),
}
for result in results
),
key=lambda item: float(item.get("elapsed_ms") or 0),
reverse=True,
)[:10]
return {
"launched": launched,
"severity_counts": severity_counts,
"status_counts": status_counts,
"slowest": slowest,
}
def strip_json_payloads(results: list[dict[str, Any]]) -> list[dict[str, Any]]:
stripped = []
for result in results:
item = dict(result)
item.pop("json", None)
stripped.append(item)
return stripped
def print_result_row(result: dict[str, Any]) -> None:
status = result.get("status_code")
elapsed = result.get("elapsed_ms")
severity = result.get("severity")
size = result.get("response_size_bytes")
fallback = result.get("fallback_used")
print(
f"{severity:8} {str(status):>4} {elapsed:>9.2f} ms {size:>9} B "
f"fallback={str(fallback):<5} {result['id']}"
)
def print_summary(summary: dict[str, Any]) -> None:
print("Summary")
print(f" launched: {summary['launched']}")
print(f" OK: {summary['severity_counts'].get('OK', 0)}")
print(f" WARNING: {summary['severity_counts'].get('WARNING', 0)}")
print(f" CRITICAL: {summary['severity_counts'].get('CRITICAL', 0)}")
print(" slowest:")
for item in summary.get("slowest", []):
print(
" "
f"{item.get('elapsed_ms')} ms {item.get('status_code')} "
f"{item.get('severity')} {item.get('id')}"
)
def normalize_base_url(value: str) -> str:
return str(value or DEFAULT_BASE_URL).strip().rstrip("/")
def resolve_repo_root(raw_value: str | None) -> Path:
if raw_value:
return Path(raw_value).resolve()
cwd = Path.cwd().resolve()
if (cwd / "backend" / "app" / "routes.py").exists():
return cwd
script_path = Path(__file__).resolve()
for candidate in (script_path.parent, *script_path.parents):
if (candidate / "backend" / "app" / "routes.py").exists():
return candidate
return cwd
def resolve_output_path(raw_value: str | None) -> Path:
if raw_value:
return Path(raw_value)
tmp_path = Path("tmp") / "public_request_audit.json"
try:
tmp_path.parent.mkdir(parents=True, exist_ok=True)
return tmp_path
except OSError:
return Path("docs") / "public_request_audit_results.example.json"
if __name__ == "__main__":
sys.exit(main())