- 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.
- data/sport_federations.json: 24 Croatian sport federations + aliases +
PGŽ local media (Novi list, Glas Istre, Rijeka.danas).
- enrich_router._sport_fed/_normalize_sport/_load_sport_feds: cached
loader that picks up file changes via mtime.
- _research_links() now sport-aware: when row.sport maps to a known fed,
the dynamic links list shows that fed (national + PGŽ regional) plus the
three PGŽ local-media search URLs in place of the static HNS Semafor +
transfermarkt fallback.
- scrape_sport_federation(sport, ime, prezime): generic profile-page
scraper (slug pattern OR search-results crawl) → returns
{profile_url, slika_url, datum_rodenja, mjesto_rodenja, klub_naziv}.
- _propose_for_sportas() now routes through the federation scraper before
HNS Semafor; HNS path is gated to nogomet or rows already linked.
- _load_row(sportas) JOINs klubovi to fall back to klub.sport when
c.sport is empty.
- Tested on 1024 Marijan Alkić (boćanje): proposed profile_url +
datum_rodenja from hrvatski-bocarski-savez.hr; /apply persisted them.
- Tested on 3335 Toni Jelenković (košarka) and 3379 Niko Miknić
(plivanje): research_links surface HKS/KS PGŽ and HPS respectively.
Worker:
- _pick_sportas now selects on coverage<70 across ALL sports (sport
set OR known external linkage), not just hns_*.
- _SOURCE_WEIGHTS extended with 16 federation hosts at 0.88-0.92.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New auth.gdpr.me_router prefix /api/users/me with:
- GET/POST /gdpr-export → Art.20 JSON download with Content-Disposition
- POST /gdpr-erase → Art.17 erasure request
- GET /gdpr-consent → consent history for caller
- jsonable_encoder fixes datetime serialisation in JSONResponse
- admin_users.html: 'Izvezi moje podatke' now POSTs to alias and uses
filename from Content-Disposition header
- 401 enforced on no-auth, 200 on valid Bearer (verified live)
- enrich_apply now imports audit_seal_router.audit_log and writes a sys_audit
row after every successful UPDATE: action='enrich.apply', target_type=kind,
target_id=eid, payload={applied: {...}, sources: [...]}, user from headers.
- Other modules (cc2 users, cc4 invoices/putni_nalozi, cc5 clanarine/lijecnicki/
obrasci) can call the same helper:
from audit_seal_router import audit_log
audit_log(action='users.update', target_type='users', target_id=u['id'],
payload={'changed':[...]}, user_email=actor)
- Verified: real apply on klub 4528 produced sys_audit id 102.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Coverage computed in SQL (filled_keys * 100 / total_keys); only rows below
threshold (default 70%, override ENRICHER_COVERAGE_MAX) are queued.
- Per-row confidence is the max of source weights (semafor.hns.family=0.95,
wikipedia.hr=0.80, sport-pgz.hr=0.55) plus a small evidence-count bonus.
Below threshold (default 0.70, override ENRICHER_CONFIDENCE), only 'hard'
structured fields (profile_url, source_url, slika_url, hns_igrac_id) are
applied — never an LLM-synthesised biografija.
- Logs now mirrored to /var/log/pgz-sport-enricher.log alongside the project
log, so 'tail /var/log/pgz-sport-enricher.log' works as the brief asks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- GET /sport/api/audit/log?limit=&action=&resource=&q=&user=&since=
Filters pgz_sport.sys_audit; returns normalised items list + total count.
Aliases target_type → resource_type for the audit.html UI.
Lifts tx_hash from payload.tx_hash / polygon_tx / seal_tx_hash.
- GET /sport/api/audit/stats — {total, today, sealed, users}
sealed counts rows whose payload jsonb has tx_hash key (or polygon_tx).
- audit_log() shared helper for cc2/cc4/cc5/cc6 to call after DB writes.
Fail-soft: never raises, writes traceback to stderr if insert fails.
trg_audit_chain on table fills row_hash + chain_idx automatically.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bare @app.get/post('/api/admin/users') decorators in pgz_sport_api.py
were registered before app.include_router(admin_users_router) and shadowed
the JWT-protected M2 routes, leaking user list to anyone.
Removed all three: GET /api/admin/users, POST /api/admin/users,
POST /api/admin/users/{uid}/toggle. The auth.admin_users router now owns
this prefix exclusively and gates every method with require_user.
Verified: no-auth → 401, invalid token → 401, valid Bearer → 200.
Critical bug fix: /v2/enrich/sportas/{id} returned proposed:{} for athletes
because the v3 pipeline was still relying on Wikipedia-only evidence and never
actually fetched semafor.hns.family/igraci/.
- enrich_router._propose_for_sportas now:
• Resolves a HNS Semafor URL from profile_url, source_url, hns_igrac_id,
vanjski_id JSONB ('hns_comet'+'hns_slug'), or source='hns_semafor'+source_id.
• Fetches and parses the player page (BS4, regex fallback) and proposes
profile_url, source_url, slika_url, hns_igrac_id, datum_rodenja,
mjesto_rodenja, broj_dresa, biografija (DeepSeek synthesis from HNS+Wiki).
- _load_row(sportas) widened to read every relevant column + vanjski_id.
- _TABLE_MAP['sportas'] writeback whitelist expanded to 12 fields.
- workers/enrichment_worker.py: 24/7 daemon, picks under-enriched
clanovi/klubovi/savezi every 5 min via SQL, calls /apply for each.
- systemd unit pgz-sport-enricher.service installed + enabled.
- Tested end-to-end: id=2222 (Abdija) and id=449 (Zec) now have
profile_url, slika_url, hns_igrac_id, biografija persisted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Standalone /sport/crm stranica (static/crm.html) već je deployana s prethodnim
commit-om (CC2). Ovaj commit dodaje vidljivi link u 5 SECTIONS handlera u
app.html (pgz:crm, klub:clanarine, klub:lijecnicki, sportas:lijecnicki,
sportas:obrasci) tako da klikom na taj gumb korisnik dolazi do live tablica
(M7 + M8 + M9):
- Tablice s filterima (status / godina / klub / vrsta pregleda)
- Action gumbi: registriraj uplatu, generiraj HUB-3 PDF, EPC QR, zakaži pregled
preko ZZJZ PGŽ (online ili e-mail fallback), popuni i potpiši obrazac
- Live PDF generator za uplatnice (HUB-3) i potpisane obrasce (sa SHA-256)
Mock SECTION sadržaj zadržan radi instant-pregleda u app.html;
puna funkcionalnost iza linka.
Live curl tests passed (5/5):
✓ /api/crm/clanarine + summary
✓ /api/crm/clanarine/{id}/uplatnica.pdf (52 KB %PDF)
✓ /api/crm/lijecnicki/uskoro-isticu (11 istekli)
✓ /api/crm/zzjz/info (live scrape; available=False, fallback=email)
✓ /api/crm/forms + draft + submit + sign + PDF (45 KB %PDF)
- enrichment/playwright_scraper.py: fetch_rendered(), scrape_sport_pgz_klub(),
scrape_federation(). Headless Chromium, 12s timeout, returns rendered text.
Import-safe when playwright is missing.
- enrich_router._sport_pgz_search() now falls back to the JS path when the
cheap urllib fetch returns empty or unparseable HTML.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- routers/audit_seal_router.py exposes:
POST /api/audit/seal (record + seal an audit event)
GET /api/audit/seal/list (recent seals for UI)
GET /api/audit/seal/{id} (single seal + onchain receipt cross-check)
- pgz_sport_api.py mounts the router under /api.
- sport2.html: new 'Audit log' nav item (🔒) and full page that surfaces
wallet, chain, live/pending mode, count, and a table of every sealed
event with polygonscan.com tx links.
- Verified end-to-end: sealing 'sufinanciranje.approved' for klub 3 lands
in pgz_sport.polygon_seals (pending mode — no POLYGON_PRIVKEY in env).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- enrichEntity() now renders {current, proposed} as a diff table with a
checkbox per field (defaults to checked).
- 'Označi sve' / 'Poništi sve' / '💾 Spremi izmjene' buttons.
- enrichApply() POSTs selected fields to /v2/enrich/{kind}/{id}/apply
with the cached source list, then refreshes the entity panel and
re-runs preview so the now-saved values are visible inline.
- Toast '✓ Spremljeno N polja u bazu' confirms the write.
- '✓ Obogaćeno YYYY-MM-DD' badge surfaces metadata.enriched_at.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
M1 (default centar):
- Augment /api/v1/presenter/graph-real with synthetic 'pgz-savez-nogometni' anchor
(PGŽ gold, size 40), connected to top 3 person + top 3 entity nodes
- centerMrezaOnAnchor() called 1.5s after render and via "🎯 Centar (PGŽ)" button
M2 (autocomplete):
- Backend GET /api/v2/search/suggest?q=&type=person|club|company
Searches pgz_sport.klubovi, pgz_sport.savezi, pgz_sport.clanovi,
civic.persons, civic.entities; returns 20 results max
- Frontend: 3 inputs get keydown+input handlers, dropdown UI under each
Enter → first suggestion, click → suggestion, blur → close
- centerMrezaOnSuggestion: finds existing node by label, or injects new node
+ edge from anchor and re-renders
M3 (forensic enrich):
- Backend POST /api/v2/forensic/findings/{id}/enrich
Extract person name from entities_involved or title regex,
hit hr.wikipedia.org REST summary, persist into raw_data.enrichment
- Frontend: forensicEnrichBlock + customFindingEnrichBlock added to alert
panel and custom-finding panel (Liverić). Custom uses direct Wikipedia
fetch since they're not in DB.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- POST /v2/enrich/{kind}/{eid} now scrapes Wikipedia HR + sport-pgz.hr +
primary site, runs relevance filter so contact info from off-topic pages
isn't lifted, optionally calls DeepSeek for opis_djelatnosti, returns
{current, proposed, sources, last_enriched_at} for diff UI.
- POST /v2/enrich/{kind}/{eid}/apply UPDATES klubovi/savezi/clanovi for
whitelisted empty fields, sets metadata.enriched_at +
metadata.enrichment_source + metadata.enrichment_history, writes a row
to pgz_sport.enrichment_log (new table).
- GET /v2/enrich/log read-back endpoint.
- Tested on klub 3 (KK Kvarner 2010): opis_djelatnosti persisted; metadata
carries enriched_at + sources.
- New tables/columns: pgz_sport.enrichment_log; metadata jsonb on klubovi/savezi.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- erp/putni_nalozi.py: FastAPI router /api/erp/putni-nalog
- GET /preview: live obračun dnevnica + kilometrine za UI
- POST /putni-nalog: kreiraj (draft) iz UI forme (voditelj, putnici, od→do, km)
- PUT /putni-nalog/{id}: izmjena s recompute dnevnica
- POST /putni-nalog/{id}/odobriti: status=odobren
- POST /putni-nalog/{id}/zatvori: linkanje računa (invoice_ids), končan obračun
- HR 2025: domaće 30 € (>8h), 15 € (5–8h), 0 € (<5h); inozemne po zemlji (NN tablica)
- km × 0.50 €/km (neoporezivi limit 2025)
- Live test: Rijeka→Zagreb 3 dana = 3 dnevnice × 30 € + 380 km × 0.50 € = 280 € prije računa, 455 € sa hotelom+meals
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>