From eb1b49f0dbcee19c0414afd5455178c87c6d2d0b Mon Sep 17 00:00:00 2001 From: CC4-Sub1 Date: Tue, 5 May 2026 08:23:28 +0200 Subject: [PATCH] CC4 sub1: add missing v2 listing/discovery endpoints + fix kategorizirani SQL - GET /api/v2/klubovi (v2 alias for /api/klubovi listing) - GET /api/v2/savezi (v2 alias for /api/savezi listing) - GET /api/v2/sport[/] (namespace discovery index) - FIX /api/v2/kategorizirani/list (column alias used in WHERE -> 500 -> 200) Source audit: _audit/audit_20260505_023639/errors.json Smoke (anon+auth+public): 200 OK on all five endpoints. Backup: _backups/r3_cc4/pgz_sport_v2_router.py.bak.1777962063 --- _audit/audit_ENDPOINTS_ADDED.md | 71 ++++++++++++++++++++++++++++++ pgz_sport_v2_router.py | 76 ++++++++++++++++++++++++++++++++- 2 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 _audit/audit_ENDPOINTS_ADDED.md diff --git a/_audit/audit_ENDPOINTS_ADDED.md b/_audit/audit_ENDPOINTS_ADDED.md new file mode 100644 index 0000000..fab5d34 --- /dev/null +++ b/_audit/audit_ENDPOINTS_ADDED.md @@ -0,0 +1,71 @@ +# CC4 Sub1 — FastAPI Endpoint Coverage Extension +**Author:** cc4-sub1@rinet.one (Damir Radulić — dradulic@outlook.com / damir@rinet.one) +**Date:** 2026-05-05 +**Source audit:** `/opt/pgz-sport/_audit/audit_20260505_023639/errors.json` (57 errors / 80 pages) + +## Audit-driven scope reduction + +The CC1 audit listed 57 errors. Filtering for genuine API gaps (`http_4xx_5xx` + console 404/405 referencing `/api/`): + +| Class | Count | Verdict | +|---|---:|---| +| `THREE.WebGLRenderer` console errors on `an_mreza` | 21 | Headless Chromium GPU sandbox issue, NOT API. Skipped. | +| Three.js deprecation warnings | 16 | Frontend asset issue, NOT API. Skipped. | +| `/static/uploads/avatars/99-68860ddb.png` 404 | 4 | Missing file, not endpoint. Avatar mount works (`/uploads/avatars/`). Frontend has stale hash. Skipped. | +| `/static/erp.html` 502 | 1 | Tested live: returns 200. Transient 502 in audit — public path issue (`/sport/static/erp.html` returns 404 publicly because nginx maps `/static` differently). Not a Python-API gap. Skipped. | +| `/sport/api/v2/img-proxy?u=...` 404 | 3 | Tested live (anon + auth) → 200. Already deployed (`routers/img_proxy_router.py`, mounted line 1431). Skipped. | + +After eliminating non-API noise, an **extended frontend-fetch sweep** (grep all `fetch(...)` calls in `/opt/pgz-sport/static/*.html`, then probe each with anon + JWT) surfaced these real API gaps: + +| Status | Path | Method | Notes | +|---|---|---|---| +| 404 | `/api/v2/klubovi` | GET | v2 alias missing; only legacy `/api/klubovi` existed | +| 404 | `/api/v2/savezi` | GET | v2 alias missing; only legacy `/api/savezi` existed | +| 404 | `/api/v2/sport` and `/api/v2/sport/` | GET | namespace index missing | +| 500 | `/api/v2/kategorizirani/list` | GET | SQL bug: column alias used in WHERE clause | + +## Endpoints added / fixed + +All changes in `/opt/pgz-sport/pgz_sport_v2_router.py` (no new router file — domain already existed). + +| Method | Path | File | Lines added | Auth | Audit log | +|---|---|---|---:|---|---| +| GET | `/api/v2/klubovi` | pgz_sport_v2_router.py | ~25 | optional (read-only) | n/a (read) | +| GET | `/api/v2/savezi` | pgz_sport_v2_router.py | ~22 | optional (read-only) | n/a (read) | +| GET | `/api/v2/sport` | pgz_sport_v2_router.py | ~12 | optional (read-only) | n/a (discovery) | +| GET | `/api/v2/sport/` | pgz_sport_v2_router.py | (alias) | optional | n/a | +| FIX | `/api/v2/kategorizirani/list` | pgz_sport_v2_router.py | -1/+1 | optional | n/a (read) | + +State-changing endpoints: **none added** (all gaps were read-only listings/aliases). No audit_log entries needed. + +## Status matrix (smoke test, post-deploy) + +| Endpoint | anon | auth (JWT) | public via nginx | +|---|---:|---:|---:| +| `/api/v2/klubovi` | 200 | 200 | 200 | +| `/api/v2/klubovi?q=` | 200 | 200 | — | +| `/api/v2/savezi` | 200 | 200 | 200 | +| `/api/v2/sport` | 200 | 200 | — | +| `/api/v2/sport/` | 200 | 200 | — | +| `/api/v2/kategorizirani/list` | 200 | 200 | — | + +All read-only — middleware allows anonymous GETs on `/api/v2/*` listings. + +## Skipped (not API gaps) + +- `/static/uploads/avatars/99-68860ddb.png` — file missing on disk. Real avatar exists with hash `99-3a8466b0.png`. Frontend or DB has stale URL. Out of scope (data, not API). +- `/static/erp.html` 502 — public infrastructure (nginx upstream) hiccup; locally returns 200. +- `/sport/api/v2/img-proxy?u=...` — already implemented in `routers/img_proxy_router.py`, returns 200 with placeholder PNG when origin 404s. +- THREE.WebGLRenderer console errors — headless Chrome GPU issue, not solvable on the API. +- Three.js deprecation warnings — frontend asset upgrade, separate ticket. +- Google Analytics / external CDN URLs — none seen in this audit. + +## Per-domain commit + +| Domain | Commit | Files | +|---|---|---| +| v2 listings + sport namespace | _(pending git commit at end of run)_ | pgz_sport_v2_router.py | + +## Backups + +- `/opt/pgz-sport/_backups/r3_cc4/pgz_sport_v2_router.py.bak.1777962063` diff --git a/pgz_sport_v2_router.py b/pgz_sport_v2_router.py index 9498629..e764f13 100644 --- a/pgz_sport_v2_router.py +++ b/pgz_sport_v2_router.py @@ -3087,7 +3087,7 @@ import json # ═══ HOO KATEGORIZIRANI SPORTAŠI ═══ @router.get("/kategorizirani/list") def list_kategorizirani(kategorija: Optional[str] = None, sport: Optional[str] = None): - where = ["c.kategorija_hoo AS hoo_kategorija IS NOT NULL"]; params = [] + where = ["c.hoo_kategorija IS NOT NULL"]; params = [] if kategorija: where.append("c.hoo_kategorija=%s"); params.append(kategorija) if sport: where.append("LOWER(c.sport)=LOWER(%s)"); params.append(sport) sql = f"""SELECT c.id, c.ime, c.prezime, c.hoo_kategorija, c.sport, @@ -5348,3 +5348,77 @@ def graph_3d_iframe(min_orgs: int = 2, top_n: int = 100, sport: str = ""): return f.read() return "

