Coverage report (`/opt/pgz-sport/data_quality_report.md`):
- 5952 entities measured (savezi 246, klubovi 2244, sportasi 3243, objekti 106, manifestacije 113)
- Weighted mean coverage 52.1%
- Per-type stats: objekt 79.7% > manif 81.9% > savez 59.8% > klub 57.1% > sportas 46.2%
- Distribution histogram per type
- TOP 50 entities for manual review (lowest coverage with non-empty name) with portal links
Mreža verification (Playwright headless):
- pgz-savez-nogometni anchor injected, label "Nogometni savez PGŽ", color #F4C430, size 40
- 6 anchor edges to top-3 persons + top-3 entities
- 90 nodes / 186 edges total after augmentation
- "🎯 Centar (PGŽ)" button visible
- centerMrezaOnAnchor() fires 1.5s after render
Cleanup v2 (`scripts/r6_cleanup_v2.sql`):
- 2636 [VERIFY] → Odbojkaški Klub "Odbojkaška Akademija Petica" (civic#114850)
- 2641 [VERIFY] → Ženski Odbojkaški Klub "Crikvenica" (civic#78781)
- 12 of 14 originals now confirmed; 2 still need manual ([VERIFY] 2619 Vrh Čavje 31, 2630 1. Istarske čete 3 — no civic.entities row at those addresses)
sport-pgz.hr scrape: site is a Vite SPA with no public JSON club listing endpoint;
individual club slugs return 404. Best authoritative source remains civic.entities.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#1 JWT middleware extended:
- Was: /api/admin/* only
- Now: any POST/PUT/PATCH/DELETE under /api/* requires Bearer JWT
- Whitelist (still anonymous): /api/auth/login, /refresh, /forgot-password,
/password/reset, /reset-password, /setup-password, /google;
/api/gdpr/consent; any path ending /avatar
- 14 mutating endpoints verified to return 401 without token
#2 Avatar upload demo mode (routers/clan_panel_router.py):
- Anonymous → returns {demo_mode:true, slika_url:null,
message:'Demo mode — slika nije spremljena. Prijavite se za pravu pohranu.'},
no FS write, no DB write
- Authenticated (valid JWT, allowed role) → real save as before
- Auth check now uses auth.auth_v2.decode_token (proper secret + revocation)
instead of the broken local _resolve_role
#3 Mock mailer (auth/mailer.py):
- send_email writes RFC 822 .eml to /tmp/pgz_mailbox + appends to INDEX.jsonl
- send_password_reset, send_invite helpers with HR text + HTML alt
- Real SMTP active when PGZ_SMTP_HOST is set (env-driven, off by default)
- forgot-password and admin invite both call mailer; audit logs mail status
#5 Rate limiting on /api/auth/login:
- Per-user: 5 wrong attempts → 5-minute DB-backed lockout
(was 5 → 15 min). Configurable via PGZ_LOGIN_LOCK_THRESHOLD/MINUTES.
- Per-IP: 10 fails / 5-min sliding window in-memory → HTTP 429
Configurable via PGZ_LOGIN_IP_THRESHOLD/WINDOW_SEC. Successful
login clears the IP counter.
- Failed attempts respond '(N/5) — račun je zaključan na 5 minuta'
- New audit actions: login.ratelimit.ip; login.fail meta now
includes fails count, locked, lock_minutes
#4 Live test report: 46/46 across 6 demo users — login, JWT gate on 14
mutating endpoints, public path whitelist, demo-mode avatar +
real save, forgot-password e-mail to mailbox, no-leak unknown email,
5-fail lockout, 423 during lockout, audit coverage.
- New scripts/geocode_v3_osm.py: matches DB objekti against OSM Overpass sports facilities
- Applied 53 OSM updates, then reverted bad cross-city matches to hand-curated coords
- Crikvenica venues now precise (Gradska dvorana, SS Antun Barac, Stadion, Sport+ Centar)
- Atletska dvorana Luciano Sušanj fixed to Kantrida
- Skate park Delta, Boulder dvorana, Boćarski Podvežica reverted from wrong matches
- Google Places API not available (project disabled), Overpass + curated fallback used
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>