Commit Graph

73 Commits

Author SHA1 Message Date
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 9fb512932a HNS+UI: 4 nova endpointa + multi-sport schema (M2M kategorije + player_stats)
Endpoints:
- GET /api/v2/enrich-sources — sport→source mapping
- GET /api/v2/klubovi/priority-sort — financirani/godišnjak prvi
- GET /api/v2/clan/{id}/kategorije — many-to-many kategorije
- GET /api/v2/clan/{id}/full — kompletna slika (profil+kategorije+sezone+utakmice+stats)
- POST /api/v2/export/klubovi — XLSX export selektiranih

Schema:
- pgz_sport.clan_kategorije (M2M: igrač u juniorskoj+seniorskoj)
- pgz_sport.player_stats (multi-sport: nogomet/košarka/rukomet/odbojka/vaterpolo)
- pgz_sport.klub_roster (multi-source)
- pgz_sport.enrichment_sources (sport→izvor)
- View: v_pgz_priority_klubovi (financiran || u_godisnjaku)
- View: v_klubovi_priority_sort (priority sort)

Sport harvesters scaffold:
- scripts/sport_harvesters/__base.py (SportHarvester class)
- hks_basketball.py, hrs_handball.py, hos_volleyball.py, hvs_waterpolo.py
2026-05-05 10:42:49 +02:00
damir c68fd4471e HNS endpoints: /clan/{id}/hns-career + /klubovi/pgz-financirani + /dashboard/hns-coverage
Backed by: pgz_sport.hns_player_seasons, hns_klub_roster, v_pgz_financirani_klubovi
Used by: cc-hns subagents for UI integration
2026-05-05 10:22:36 +02:00
damir a20230187f Playwright logout test: dialog handler + wait_for_url for async flow 2026-05-05 09:24:59 +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 a0fb328029 Playwright E2E: better logout selector chain + JS fallback
Test now tries (in order):
1. .sb-foot .lo (topbar logout in sidebar foot)
2. .lo (any logout class)
3. #pgz-menu-logout (sidebar.js menu link)
4. a/button :has-text('Odjava')
5. JS fallback: window.logout() or PGZSidebar.logout()

Also: dialog handler accepts confirm() automatically.
2026-05-05 09:23:13 +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
CC Data Integrity 49ac2c0dc8 Data integrity sweep: clanovi clean — 3 HNS dups merged, 1 trim normalized, 4 constraints active
Subagent A: merged 3 HNS profile/roster duplicate pairs (3243 → 3240 rows).
  Authoritative auths preferred /igraci/ source_url over /klubovi/ roster scrape.
  Manuel Boras Mandić (id=481) reconciled — pozicija=Vratar, hns_igrac_id=436387.
Subagent B: 1 trim auto-applied (id=634); 4 ALL CAPS held for manual review.
Subagent C: 0 strict cross-klub transfers; 56 soft groups in review queue.
Subagent D: 4 constraints applied (no_camelcase, trimmed, hns_uniq partial, normalize trigger);
  2 skipped (length>=2 — 22 historical violators; klub+name+dob unique — 68 NULL-DOB groups).

Backup: pgz_sport.clanovi_backup_20260505_0836 (3243 rows untouched).
Audit: 5 sys_audit rows (3 PURGE, 1 NORMALIZE, 1 C_DETECTION_RUN).
Smoke: 5/5 endpoints 200; HNK Lovran 31 → 30 clanovi confirmed.

Full report: _audit/data_integrity_20260505_0836/CONSOLIDATED.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 09:08:35 +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 e7102c720d Dashboard top-primatelji: psycopg2 LIKE escape fix (%% in CASE WHEN)
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).
2026-05-05 09:01:25 +02:00
damir b95b2e8423 BIG FIX: profile save + sport classification + KUD separation
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
2026-05-05 08:57:09 +02:00
CC4 125ba6dbfb CC4: fix 3 outstanding bugs (dokumenti dup, upload, kategorizirani SQL)
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>
2026-05-05 08:52:47 +02:00
CC1 3e60e5095a CC1 audit fixes #6/#8/#9 from CONSOLIDATED.md
#9 sportas trailing-slash (verified clean):
  Frontend constructs /sportas/{id}/profil cleanly (sport2.html:L1582).
  Live test: 200 on /sportas/449/profil; 307 on trailing-slash variant
  (FastAPI auto-redirect, harmless). No fix needed.