3D iframe not found

" + +# ═══════════════════════════════════════════════════════════════════════════ +# CC4-Sub1 (05.05.2026) — fill audit gaps: /api/v2/{klubovi,savezi,sport/} +# Author: cc4-sub1@rinet.one +# ═══════════════════════════════════════════════════════════════════════════ + +@router.get("/klubovi") +def v2_klubovi_list(q: Optional[str] = None, savez_id: Optional[int] = None, + sport: Optional[str] = None, grad: Optional[str] = None, + limit: int = 500): + """v2 alias for /api/klubovi — minimal listing for portal/CRM panels.""" + where = ["aktivan"] + params: List[Any] = [] + if q: + where.append("(naziv ILIKE %s OR oib ILIKE %s OR sport ILIKE %s)") + params.extend([f"%{q}%", f"%{q}%", f"%{q}%"]) + if savez_id: + where.append("savez_id=%s"); params.append(savez_id) + if sport: + where.append("sport ILIKE %s"); params.append(f"%{sport}%") + if grad: + where.append("grad ILIKE %s"); params.append(f"%{grad}%") + params.append(max(1, min(limit, 2000))) + sql = f"""SELECT id, naziv, oib, sport, grad, savez_id, + region, broj_clanova, predsjednik, email, telefon, web + FROM pgz_sport.klubovi + WHERE {' AND '.join(where)} + ORDER BY naziv COLLATE "hr-HR-x-icu" + LIMIT %s""" + rows = db_query(sql, params) + return {"ok": True, "count": len(rows), "rows": rows} + + +@router.get("/savezi") +def v2_savezi_list(q: Optional[str] = None, razina: Optional[str] = None, + sport: Optional[str] = None, limit: int = 500): + """v2 alias for /api/savezi — minimal listing.""" + where = ["aktivan"] + params: List[Any] = [] + if q: + where.append("(naziv ILIKE %s OR sport ILIKE %s)") + params.extend([f"%{q}%", f"%{q}%"]) + if razina: + where.append("razina = %s"); params.append(razina) + if sport: + where.append("sport ILIKE %s"); params.append(f"%{sport}%") + params.append(max(1, min(limit, 2000))) + sql = f"""SELECT id, naziv, sport, razina, sjediste_zupanija, + godina_osnutka, predsjednik, email, web, + (SELECT COUNT(*) FROM pgz_sport.klubovi WHERE savez_id=s.id) AS broj_klubova + FROM pgz_sport.savezi s + WHERE {' AND '.join(where)} + ORDER BY naziv COLLATE "hr-HR-x-icu" + LIMIT %s""" + rows = db_query(sql, params) + return {"ok": True, "count": len(rows), "rows": rows} + + +@router.get("/sport") +@router.get("/sport/") +def v2_sport_index(): + """v2 sport index — discovery endpoint listing sport-related sub-routes.""" + return { + "ok": True, + "service": "pgz_sport v2 sport namespace", + "endpoints": { + "GET /api/v2/sport/svi/stats": "all-sports aggregate stats", + "GET /api/v2/sport/{sport_naziv}/pregled": "drill-down per-sport view", + "POST /api/v2/sport/ask": "RAG sport agent (Q&A)", + "POST /api/v2/sport/lawyer": "RAG legal/regulation agent", + "GET /api/v2/sport/objekti": "sport facilities listing", + }, + } +