Add full public request audit
This commit is contained in:
167
ai/tasks/done/TASK-224-full-application-request-audit.md
Normal file
167
ai/tasks/done/TASK-224-full-application-request-audit.md
Normal 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.
|
||||||
386
docs/FULL_APPLICATION_REQUEST_AUDIT.md
Normal file
386
docs/FULL_APPLICATION_REQUEST_AUDIT.md
Normal 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.
|
||||||
676
scripts/audit_public_requests.py
Normal file
676
scripts/audit_public_requests.py
Normal 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())
|
||||||
Reference in New Issue
Block a user