Commit Graph

53 Commits

Author SHA1 Message Date
Damir Radulić 6e5ada8517 Merge agent3-payments: SEPA + CSV import + match workflow 2026-05-05 18:35:01 +02:00
Damir Radulić 47df057270 Merge agent2-putni: Putni nalozi CRUD + status workflow 2026-05-05 18:34:56 +02:00
Damir Radulić c4640ca3af Merge agent1-ocr: OCR u ERP/CRM 2026-05-05 18:34:46 +02:00
Damir Radulić 9b0ed43b92 RUSH 4-sub: filteri Klubovi/Sportaši + manifestacije card view + CRM v2 redesign
RUSH-1 Klubovi: list_klubovi() LEFT JOIN v_klubovi_financiranje (prima_pgz/rss/grad_rijeka, u_godisnjaku, ukupno_potpora). financiran=true sad OR od 3 davatelja (drop legacy klubovi.pgz_sufinanciran s 1312 false-positive). Sort sort=potpora&order=desc. UI: gold ukupno_potpora + tooltip + sortable kolona. Defaults priority view (financirani+godišnjak ON, hns_roster OFF). Test: priority=604, +hns=36, all=1641, financiran=15 sorted ZAMET 80208€.

RUSH-2 Sportaši: SELECT widened (slika_url, reprezentativac, kategoriziran, broj_dresa). avatarUrl() helper s 3 forme (apsolutni / lokalni /sport/uploads/avatars / initials fallback) + 32px circular avatar lijevo od imena. Test: priority=3712, no-priority=6086, +hns=1439, 1990-2000=645.

RUSH-3 Manifestacije: bugfix razina filter HTTP 500 (ambiguous column nakon LEFT JOIN savezi → m.razina/mjesto/organizator). 3 dropdowna iz meta (26 mjesta / 8 razina / 50 organizatora), view toggle 🃏 Kartice / 📋 Tablica (localStorage), 🔗 link ikona u card+table, source_url → Google fallback. Test: default=3, mjesto=Lošinj=2, razina=Tradicionalna=3, organizator=AK Kvarner=1.