#8 [VERIFY]/[UNRESOLVED] klubovi surfaced to audit log:
  3 manual_review klubovi (2619 Čavle, 2630 Opatija, 4426 empty) inserted
  into pgz_sport.sys_audit with action='klub.manual_review_pending',
  visible in /audit page for human triage. Total audit rows: 633.

#6 backup-table archival:
  Moved 26 *_backup_*/*_premerge_*/*_pre_*/*_dedup_*/*_deprecated_*/*_garbage_*
  tables (~97k rows) from pgz_sport → pgz_sport_archive schema.
  - Snapshot dump: _audit/db_snapshots/backup_tables_20260505_085057.sql.gz (56 MB)
  - Script: scripts/archive_backup_tables.sql (idempotent, with rollback)
  - pgz_sport canonical tables: 112 → 86
  - All live API endpoints still 200, db=ok, errors_logged stable at 23
  - Josip Zec test 257/182/15 still PASS

.gitignore: exclude _audit/db_snapshots/ (large pg_dump archives)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 08:52:07 +02:00
damir aad034a59d PHASE 5: sidebar Debug link + swarm_monitor.py daemon
- static/shared/sidebar.js: '🩺 Debug' link in pgz_admin sidebar
- scripts/swarm_monitor.py: detects stuck/idle CC agents,
  Telegram alerts on session expired or limit prompts
- pgz-swarm-monitor.service running 60s checks

Full debug stack now active:
- pgz-debug-tail: error stream
- pgz-auto-triage: pattern → CC dispatch
- pgz-swarm-monitor: agent health
- /api/debug/* dashboard
2026-05-05 08:48:02 +02:00
damir 52db3d91a4 DEBUG observability: router properly mounted before root() handler 2026-05-05 08:47:10 +02:00
damir 63ca005b6e DEBUG OBSERVABILITY: live error feed + auto-triage bot + dashboard
PHASE 1 — DEBUG mode:
- /etc/systemd/system/pgz-sport.service.d/debug.conf: DEBUG=1, LOG_LEVEL=DEBUG, PYTHONUNBUFFERED=1, UVICORN_LOG_LEVEL=debug

PHASE 2 — Error stream:
- /opt/pgz-sport/scripts/debug_tail.sh: tail journalctl + nginx → /var/log/pgz-sport-debug/{stream,errors}.jsonl
- pgz-debug-tail.service (always restart, multiplexes 4 sources)

PHASE 3 — Auto-triage bot:
- /opt/pgz-sport/scripts/auto_triage.py: classifies errors, dispatches CC agents
- Patterns: 5xx spike → CC4, 401/403 spike → CC2, 4xx API → CC3, ImportError/DB → CC4
- Rate limit: 6 telegram/5min
- Records decisions in triage_decisions.jsonl
- pgz-auto-triage.service

PHASE 4 — Live dashboard:
- routers/debug_router.py mounted in pgz_sport_api
- GET /api/debug/health — services + DB + error count
- GET /api/debug/errors?limit=N — last N errors (JSON)
- GET /api/debug/decisions — auto-fix decisions
- GET /api/debug/stream — full log tail
- GET /api/debug/dashboard — live HTML refresh 5s

Damir admin tier dashboard: https://sport.rinet.one/sport/api/debug/dashboard
2026-05-05 08:46:09 +02:00
CC1 7adcec3309 CC1: 3-subagent deep audit — Frontend / API gap / DB integrity
Reports in _audit/:
  audit_FRONTEND_COVERAGE.md  — SA-1 (Explore): 9 HTML files, 0 orphan handlers (clean)
  audit_API_GAP.md            — SA-2 (Explore): 356 backend routes vs 54 frontend paths
                                                23 missing routes / 39 call sites
  audit_DB_INTEGRITY.md       — SA-3 (general-purpose): 8 SQL probes, FKs/NULLs clean,
                                                        48 dup-OIB clusters, 518 low-cov klubovi
  audit_CONSOLIDATED.md       — top 10 critical with owner matrix (cc1/cc4/cc5/cc6)

Headlines:
  Frontend: clean (post-R3 refactors landed)
  API gap:  CRM module systemic — 16 of 23 missing routes need /crm prefix in crm.html
            6 missing routes are trailing-slash bugs in crm.html
  DB:       48 OIB dup clusters in klubovi (~100 rows) need merge+unique-index
            518/2244 klubovi (23%) <33% coverage → enrichment_worker target list
            14 scoreboard-string klubovi rows (RK ... HRL Zapad od X) → DELETE
            ~30 backup tables (~97k rows) cluttering pgz_sport schema

Owner allocation:
  cc1 → #6 backup-table archival, #8 verify, #9 sportas trailing-slash
  cc4 → #1 OIB dedup script, #4 scoreboard DELETE, #10 schema CHECKs
  cc5 → #2 /crm prefix sweep on crm.html, #3 trailing-slash sweep, #7 notif endpoint
  cc6 → #5 enrichment_worker batch on filled<4 klubovi

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 08:29:04 +02:00
CC4 3e5b98a935 CC4: 3-subagent backend hardening done + CRM audit_log fix
Sub1 (commit eb1b49f): 4 v2 listing/discovery endpoints + SQL fix
Sub2: CRM 4 modula PASS (M7 članarine, M8 liječnički, M9 obrasci, dokumenti partial)
Sub3: ERP 4 modula GREEN — racuni/putni/placanja/xlsx, E2E demo flow (7 steps) PASS

Critical fix this commit:
- erp/audit_helper.py (centralni helper za audit_log writer)
- routers/clanarine_router.py: audit hook na POST /clanarine
- routers/lijecnicki_router.py: audit hook na POST /lijecnicki
- routers/obrasci_router.py: audit hook na POST /submissions + /submit

Verify: prije 0 / poslije 1 audit entry za POST /api/crm/clanarine
   "33|create|api|clan=4946 klub=2320 300.0€"

Outstanding (next round):
- /api/v2/dokumenti plain route shadowing with RAG
- /api/v2/dokumenti/upload missing
- SQL alias bug u pgz_sport_v2_router.py:3099

Reports:
  _audit/audit_CC4_FINAL.md  (konsolidirani)
  _audit/audit_CRM_VERIFIED.md
  _audit/audit_ERP_VERIFIED.md
  _audit/audit_ENDPOINTS_ADDED.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 08:28:49 +02:00
CC4-Sub1 eb1b49f0db 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
2026-05-05 08:23:28 +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
CC1 662f448590 CC1: Playwright audit 20260505_023639 — 57 errors across 80 pages
Outputs in _audit/audit_20260505_023639/:
  ERROR_REPORT.md  — markdown grouped by category & page
  errors.json      — structured 57 findings
  shots/           — 80 full-page PNG screenshots (ignored by git)
  run.log          — verbose trace

Sweep: 10 anonymous public URLs + 3 demo accounts (pgz_admin, savez_admin,
klub_admin) × 22 sidebar sections each (PORTAL/OPERATIVA/CRM/ERP/ANALITIKA;
ADMIN only for pgz_admin).

Categories:
  console_error   29
  console_warning 16
  http_4xx_5xx     8
  page_error       3
  empty_page       1

Hot spots (top 20 in flag file):
  ANALITIKA/an_mreza — 8 errors per role × 3 roles = 24 total (CDN/init issue)
  anon/public/erp    — 3
  PORTAL/portal_dashboard, portal_sportasi, CRM/crm_clanarine,
  ANALITIKA/an_financije — 2 errors per role × 3 = stable across roles

Flag: _audit/CC1_DONE_20260505_023639.flag with summary YAML for cc2-6 to consume.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 08:05:18 +02:00
CC4 e28c10d25b CC4: ERP — računi, putni, plaćanja, XLSX, OCR end-to-end verified
Demo dataset (S3): 5 INA gorivo, 3 Konzum office, 2 Telekom, 1 Hotel za PN
- count=13 invoices kinds={cestarina,gorivo,hotel,oprema,ostalo}
- year_total=€818.15, putni_nalozi_year=€639.62

Final E2E smoke (10/10 ✓):
1. GET /erp → 200 / 61.8 KB
2. GET /api/erp/invoices → 13 redova, 5 kinds
3. GET /api/erp/putni-nalozi (alias plural) → 200
4. GET /api/erp/placanja → 6 kandidata (neplaćeni računi + odobreni PN)
5. POST /api/erp/placanja → HUB-3 PDF 11.1 KB
6. GET /api/erp/placanja/invoice/2/pdf → 200 application/pdf
7. GET /api/erp/export/invoices.xlsx → 200 valid PK header
8. GET /api/erp/export/putni.xlsx → 200 valid PK header
9. GET /api/erp/stats → month=€278.15 year=€818.15 pn_year=€639.62
10. Branding: 3× "Ri.NET AI" / 0× DeepSeek u /erp

Coverage:
- /erp#racuni: GET list, POST create (s/bez OCR), DELETE (admin-only), filter ✓
- /erp#putni: GET, POST, PATCH approve|reject|submit|pay (s body.action) ✓
- /erp#placanja: GET kandidati, POST kreiraj HUB-3 (invoice|putni_nalog), GET pdf ✓
- /erp#xlsx: invoices.xlsx + putni.xlsx ✓
- OCR: INA gorivo → vendor=INA, OIB=27759560625, brutto=€63.15, PDV=€12.63, cat=gorivo ✓

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 08:03:04 +02:00
CC4 8c97a5b778 CC4 R7 ERP S2: DELETE invoice + /putni-nalozi alias + /placanja + /export/putni.xlsx
erp/ocr.py:
- DELETE /api/erp/invoices/{id} (samo pgz_admin) + cascade payment cleanup + audit
  (briše vezana payments, otkapča invoice_uploads.invoice_id NULL, audit log "delete")

erp/putni_nalozi.py:
- GET/POST /api/erp/putni-nalozi (alias plural od /putni-nalog) za CC1 brief kompatibilnost
- GET /api/erp/putni-nalozi/{id}
- PATCH /api/erp/putni-nalozi/{id} sa body.action: approve|reject|submit|pay (route kroz lifecycle)
- PATCH /api/erp/putni-nalog/{id} (singular alias)
- GET /api/erp/export/putni.xlsx — openpyxl 19 stupaca (klub, voditelj, ruta, datumi, km, dnevnice, ukupno, status...)
- GET /api/erp/placanja — lista neplaćenih računa + odobrenih putnih naloga (kandidati za isplatu)
- POST /api/erp/placanja {kind:invoice|putni_nalog, id, iban, model, opis, poziv_na_broj}
  → generira HUB-3 PDF + EPC QR (reuse crm.payments.build_hub3_pdf), pohranjuje u
  _data/uploads/placanja/{kind}_{id}_HUB3_*.pdf
- GET /api/erp/placanja/{kind}/{id}/pdf → streama zadnji generirani PDF, ili kreira on-demand
- Dodan from pathlib import Path (fix NameError)

Live tests:
- DELETE /invoices/4 → 200 (test invoice obrisan)
- GET /putni-nalozi → 200, /putni-nalozi/1 → 200
- GET /placanja → 200 lista; POST → ok pdf 11 KB; GET pdf → 200 application/pdf %PDF-
- /placanja invoice 1 (INA €63.15) i putni_nalog 2 (€133.08) PDF generirani
- /export/putni.xlsx → 200 application/vnd.openxmlformats... PK header valid
- OCR INA gorivo: vendor=INA, OIB=27759560625, brutto=€63.15, PDV=€12.63, cat=gorivo
- UI 3× "Ri.NET AI" / 0× "DeepSeek"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 08:01:49 +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
Damir Radulic 28fa98d83f Master handoff document for next chat session 2026-05-05 01:54:19 +02:00
claude-cc1 7251d27c21 CC1 R6 — coverage report + 2 more klubovi fixed
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>
2026-05-05 01:46:39 +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ć d45fbca4b3 CC5 R5: fix bulk-uplatnice + xlsx + notify-scan extended (incl. expired)
- /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.
2026-05-05 01:31:00 +02:00
CC6 Worker faf6beb536 M12.6 SF: sport-aware enrichment + federation map (HBS, HKS, HRS, HOS, HVS, HPS, HBS bocanje…)
- 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>
2026-05-05 01:30:16 +02:00
claude-cc1 73163de39c CC1 R4-DC — data cleanup pass on pgz_sport.klubovi
Backup: pgz_sport.klubovi_backup_20260505 (2244 rows snapshot before changes).

Issues fixed (18 of 23 detected):

1. Address-in-naziv (14 odbojkaški klubovi):
   - 10 auto-fixed by joining civic.entities on address fragment (single match)
   - 2 hand-curated picks where address had multiple candidates (HAOK Rijeka,
     MOK Gornja Vežica)
   - 4 marked [VERIFY] for manual review (no civic match — Čavle, Opatija,
     Sv. Križ Rijeka, Crikvenica)

2. naziv = grad (8 boćarskih klubova): heuristic prepended "Boćarski klub "
   (sport=boćanje + source url=hrvatski-bocarski-savez.hr confirms pattern).

3. Empty naziv (1 klub id 4426): marked [UNRESOLVED] with manual_review=true.

4. Sportaši with email/phone in ime/prezime: 0 found (schema clean).

All updates write metadata.cleanup_at / cleanup_reason / cleanup_source for audit
trail. Rollback path documented in data_cleanup_report.md.

Files added:
  scripts/cleanup_garbage_clubs.py  (idempotent, env-driven DSN)
  data_cleanup_report.md            (per-row table + manual review queue)
  data_cleanup_run.json             (raw script output)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 01:29:27 +02:00
Damir Radulić 0046b8d695 CC2 R5: defense-in-depth JWT + invite/reset token flows + audit
#1 JWT middleware:
- pgz_sport_api.py: starlette middleware require_jwt_on_admin runs before
  every /api/admin/* route. Even routes that lack Depends(require_user)
  cannot be reached without a valid Bearer token (verifies signature,
  exp, typ='access', revocation via user_sessions). OPTIONS passes for CORS.

#2 Invitation flow:
- pgz_sport.user_action_tokens table (token_hash, user_id, kind, expires_at,
  used_at, created_by, ip, meta). Single-use, raw token never persisted.
- POST /api/admin/users/{id}/invite — issues 'invite' token (TTL 7d),
  marks must_change_pwd, revokes existing sessions, returns invite_link.
- GET  /api/auth/setup-password?token=X — preflight (no consume).
- POST /api/auth/setup-password — consumes token, sets password, sets
  email_verified=true.

#3 Password reset flow:
- POST /api/auth/forgot-password — generic 'ako račun postoji' response;
  issues 'reset' token (TTL 2h) only for active users. Token returned in
  response only on localhost or if PGZ_REVEAL_RESET_TOKEN=1.
- GET  /api/auth/reset-password?token=X — preflight.
- POST /api/auth/reset-password — consumes token, sets new password,
  revokes all active sessions.

#4 Audit coverage (auth events):
- login.ok, login.fail (with reason), login.locked, login.2fa_required,
  login.2fa_fail, logout, auth.refresh, password.change, password.reset.ok,
  password.reset.fail, password.forgot.issue, password.forgot.miss,
  invite.consume.ok, invite.consume.fail, user.invite, user.create,
  user.update, user.delete, user.role.change, user.suspend, user.unsuspend,
  user.password.reset, 2fa.verify.ok, 2fa.verify.fail, 2fa.disable.

#5 Live tests: 41/41 across 6 demo users (incl. fresh invited+deleted user).
   Phase 2 verifies 14 endpoints reject no-auth and accept valid Bearer.
2026-05-05 01:28:29 +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
claude-cc1 ca92717039 CC1 R4-A3 — wire audit_log() into enrich /apply + helper available to all routers
- 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>
2026-05-05 00:46:41 +02:00
CC6 Worker 9c5116eaa3 M12.5 R4: coverage<70 picker + confidence>=0.7 gate + /var/log target
- 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>
2026-05-05 00:45:48 +02:00
claude-cc1 cf993b0221 CC1 R4-A1+A2 — audit log + stats endpoints + audit_log() helper
- 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>
2026-05-05 00:45:20 +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