DB:
- pgz_sport.sufinanciranje_sport.je_klub flag (RSS programi/totals false)
- pgz_sport.sufinanciranje_sport.klub_id matched
Endpoints:
- /v2/potpore/by-year: samo_klubovi=True default + davatelj filter
Frontend:
- sport2.html PANEL FORCE HIDE CSS (right:-100vw default)
- crm_v2.html: redirect to /login only on actual 401, not on page load
- pgz nav now includes /erp/full, /crm/v2, /admin/users, /dokumenti
- 4 dokumenti endpoints: list, godišnjaci/list, godišnjak/{godina} PDF, detail
- 18 godišnjaka u pgz_sport.dokumenti (2006-2024) with savez_id=333
- PGŽ filter helpers (window._pgz_filter_priority, togglePGZFilter)
- navItemClick handler for nav items with href
Old logout() was demo placeholder:
- only cleared 'app-role' + 'jwt' (NOT pgz_access/refresh/user)
- did NOT call POST /auth/logout to revoke JWT
- redirected to /static/sport2.html (wrong)
New logout() now:
1. POST /auth/logout to revoke JWT server-side
2. Clear ALL keys: pgz_access, pgz_refresh, pgz_user, app-role, jwt, access_token, refresh_token, pgz_session_id (both localStorage + sessionStorage)
3. Redirect to /login
Verified by Playwright E2E: token absent after logout.
- Frontend (sport2.html): refreshDashNositelji() koristi /api/dashboard/top-primatelji
umjesto /v2/potpore/by-year (koji je za 2025 vraćao samo 1 agregirani redak).
Dropdown proširen na "Sve godine" + 2021..2026. Dodana kolona "Platitelj".
- Backend (pgz_sport_api.py): top-primatelji endpoint sada parsira napomena
'doc_id=N' i JOIN-a pgz_sport.dokumenti za pdf_url; godina<=0 → sve godine;
dodane kolone vrsta + pdf_url + doc_title.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CASE WHEN ... ILIKE '%X%' patterns conflicted with %s param placeholder.
Escaped to %%X%%. Endpoint now returns 200 with full klubovi list +
inferred davatelj_naziv (RSS / Županijski / Grad Rijeka / fallback).
1) auth/auth_v2.py — update_me bug fix:
PUT /auth/me return value was 'return me(user)' but me() is a
FastAPI route handler, not callable directly. Replaced with explicit
re-fetch returning correct JSON shape. Profile changes now persist
in UI after save.
2) DB: HNK Goranin Delnice (id 782) sport='skijanje' → 'nogomet'
+ napomena cross-contamination cleaned (id 782, 192, 347, 2280)
+ general rule: NK/HNK/Nogometni klub → nogomet
+ RK/Rukometni klub → rukomet
+ OK/Odbojkaški klub → odbojka
3) DB: KUD/folklorne/lovačke/vatrogasne udruge marked as
sport='kulturno-umjetnicko' + razina='NE-sportsko' so frontend
can filter them out of sportski savezi list
4) Backup: pgz_sport.klubovi_backup_20260505_0857
Verified: PUT /auth/me with damir@pgz.hr persists telefon change to DB
and returns fresh data
Bug #1 — `/api/v2/dokumenti/{did:int}` duplicate registration (route shadowing):
- Bila dva @router.get-a za isti path, drugi (s chunks) bio dead code
- Renamed duplicate na /dokumenti/{did}/full → eksplicitan RAG-rich varianta
- /dokumenti/{did} ostaje simple (8 osnovnih polja), /full vraća chunks za RAG
Bug #2 — `/api/v2/dokumenti/upload` MISSING:
- Dodan @router.post("/dokumenti/upload") s multipart formom
- Polja: file, title, vrsta, razina, organizacija, sport, izvor_url, godina, kratak_opis
- Tekst ekstrakcija: pdftotext za PDF, raw decode za TXT
- Pohrana: /opt/pgz-sport/_data/dokumenti_uploads/{ts}_{sha12}_{safe_fname}
- Insert u pgz_sport.dokumenti (vraća dokument_id, fname, chars, sha12)
- Audit log entry preko erp.audit_helper
- Validacija: max 32 MB, dozvoljeno PDF/DOC/DOCX/TXT/RTF
Bug #3 — SQL alias bug u /api/v2/kategorizirani/list:
- Već popravljen u Sub1 commitu eb1b49f
- Verify: /kategorizirani/list i /kategorizirani/by-sport oba 200, count=270
Smoke 5/5 ✓:
- GET /v2/dokumenti -> 200
- GET /v2/dokumenti/1577 (simple) -> 200, 8 keys, no chunks
- GET /v2/dokumenti/1577/full (RAG) -> 200, has_chunks, count=0
- POST /v2/dokumenti/upload (multipart)-> 200, doc_id=31214, chars=50, size=52
- GET /v2/kategorizirani/list -> 200, 270 redova
/api/debug/dashboard:
- services: pgz-sport, pgz-debug-tail, pgz-auto-triage, nginx ACTIVE
- db: ok
- 0 novih grešaka nakon restart-a (samo stari nginx-access 502 iz prijašnjih restarta)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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
1) HNS direct link u research_links: za sportaš s profile_url/source_url
(npr. https://semafor.hns.family/igraci/X/...) generira [⭐DIRECT] link na vrhu liste,
umjesto generic Google search. _research_links sada prima row dict.
2) Avatar cache buster: applyMeToHeader dodaje ?t=Date.now() na sve avatar img tagove.
Avatar upload handler dodatno persistira novi avatar_url u localStorage.pgz_user
tako da preživi page refresh + cross-page navigacije.
3) Logo home link: <div class='logo'> → <a href='/' class='logo'> u app.html i sport2.html.
Klik na PGŽ SPORT logo vodi na public portal.
4) Klub → Sportaši drill-down: u klub Info tabu dodan button
'👥 Vidi sportaše ovog kluba (N)' koji prebacuje na k-clan tab.
Plus '🌐 Službena stranica' link kad klub ima web.
5) Smarter klub enrichment:
- URL validacija (skip placeholder strings poput 'godisnjak_zspgz_2025')
- Domain candidate guesser (slug → 16 candidate URLs s common HR TLD-ovima i sport prefix-ima)
- Parallel HEAD probe (8 threads, 10s budget) — first 200 + name token match wins
- Subpage scrape (/kontakt, /uprava, /o-nama, /o-klubu, /predsjednik) za richer evidence
- HNK Orijent (id 3766) test: pogađa https://www.orijent.hr/, predlaže web+email+telefon+opis
E2E verified:
- 9/9 sidebar URL-ova → 200
- /users/me/gdpr-export → 200 (28KB JSON)
- /users/me/request-deletion → 200 (DB row pgz_sport.gdpr_erasure_requests)
- /enrich/klub/3766 → 4 proposed fields (web, email, telefon, opis)
- HNS sportaš research_links: ⭐ HNS profil DIRECT link na vrhu
Backend: routers/enrich_router.py
Frontend: static/app.html, static/sport2.html
Backups: _backups/sprint_1777940670/
Tag: R7-demo-ready
- auth/gdpr.py: dodan @me_router.post('/request-deletion') alias
koji proxy-a na request_erasure (Art. 17). Koristi pravi EraseReq pydantic.
- static/app.html: obrisana placeholder profileDeleteAccount funkcija
na liniji 944 (M10 mock alert) — sada samo real implementacija na 1902.
- E2E verified: damir@pgz.hr → POST /users/me/request-deletion → 200,
DB row pgz_sport.gdpr_erasure_requests #1 pending.
Tag: P0-demo-fix
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>
Backend (already in master via parallel commits):
- enrich_router enrich_apply now returns
{status, applied_count, applied_fields, applied, after} so the toast
doesn't have to count manually.
- POST /api/v2/enrich/bulk runs preview+apply on N random under-enriched
rows of one kind, returns aggregate {processed, fields_total, items}.
- Worker dashboard: GET /api/v2/enrich/worker/status (heartbeat,
paused, last_cycle, confidence_threshold, fields_24h, recent),
POST /worker/pause, /worker/run-now, /worker/confidence.
- enrichment_worker honours Redis flags: cc:pgz-enricher:pause,
:run_now, :confidence (live override). Heartbeat + last_cycle JSON +
rolling fields_24h counter all written to Redis.
- pgz_sport_api JWT middleware now whitelists /api/v2/enrich/* under
_PUBLIC_MUTATING_PREFIXES so the demo UI works without a bearer.
Frontend (this commit):
- Reusable toast(msg, type, duration) helper with success/error/info/warn,
slide-up animation, auto-dismiss.
- Diff modal now has explicit ❌ Odustani + 💾 SPREMI IZMJENE buttons;
enrichApply consumes applied_count + applied_fields and surfaces them
in a multi-line success toast.
- '✨ Obogati sve (50)' button in klubovi + sportasi list toolbars; calls
enrichBulk() which posts to /v2/enrich/bulk with confirm dialog and
reload-on-success.
- Audit page renders a live Enrichment Worker card: heartbeat badge,
last-cycle stats, fields-24h, recent enrich-write rows, Pauziraj/
Nastavi/Pokreni-odmah buttons, confidence slider 0..1. Auto-refreshes
every 10s while the audit page is open.
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.
app.html — Kalendar sekcija (NOVO za sve role):
- Mjesečni grid (pon-ned), klik na dan = prikaz svih eventa
- KPI: liječnički isteci, manifestacije, neprocitano InApp, ukupno eventa
- Eventi: liječnički termini + manifestacije iz API + ZZJZ termin slot mock
- Navigacija prev/next + month picker
- "Scan isteke → notifikacije" gumb (one-click backend POST)
- Lista nadolazećih (10) + lista InApp neprocitanih s mark-read
crm.html — 2 nova taba:
- 📊 Statistika: aktivni vs neaktivni, reprezentativci, kategorizirani,
članarine summary, liječnički status, SVG bar chart trend uplata 12 mj,
podjela po spolu/kategoriji, top 10 najnovijih uplata
- 🔔 Notifikacije: lista InApp+Email s filterima (channel/status), gumb
za scan liječničkih (kreira 30/15/7 + expired bucket), mark-read pojedinačno
i bulk, deep-link na /lijecnicki/{id}/zakazi i /clanarine/{id}/uplatnica.pdf
iz meta polja
Bulk akcije za clanarine (R5 #3):
- Checkbox po retku + master + "Sve nepladene" gumb
- Bulk bar pokazuje selected count + total dug
- "Pošalji opomenu" → POST /bulk/notify (sa specifičnim ids ili sve dužnike)
- "Generiraj uplatnice" → POST /bulk/uplatnice → modal s linkovima na PDF/QR
XLSX export (R5 #4):
- "📥 Export XLSX" gumb na Članovi tab → otvara /clanovi/export.xlsx
s trenutnim filterima (klub_id, q)
- /api/crm/clanovi/export.xlsx: fix col_letters list construction (str+list bug)
- /api/crm/lijecnicki/notify-scan: dodan include_expired=True bucket, jasniji
subject za already-expired vs uskoro istek
CC2 commit 0046b8d je već unio crm_extras_router.py na master; ovaj commit
samo sređuje bugove i extends scan logiku.