RUSH-4 CRM v2: tab strip rewrite (10 taba u spec redu Članarine|Liječnički|Obrasci|E-mail|Accounts|Contacts|Leads|Opps|Activities|Cases, sticky+scrollable+gold underline). Pipeline → Opps tab. Novi e-mail templates tab (5 endpointa, 3 seed templates, +Novi modal). Card layout (.cgrid/.ccard) za Accounts/Contacts/Leads/Opps. Export dropdown 📥 ▾ CSV/XLSX(SheetJS CDN)/PDF na svaki tab. Test: /crm_v2 200, 10/10 tab labela, 10 Export dropdowna + 31 exportTab() handlera.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 18:33:20 +02:00
Damir Radulić 55a27fb315 Task 3: Plaćanja — POST/PATCH + CSV batch import + SEPA XML mock
- routers/erp_full_router.py: POST/PATCH/import-csv/sepa-export
- static/erp_full.html: high-end UI s match workflow + SEPA export + summary tiles

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 18:29:51 +02:00
Damir Radulić efa15d0086 Task 2: Putni nalozi — full CRUD + status workflow
- routers/erp_full_router.py: GET/POST/PATCH/DELETE /api/v2/erp/putni-nalozi
  - status workflow: draft → poslano → odobreno/odbijeno → isplaceno
  - cost_total auto-calc, approved_at/paid_at on transitions
  - alias under /expense-reports/* preserved
- static/erp_full.html: novi UI lista + modal + status buttons

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 18:28:53 +02:00
Damir Radulić f488623920 Task 1: OCR u ERP/CRM — /api/ocr/upload + tab Računi (OCR)
- routers/ocr_router.py: POST /api/ocr/upload (Tesseract+pdf2image, regex field extraction)
- pgz_sport_api.py: mount ocr_router with try/except guard
- static/erp_full.html: nova tab "📷 OCR" + panel
- static/crm_v2.html: OCR upload modal/tab

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 18:28:22 +02:00
damir b72d037141 CRITICAL FIX (Slika 11, 12): /api/v2/auth/me alias + frontend fix
Bug: crm_v2.html, admin_users.html, ostali pozivali /api/v2/auth/me
koji ne postoji u backendu (postoji /api/auth/me bez v2).
401 redirect na /login?reason=unauthorized iako Damir prijavljen.

Fix:
- Frontend: replace /api/v2/auth/me → /api/auth/me u svim file-ovima
- Backend: dodan defensive alias @app.get('/api/v2/auth/me')
2026-05-05 18:25:52 +02:00
damir 8127e2ef22 Slika 3: Savez 3 KPI (ukupna potpora, sportaša, dokumenata)
Endpoint:
- /api/v2/savez/{id}/kpi (ukupna_potpora, broj_sportasa, broj_klubova, najvisi_rang, broj_dokumenata, broj_manifestacija)

Frontend sport2.html:
- loadSavezKpi() function
- Auto-call after openSavez(id) panel render
2026-05-05 18:24:05 +02:00
damir 7608839473 Auth fix: apiPost/apiPut/apiDelete uses Bearer token
sport2.html:
- apiPost: localStorage pgz_access → Authorization: Bearer
- apiPut, apiDelete added
- Better error toast

Login redirect (multiple files):
- Wrap auto-redirect in __pgz_made_api_call check
- Don't redirect on initial page load if user has no API call yet
2026-05-05 18:22:52 +02:00
damir 1bc30d7881 /sport/dokumenti UI podrzava i rows i dokumenti response key 2026-05-05 18:13:51 +02:00
damir 80ed621683 Frontend Financije: 4 dropdown (godina, davatelj, sport, vrsta) + listeners
sport2.html:
- loadFinancije: dynamic dropdown options from /v2/potpore/meta
- refreshFinancije: sport/vrsta/davatelj filter params
- 4 dropdown change listeners
2026-05-05 18:11:45 +02:00
damir a428363d42 V8 MEGA: meta endpoints + manifestacije + HNS V8 harvester batch
Endpoints:
- /v2/potpore/meta — dropdown options (sportovi, vrste, davatelji, godine)
- /v2/potpore/by-year — sport, vrsta filters
- /v2/manifestacije/meta — mjesta, razine, organizatori
- /v2/manifestacije — lista s filterima

HNS:
- 20 PGŽ priority klubova batch harvester pokrenut (HNK Goranin, HNK Orijent 1919, HNK Rijeka, NK Crikvenica, ...)
- ETA 30 min
2026-05-05 18:10:02 +02:00
damir f07fdad919 Crisis V7 MEGA: sufinanciranje_sport + panel + CRM auth
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
2026-05-05 15:02:47 +02:00
damir 007825acee Bug hunt V7:
DB:
- Aggressive je_klub=false flag for programs/treninzi/totals (>100K€ no klub_id)
- 53 ne-klubovi flagged false (RSS Rijeka ukupni, Stručni rad, Potpora loptačkim, etc)

Frontend (sport2.html):
- Panel back button (← Natrag) + history stack
- window._panelHistory + pushPanelState + panelBack functions
- closePanel resets history
2026-05-05 14:56:53 +02:00
Damir Radulić 1e611d59f1 HNS sprint: 3-tab drill-down + parallel deep scraper dispatch
HNS-1 verify: smoke test 93409 OK, gap 854 uncovered, throughput ~60/min
HNS-2 dispatch: scripts/hns_dispatch.sh + 5 parallel workers shard'd po roster ID; coverage 265→1098 distinct_seasons (93.7% of 1172 roster), 125→971 distinct_matches; total seasons 3170→13371, matches 23515→150071
HNS-3 UI: 6-tab panel collapsed na 3 (🏆 Karijera / 📅 Utakmice / 👤 Profil); novi /api/v2/clan/{id}/hns-matches?limit=30 + /clan/{id}/hns-profile (bio + summary + HNS deep link); prof-grid 3-col card s gold jersey badge; OIB RBAC-mask. Test Josip Zec 449: 72 sez/30 utak.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 14:24:05 +02:00
damir 448273945c /sport/* aliases u app: admin, dokumenti, crm/v2, erp/full 2026-05-05 14:13:32 +02:00
damir 360b8008ba Crisis V6: panel expand + klub matching + ne-klub filter + samo_klubovi default
DB:
- pgz_sport.potpore_nositelji.je_klub flag (false za RSS programs/savezi)
- Re-match klub_id case-insensitive trim normalize

Endpoint:
- /api/dashboard/top-primatelji: samo_klubovi=True default

Frontend:
- sport2.html #panel/#dpanel: 70vw / 1100px max-width za HNS karijera
- mobile responsive za panel
2026-05-05 14:09:47 +02:00
Damir Radulić ce544e660c 7-sub sprint UI residual: footer login + kalendar CRUD + notif center + CRM extra tabs
A: shared/sidebar.js footer onclick → handleFootClick (Guest→/sport/login, logged-in→logout()), a11y role+keyboard, popup link fix
B: app.html SECTIONS['kalendar'] kalOpenModal/Save/Edit/Delete + Akcije kolona, mock savez:kalendar maknut
C: app.html renderNotifCenter() (sve 4 role) + sidebar bell unread badge (30s poll)
F: crm_v2.html +443 linija — Članarine, Liječnički, Obrasci tabovi (split view + dynamic schema modal)
G: index.html minor + sidebar dokumenti link refresh

Note: backend (kalendar_router, notif_router, crm_router, erp_full_router uploads, dokumenti unified) već u commit f7b5114.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 13:55:33 +02:00
damir f7b5114f58 PDF link target=_blank + nginx timeouts + priority filteri (samo s podacima)
nginx (sport.rinet.one):
- proxy_read_timeout 60s → 300s
- proxy_send_timeout 300s
- proxy_buffering off (PDF stream)
- client_max_body_size 50M → 100M

Endpoints:
- /api/v2/klubovi/financirani: +with_data filter (samo s potporama/godišnjakom/HNS)
- /api/v2/sportasi/filtered: +samo_priority +samo_s_hns

Frontend:
- PDF link target=_blank rel=noopener
- window._klub_only_priority = true (default)
- window._sportas_only_priority = true (default)

DB View:
- pgz_sport.v_nogomet_priority (prima_potpore, u_godisnjaku, ima_hns_roster)
2026-05-05 13:51:07 +02:00
damir c6a5ec62aa Dashboard UI: davatelj dropdown + dynamic years + KORISNIK truncate + PDF link 2026-05-05 13:43:30 +02:00
Damir Radulić 16b980e842 6-sub sprint: Dokumenti+HNS profil+Admin+ERP+CRM+PGŽ filter
SUB1 Dokumenti: pgz:dokumenti SECTIONS handler u app.html (klikabilan grid 19 godišnjaka, PDF stream)
SUB2 HNS profil: sport2.html drill-down — bio-chips (visina/težina/noga/poz/dres) + HNS deep + Google + Wiki + 🏆 Karijera/📅 Utakmice tabovi (Josip Zec id=449: 257 nast/182 gol/15 sez)
SUB3 Admin Users: sidebar.js href fix /admin/users → /sport/admin/users + razriješen audit ID konflikt
SUB4 ERP Full: 5 novih endpointa (invoice-uploads, racuni/ulazni/{rid}/uploads, expense-reports, putni-nalog-racuni, payments) + 3 nova taba (📎 Uploads/OCR, ✈ Putni, 💰 Plaćanja) + inline stavke drill-down + sidebar entry
SUB5 CRM Salesforce-Lite: dodan crm_v2 sidebar entry (router 956 linija već mounted)
SUB6 PGŽ filter: 2 nova endpointa /api/v2/savezi/priority-sort + /api/v2/clanovi/priority-sort; togglePGZFilter wired u Klubovi/Savezi/Sportaši (sport2.html + app.html); 💰📖 badge prefix; klubovi 1536/1641, savezi 35/246, sportaši 4979/5499

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 13:17:56 +02:00
damir 1d02c0897d Sidebar: +ERP +CRM +Dokumenti, godišnjaci import (18 PDFs), filter helpers
- 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
2026-05-05 13:08:11 +02:00
damir e07292ba44 logout() proper fix: revoke backend + clear ALL session keys
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.
2026-05-05 09:24:12 +02:00
damir dd2f7daaf8 CRISIS V3: definitive apiAuth + mobile hamburger + Playwright E2E test
apiAuth in app.html:
- Pre-checks JWT exp client-side BEFORE making request
- On expired: clears localStorage + redirects /login?reason=expired
- On 401 from server: clears + redirects /login?reason=unauthorized
- Single-flight redirect via window.__pgz_redirecting flag

login.html:
- Toast for ?reason=expired (red) / ?reason=unauthorized (orange)

app.html mobile:
- Hamburger button injected into topbar (.tb)
- Mobile CSS: sidebar slide-in -280→0, backdrop overlay, full-width drill-down
- toggleMobileSidebar() global function
- @media (max-width:768px) display:inline-flex, sidebar fixed pos

scripts/playwright_e2e.py:
- Desktop test (1280x800): login, JWT persist, profile, logo, logout
- Mobile test (375x812 iPhone X): viewport, login flow, hamburger, no h-scroll
- Output: _audit/playwright_<TS>/results.json + screenshots/*.png

Reproducible: TS=YYYYmmdd_HHMM python3 scripts/playwright_e2e.py
2026-05-05 09:21:39 +02:00
damir 8e136351f9 CRISIS FIX: login flow + mobile responsive + token expiry handling
ROOT CAUSE ISOLATED:
Backend POST /api/auth/login, GET/PUT /api/auth/me, POST avatar, POST /logout
all return 200 OK (verified curl). Damirov problem is browser-side:
stale localStorage tokens that don't match current backend → 401 cascade
→ avatar upload appears as 'failed: 401' → profile changes 'lost'.

FIXES:
1. apiAuth() in app.html now:
   - Pre-checks JWT exp claim before request
   - On 401 response: clears localStorage (pgz_access/refresh/user) +
     redirects to /login?reason=unauthorized
   - On JWT expired: redirects to /login?reason=expired

2. login.html displays toast for ?reason=expired/unauthorized

3. Mobile responsive CSS (max-width: 768px):
   - app.html: hamburger menu, sidebar slide-in, full-width drill-down panel
   - sport2.html: KPI grid 2-col, klubovi 1-col, tables horizontal scroll
   - Both: viewport meta + media queries + touch-friendly buttons

4. Mobile menu toggle button + backdrop overlay added

VERIFIED E2E (curl):
- POST /auth/login → 200 + JWT
- GET /auth/me → 200 + telefon persisted
- PUT /auth/me → 200, DB row updated
- POST /auth/me/avatar → 200, file saved + avatar_url returned
- POST /auth/logout → 200, token revoked (next /me returns 401)
2026-05-05 09:14:46 +02:00
Damir Radulić 31e0374465 Dashboard top primatelji wired to live endpoint (default 2025, year filter)
- 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>
2026-05-05 09:11:47 +02:00
damir 4e4d69c04a DI exec: applied CC-DI Subagent A+B SQL — 3245 clanovi, Manuel Boras merged 2026-05-05 09:04:14 +02:00
damir 4fc8327789 R7+ orchestrator + CC3 logo home: combined patches
Orchestrator-side:
- routers/img_proxy_router.py: 4xx/5xx → 1x1 transparent PNG (eliminates cascade <img onerror>)
- static/sport2.html: removed standalone three.min.js (3d-force-graph bundles), bumped to 1.73.4

CC3 (before limit hit):
- Logo home link applied to ALL HTML pages (admin.html, admin_users.html, audit.html, crm.html, erp.html, kpi.html, login.html)
- Backups in _backups/*.cc3_pre_logo.$ts

CC4 R3 (before plan mode):
- _backups/r3_cc4/ocr.py.pre_S2.$ts

Audit screenshots (80 pages) committed to _audit/audit_20260505_023639/shots/
2026-05-05 08:20:07 +02:00
damir c38f15a566 R7+: 5x P0 demo fixes — HNS direct link, avatar cache, logo home, klub→sportaši, smarter enrichment
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
2026-05-05 02:24:30 +02:00
damir 67372d6c58 R7: GDPR /users/me/request-deletion alias + remove duplicate profileDeleteAccount
- 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
2026-05-05 02:06:34 +02:00
CC6 Worker bd5bbe71f2 M12.7 SB: enrichment SAVE button + toast + bulk + worker dashboard
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>
2026-05-05 01:46:28 +02:00
Damir Radulić 5cf9236d52 CC5 R6: ZIP batch HUB-3 + e-mail templates + /api/notifications/me
Backend (routers/crm_extras_router.py):
- POST /api/crm/clanarine/bulk/uplatnice.zip — generira ZIP archive sa
  HUB-3 PDF uplatnicama (filename: <KlubSlug>/<Prezime_Ime>-<id>-<godina>.pdf),
  + _manifest.txt + _manifest.json. Header X-Batch-Count = broj PDF-ova.
- pgz_sport.email_templates tablica (NEW) + 3 default templata seed-ana:
    clanarina_opomena, lijecnicki_podsjetnik, obrazac_potpis
- GET/POST/PUT /api/crm/email-templates — CRUD
- POST /api/crm/email-templates/{code}/render — popuni {{var}} → subject+body
- POST /api/crm/email-templates/{code}/send — mock send (upiše u notifications
  s channel=email + inapp)
- GET /api/notifications/me + /api/crm/notifications/me — user-scope unread
  notifs (resolva user_id iz JWT 'sub' ili X-User-Id headera, fallback =
  broadcast s user_id IS NULL); summary za badge

Frontend (crm.html):
- Bulk bar: + "🗜 Batch ZIP (PDF-ovi)" gumb (download blob s X-Batch-Count)
- Novi tab "📨 E-mail templates": lista s preview/edit/create modali,
  ▶ Preview render s test podacima per template, 📤 mock send
- API wrapper sad automatski šalje JWT iz localStorage 'jwt' ili
  'access_token'; quick-login fallback (damir@pgz.hr / PGZ2026!) na 401
  za POST/PUT zahtjeve. Avatar upload + ZIP fetch također passu Bearer.

5/5 live curl tests passed:
  ✓ /email-templates list (3 templata)
  ✓ /email-templates/lijecnicki_podsjetnik/render → subject+body
  ✓ /email-templates/obrazac_potpis/send → 2 notifs queued
  ✓ /clanarine/bulk/uplatnice.zip (50 IDs → 40 PDFs + 2 manifests, 354 KB)
  ✓ /api/notifications/me (X-User-Id:1 → user_id=1, 19 unread)
2026-05-05 01:45:45 +02:00
Damir Radulić f9ebcddf28 CC2 R6: middleware-wide JWT, avatar demo mode, mock mailer, login rate limit
#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.
2026-05-05 01:42:53 +02:00
Damir Radulić 3a79965899 CC3 R3: Sectioned sidebar redesign (DABI-style) — PORTAL/OPERATIVA/CRM/ERP/ANALITIKA/ADMIN
Reference: app.rinet.one/klasik/dabi — uppercase section headers + grouped items.

Shared module rewrite:
- /static/shared/sidebar.css   v2.0
   * 6 named sections, 240px expanded / 58px collapsed
   * Active item: gold left-border + transparent gradient fill
   * Hover: blue left-border accent
   * Section header hidden in collapsed mode (replaced with dashed separator)
   * Tooltip on hover (data-label) when collapsed
   * Mobile <768px overlay with backdrop
- /static/shared/sidebar.js    v2.0
   * SIDEBAR_SECTIONS = [PORTAL, OPERATIVA, CRM, ERP, ANALITIKA, ADMIN]
   * ADMIN section hidden unless user_type ∈ {pgz_admin, super_admin} (gated by /api/auth/me)
   * Cross-portal links (↗ marker) for items that target a different page
   * Same-page items trigger hashchange instead of full reload
   * Footer = avatar + name + role + ▾ user menu (Profil / Postavke / Public portal / Prijava ↔ Odjava)
   * localStorage 'sidebarCollapsed' persists across all 8 pages

Page integration:
- sport2.html  ← native .sb hidden; data-active=dashboard; hashchange→navTo
- app.html     ← native .sb hidden; data-active=profil; hashchange→navTo
- admin.html   ← native .sidebar hidden; data-active=korisnici
- erp.html     ← native .sidebar hidden; data-active=racuni
- crm.html     ← data-active=clanarine
- audit.html   ← data-active=audit (existing)
- kpi.html     ← data-active=kpi (existing)
- login.html   ← data-active=login (no item match → no highlight; user menu shows Prijava)

Backups: _backups/*.cc3_pre_redesign.{TS}

Live verified: all 8 pages HTTP 200; shared sidebar.css 200 (8664 B); sidebar.js 200 (12678 B); 6 sections present.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 01:42:16 +02:00
Damir Radulić 7e674ad1ec CC5 R5 UI: Kalendar + Stats + Notifs + bulk akcije + XLSX export
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)
2026-05-05 01:36:45 +02:00
CC4-PGZ-Sport 6752ecaf07 R5 ERP: bulk ops + XLSX export + HUB-3 PDF + stats + m2m + UI
Backend:
- pgz_sport.putni_nalog_racuni (m2m) — backfill iz attachments.invoice_ids
- erp/putni_nalozi.py:
  * GET /putni-nalog/{id} sada vraća invoices (m2m) + suggested_invoices (auto-suggest po
    klubu/datumu, ne-vezani)
  * POST /putni-nalog/{id}/attach-invoice {invoice_id, kategorija}
  * DELETE /putni-nalog/{id}/invoice/{invoice_id}
  * GET /putni-nalog/{id}/hub3.pdf — A4 HUB-3 uplatnica + EPC QR (reuse crm.payments.build_hub3_pdf)
- erp/ocr.py:
  * POST /invoices/bulk-pay  {ids:[], paid_date, payment_method, iban_*, reference, tx_id}
  * POST /invoices/bulk-cancel  {ids:[], razlog}  (audit per record)
  * GET /export/invoices.xlsx — openpyxl, 17 stupaca (datum, izdavatelj, OIB, klub,
    neto/PDV/brutto, status, IBAN, opis, kategorija); permission filtered
  * GET /stats — month/quarter/year totals, by_kind breakdown, top_klubovi, putni_nalozi totals

UI (static/erp.html):
- Novi tab "📊 Statistika" (default) — 3 KPI kartice (mjesec/kvartal/godina) za račune
  + putne naloge, top klubovi godina, klub filter, Export XLSX gumb
- Računi tab: bulk toolbar (checkbox per row + Select All) → Plati sve modal
  (IBAN platitelja, datum, ref) / Otkaži označene (prompt razlog) / Export XLSX
- Putni-nalog detail modal: novi gumb "📄 HUB-3 uplatnica (PDF)"
- klub selector bonus za stats tab

Live tests (8/8):
- GET /erp → 200, 61.5 KB
- /api/erp/stats month=€63.15 / pn_year=€455
- /export/invoices.xlsx → 200, application/vnd.ms-excel, valid PK header
- /putni-nalog/1/hub3.pdf → 200, application/pdf 53562 B (%PDF-)
- /attach-invoice → ok, link_id=1
- /bulk-pay {ids:[1]} → skipped:1 (već plaćen)
- /bulk-cancel {ids:[999]} → 0/0 (ne postoji, tolerantno)
- Suggested invoices vraća praznu listu nakon attach

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 01:32:05 +02:00
Damir Radulić 8dce58c5f9 CC3: Unified sidebar with external portal links + collapsible icon mode
Shared module:
- /static/shared/sidebar.css   ← unified CSS (#pgz-sb, .pgz-collapsed, mobile overlay, tooltip)
- /static/shared/sidebar.js    ← auto-mounting JS shell + PGZSidebar API
   * Auto-renders #pgz-sb na <body> start (data-inline=1 to opt out)
   * NAV_EXTERNAL: Prijava, Aplikacija, Administracija, CRM, ERP, KPI, Audit, Public portal
   * Toggle (≡) -> localStorage 'sidebarCollapsed' (perzistira preko SVIH stranica)
   * Mobile <768px: ≡ burger + ✕ close, body backdrop
   * Loads /api/auth/me u footer (avatar/username/uloga); ⎋ logout briše JWT i ide na /login
   * data-active="<key>" highlight aktivnog portala

Page integration:
- sport2.html  ← inline NAV_EXTERNAL u buildNav() + "Portali" separator (zadrži postojeći sidebar)
- app.html     ← inline NAV_EXTERNAL u buildNav() (zadrži role-based interni nav, dopuni Portalima)
- admin.html   ← Portali stavke u <aside class="sidebar"> (matching .nav-item style)
- erp.html     ← Portali stavke u <aside class="sidebar"> (matching .nav-item style)
- crm.html     ← include shared sidebar.css + sidebar.js  data-active="crm"
- audit.html   ← include shared sidebar.css + sidebar.js  data-active="audit"
- kpi.html     ← include shared sidebar.css + sidebar.js  data-active="kpi"
- login.html   ← include shared sidebar.css + sidebar.js  data-active="login"

Backups: _backups/{*.cc3_pre_unified_sidebar.*}

Live verified: 8 pages serve HTTP 200; sidebar.css/js HTTP 200; portal markers per page OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 01:11:24 +02:00
Damir Radulić bd3773434e CC2 R4 #6: real TOTP 2FA (setup + verify + disable + login flow)
- auth/auth_v2.py:
  - pyotp-based TOTP (RFC 6238, base32 secret, ±30s window)
  - new pgz_sport.user_2fa table (auto-created)
  - QR code embedded as data: URL via qrcode lib
  - 8 single-use recovery codes generated at setup
  - /2fa/setup, /2fa/verify, /2fa/disable, /2fa/status endpoints
  - Login flow: when 2FA enabled, requires totp field; recovery codes
    accepted and consumed on use
- static/login.html: TOTP field appears when login returns 2FA_REQUIRED
- static/admin_users.html: full 2FA panel in Sigurnost tab
  (status badge, QR + secret + recovery code display, verify input)

Live tests pass:
  T1 status (no setup) → enabled:false
  T2 setup → secret + 1.5KB QR PNG + 8 recovery codes
  T3 verify wrong code → 401
  T4 verify real TOTP → enabled:true
  T5 login w/o TOTP after enable → 401 detail=2FA_REQUIRED
  T6 login w/ TOTP → 200
2026-05-05 00:50:28 +02:00
Damir Radulić a0db65fc31 CC2 R4 #4: /api/users/me/gdpr-export alias
- 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)
2026-05-05 00:47:22 +02:00
Damir Radulić f5c6570d47 CC2 R4 #2+#5: remove legacy unauth /api/admin/users — close 401 gap
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.
2026-05-05 00:44:50 +02:00
Damir Radulić 47c366de7e CC5 R3 UI: link iz app.html sekcija na live /sport/crm workspace
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)
2026-05-05 00:23:34 +02:00
CC6 Worker c8be132e0f M11.2: /api/audit/seal endpoints + Audit log UI page
- 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>
2026-05-05 00:21:32 +02:00
Damir Radulić 8fe2478b84 CC2 R3 frontend: login.html + admin_users.html (M1+M2+M10 UI)
- static/login.html: dark Palantir-style login with PGŽ branding,
  Prijava se / Zaboravljena lozinka, demo account quick-fills,
  GDPR cookie banner, autostore tokens (local/session)
- static/admin_users.html: full user-management admin panel:
  - Collapsible left sidebar (Pregled, Korisnici, Tenanti, Audit log,
    Sigurnost, GDPR, links to ERP/CRM)
  - Users table with filters (q, tenant, role, status, limit)
  - + Dodaj korisnika modal (CRUD via /api/admin/users/*)
  - Suspend / unsuspend / reset-password / delete actions
  - Audit log viewer + Security KPIs + GDPR queue
  - Self-service: change pwd, export data (Art. 20), erasure request (Art. 17)
- pgz_sport_api.py: /login and /admin/users URL routes
- auth/seed_demo.py: added tajnik@atletski.pgz.hr/Atl2026!,
  admin@ak-kvarner.hr/Kvarner2026! demo users

5/5 live tests pass: login JWT, /me, /admin/users, /gdpr/consent, /gdpr/export

Note: existing admin.html (CC4 ERP/OCR work) preserved intact;
admin_users.html is dedicated user-mgmt page linked from sidebar.
2026-05-05 00:20:03 +02:00
CC6 Worker cef4d2575b M12.2 UI: enrichment diff modal + apply button (sport2.html)
- 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>
2026-05-05 00:17:52 +02:00
claude-cc1 fbbe953de3 CC1 R3B-Mreža M1+M2+M3 — autocomplete + 3D centar + forensic enrich
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>
2026-05-05 00:16:45 +02:00
Damir Radulić 59a537388d CC3 R3 M3+M4: sport2 sidebar + app.html operativna aplikacija
M3 (sport2.html):
- ≡ toggle gumb u sidebar headeru, .sb.collapsed -> 58px (samo ikone)
- localStorage 'sidebar-state' (expanded|collapsed)
- restoreSidebar() pri DOMContentLoaded, prije buildNav
- Hover tooltip na collapsed nav itemima preko data-label

M4 (static/app.html — novi):
- 4 dashboard varijante po roli: PGŽ admin, Savez admin, Klub admin, Sportaš
- Role switch u topbar-u (demo) + localStorage 'app-role'
- Sidebar collapse (M3 logika), tooltip-ovi na collapsed
- Sidebar footer s avatar/username/role i Odjava (⎋) gumbom
- Klikabilni KPI/cards -> detail sub-stranice (savezi, klubovi, financije...)
- PGŽ: KPI + zahtjevi pending + audit log + Chart.js trend grafikon
- Savez: klubovi grid + zahtjevi PGŽ + lijecnicki uskoro istek + kalendar
- Klub: clanovi tablica + clanarine + lijecnicki + dokumenti + manifestacije + HUB-3 placeholder
- Sportaš: profile card + clanarina + lijecnicki + ZZJZ link + obrasci za potpis
- Iste CSS varijable kao sport2.html (PGŽ blue/gold dark theme)
- Real API: /sport/api/dashboard, /api/savezi, /api/klubovi, /api/clanovi, /api/proracun
- Mock fallback gdje API još ne postoji (M5/M7/M9 produkti)

Backups: static/sport2.html.bak.cc3.m3*, static/app.html.bak.cc3.m4*

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 00:16:29 +02:00
claude-cc1 98f823b4d9 CC1 R3B-P4 — Forenzika scan radi
Backend:
- enrich_router.py: POST /api/v2/forensic/scan {name} → searches civic.persons,
  joins person_entity_links, scans forensic_findings (by OIB + by name),
  synthesises per-person risk score (PEP function +30, party +15, links +5×, findings +10×, crit +20).
- Forced PG_HOST to 10.10.0.2 when env says localhost (local PG disabled).

Frontend:
- New scan card with name input + "Pokreni" button on Forenzika section.
- Renders matched persons with risk score, links, findings.
- Test: "Velimir Liverić" → 2 osoba, 2 linka, OIB 91528083847 found.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 00:10:21 +02:00
Damir Radulić 492c8fdd87 M1+M2+M10 (CC2 R3): JWT auth + admin users + GDPR backend
- auth/auth_v2.py: JWT login/refresh/logout/me + bcrypt + tenant_id/role/tier claims
- auth/admin_users.py: /api/admin/users CRUD + invite/role/suspend + bulk CSV
- auth/gdpr.py: cookie consent + Art.20 export + Art.17 erasure + admin queue
- auth/seed_demo.py: 3 demo tenants + 4 users (damir@pgz.hr / PGZ2026!)
- Removed legacy /api/auth/login + /api/auth/me from pgz_sport_api.py
- Wired auth/admin/gdpr routers into FastAPI

5/5 live curl tests pass: damir@pgz.hr login → JWT with tenant_id=1, role=pgz_admin, tier=0
2026-05-05 00:09:09 +02:00
claude-cc1 382d35af30 CC1 R3B-P2 — Mreža 3D force graph (replace D3 2D)
- Add three.js + 3d-force-graph CDN script tags
- Replace renderMrezaGraph with ForceGraph3D() implementation
- onNodeClick: center camera + open detail panel
- onNodeHover: cursor swap (grab ↔ pointer)
- ResizeObserver for dynamic container sizing
- Rich HTML node labels with risk score
- Hint overlay: drag rotate, scroll zoom, right-drag pan

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 00:02:29 +02:00