Files
pgz-sport/static/index.html
T
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

1148 lines
51 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<!-- sport.rinet.one ─ DABI CIVIC INTELLIGENCE OS -->
<!-- v4.0 BOMBASTIC EDITION | dradulic@outlook.com | 2026-05-04 -->
<!-- Ri.NET AI OS — Damir Radulić | Croatia's Palantir -->
<html lang="hr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>DABI · PGŽ Sport Intelligence</title>
<meta name="description" content="Svaki euro. Svaki klub. Svaka veza. DABI zna.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=JetBrains+Mono:wght@300;400;700&family=Syne:wght@400;700;800&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.min.js"></script>
<script src="https://unpkg.com/3d-force-graph@1.73.4/dist/3d-force-graph.min.js"></script>
<style>
/* ─────────────────────────────────────────────────────────
TOKENS & RESET
───────────────────────────────────────────────────────── */
:root {
--void: #01020a;
--deep: #05070f;
--dark: #080c18;
--panel: #0d1220;
--card: #111827;
--rim: #1c2540;
--rim2: #253060;
--ice: #00d4ff;
--ice2: #00f0ff;
--plasma: #4f8fff;
--gold: #f0b429;
--gold2: #ffd700;
--lime: #39ff14;
--red: #ff1a3c;
--orange: #ff6b00;
--violet: #8b5cf6;
--t0: #ffffff;
--t1: #e8eaf2;
--t2: #9aa5be;
--t3: #c4cadc;
--t4: #5a6480;
--gice: 0 0 40px rgba(0,212,255,.3);
--ggold: 0 0 40px rgba(240,180,41,.3);
--gred: 0 0 40px rgba(255,26,60,.4);
--bb: 'Bebas Neue', cursive;
--sy: 'Syne', sans-serif;
--jb: 'JetBrains Mono', monospace;
}
*, *::before, *::after { box-sizing:border-box; margin:0; padding:0 }
html { scroll-behavior:smooth }
body {
background:var(--void);
color:var(--t1);
font-family:var(--sy);
font-size:14px;
line-height:1.6;
overflow-x:hidden;
}
a { color:var(--ice); text-decoration:none }
::selection { background:rgba(0,212,255,.25); color:var(--t0) }
/* ─────────────────────────────────────────────────────────
CANVAS BACKGROUND
───────────────────────────────────────────────────────── */
#canvas-bg {
position:fixed; inset:0; z-index:0; pointer-events:none;
opacity:.55;
}
/* ─────────────────────────────────────────────────────────
NAV
───────────────────────────────────────────────────────── */
#nav {
position:fixed; top:0; left:0; right:0; z-index:100;
height:56px;
background:rgba(1,2,10,.85);
border-bottom:1px solid rgba(0,212,255,.12);
backdrop-filter:blur(20px) saturate(180%);
display:flex; align-items:center; padding:0 32px; gap:32px;
transition:all .3s;
}
.nav-logo {
font-family:var(--bb);
font-size:22px;
letter-spacing:2px;
color:var(--ice2);
text-shadow:var(--gice);
flex-shrink:0;
}
.nav-logo span { color:var(--t2); font-size:12px; font-family:var(--jb); margin-left:8px; vertical-align:middle; letter-spacing:.5px }
.nav-links { display:flex; gap:4px; flex:1 }
.nav-a {
padding:6px 14px; border-radius:4px;
font-size:11px; font-weight:700; letter-spacing:.5px; text-transform:uppercase;
color:var(--t4); cursor:pointer;
transition:all .2s; border:1px solid transparent;
font-family:var(--jb);
}
.nav-a:hover, .nav-a.on {
color:var(--ice); border-color:rgba(0,212,255,.25);
background:rgba(0,212,255,.06);
}
.nav-a.danger { color:rgba(255,26,60,.7) }
.nav-a.danger:hover, .nav-a.danger.on { color:var(--red); border-color:rgba(255,26,60,.3); background:rgba(255,26,60,.06) }
.nav-right { display:flex; gap:8px; align-items:center; margin-left:auto }
.live-pill {
display:flex; align-items:center; gap:6px;
background:rgba(57,255,20,.1); border:1px solid rgba(57,255,20,.25);
padding:4px 12px; border-radius:20px;
font-family:var(--jb); font-size:9px; color:#39ff14; letter-spacing:.5px;
}
.live-dot { width:6px; height:6px; border-radius:50%; background:#39ff14; animation:pulse 1.5s infinite; box-shadow:0 0 8px #39ff14 }
@keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.5;transform:scale(.8)} }
/* ─────────────────────────────────────────────────────────
PAGES
───────────────────────────────────────────────────────── */
.page { display:none; min-height:calc(100vh - 56px); padding-top:56px; position:relative; z-index:1 }
.page.on { display:block }
@keyframes pIn { from{opacity:0;transform:translateY(16px)} to{opacity:1;transform:translateY(0)} }
.page.on { animation:pIn .4s ease }
/* ─────────────────────────────────────────────────────────
PAGE: HERO / DASHBOARD
───────────────────────────────────────────────────────── */
#hero {
min-height:100vh;
display:flex; flex-direction:column; justify-content:center;
padding:80px 60px 60px;
position:relative; overflow:hidden;
}
.hero-eyebrow {
font-family:var(--jb); font-size:10px; color:var(--ice);
letter-spacing:3px; text-transform:uppercase; margin-bottom:20px;
display:flex; align-items:center; gap:10px;
}
.hero-eyebrow::before { content:''; width:40px; height:1px; background:var(--ice) }
.hero-headline {
font-family:var(--bb);
font-size:clamp(54px,8vw,120px);
line-height:.92;
letter-spacing:2px;
color:var(--t0);
margin-bottom:12px;
}
.hero-headline .hl-ice { color:var(--ice); text-shadow:var(--gice) }
.hero-headline .hl-gold { color:var(--gold); text-shadow:var(--ggold) }
.hero-sub {
font-family:var(--jb); font-size:13px; color:var(--t2);
margin-bottom:48px; max-width:500px; line-height:1.8;
letter-spacing:.2px;
}
.hero-sub strong { color:var(--t1); font-weight:400 }
/* MEGA STATS */
.mega-stats {
display:grid; grid-template-columns:repeat(4,1fr); gap:1px;
background:var(--rim);
border:1px solid var(--rim); border-radius:8px; overflow:hidden;
max-width:900px; margin-bottom:60px;
}
.mstat {
background:var(--deep); padding:28px 24px;
position:relative; overflow:hidden; cursor:default;
transition:background .2s;
}
.mstat:hover { background:rgba(0,212,255,.04) }
.mstat::after {
content:''; position:absolute; top:0; left:0; right:0; height:2px;
opacity:0; transition:opacity .2s;
}
.mstat:hover::after { opacity:1 }
.mstat.c::after { background:linear-gradient(90deg,var(--ice),transparent) }
.mstat.g::after { background:linear-gradient(90deg,var(--gold),transparent) }
.mstat.r::after { background:linear-gradient(90deg,var(--red),transparent) }
.mstat.v::after { background:linear-gradient(90deg,var(--violet),transparent) }
.mstat-n {
font-family:var(--bb); font-size:52px; line-height:1; letter-spacing:1px;
margin-bottom:4px;
}
.mstat.c .mstat-n { color:var(--ice); text-shadow:0 0 30px rgba(0,212,255,.5) }
.mstat.g .mstat-n { color:var(--gold); text-shadow:0 0 30px rgba(240,180,41,.5) }
.mstat.r .mstat-n { color:var(--red); text-shadow:0 0 30px rgba(255,26,60,.5) }
.mstat.v .mstat-n { color:var(--violet); text-shadow:0 0 30px rgba(139,92,246,.5) }
.mstat-l { font-family:var(--jb); font-size:9px; color:var(--t4); letter-spacing:1px; text-transform:uppercase }
.mstat-sub { font-family:var(--jb); font-size:9px; color:var(--t2); margin-top:6px }
/* HERO CTA */
.hero-actions { display:flex; gap:12px; flex-wrap:wrap }
.cta {
padding:14px 28px; border-radius:4px;
font-family:var(--jb); font-size:11px; font-weight:700; letter-spacing:1px; text-transform:uppercase;
cursor:pointer; transition:all .2s; border:1px solid;
}
.cta-pri {
background:rgba(0,212,255,.15); border-color:rgba(0,212,255,.5); color:var(--ice);
box-shadow:0 0 20px rgba(0,212,255,.15);
}
.cta-pri:hover { background:rgba(0,212,255,.25); box-shadow:var(--gice); transform:translateY(-1px) }
.cta-ghost { background:transparent; border-color:var(--rim2); color:var(--t2) }
.cta-ghost:hover { border-color:var(--t2); color:var(--t1) }
.cta-danger {
background:rgba(255,26,60,.1); border-color:rgba(255,26,60,.4); color:var(--red);
}
.cta-danger:hover { background:rgba(255,26,60,.2); box-shadow:var(--gred); transform:translateY(-1px) }
/* SCROLL INDICATOR */
.scroll-hint {
position:absolute; bottom:30px; left:50%; transform:translateX(-50%);
display:flex; flex-direction:column; align-items:center; gap:6px;
font-family:var(--jb); font-size:8px; color:var(--t4); letter-spacing:2px;
animation:float 2s ease infinite;
}
@keyframes float { 0%,100%{transform:translateX(-50%) translateY(0)} 50%{transform:translateX(-50%) translateY(-6px)} }
.scroll-arrow { width:20px; height:20px; border-right:1px solid var(--t4); border-bottom:1px solid var(--t4); transform:rotate(45deg) }
/* ─────────────────────────────────────────────────────────
DASHBOARD BODY (below hero)
───────────────────────────────────────────────────────── */
#dash-body { padding:0 60px 80px }
.section-label {
font-family:var(--jb); font-size:10px; color:var(--t4);
letter-spacing:2px; text-transform:uppercase;
display:flex; align-items:center; gap:12px;
margin-bottom:24px; margin-top:60px;
}
.section-label::after { content:''; flex:1; height:1px; background:var(--rim) }
.section-label .tag-pill {
background:rgba(0,212,255,.08); border:1px solid rgba(0,212,255,.2);
color:var(--ice); padding:2px 10px; border-radius:20px; font-size:9px;
}
/* 2-col */
.g2 { display:grid; grid-template-columns:1fr 1fr; gap:20px }
.g3 { display:grid; grid-template-columns:1fr 1fr 1fr; gap:16px }
/* CARD */
.k {
background:var(--panel);
border:1px solid var(--rim);
border-radius:6px; overflow:hidden;
position:relative;
}
.k-head {
padding:16px 20px; border-bottom:1px solid var(--rim);
display:flex; align-items:center; justify-content:space-between;
}
.k-title { font-size:12px; font-weight:700; color:var(--t1); letter-spacing:-.2px }
.k-body { padding:20px }
.k-accent-top { position:absolute; top:0; left:0; right:0; height:2px }
.k-accent-top.ice { background:linear-gradient(90deg,var(--ice) 0%,rgba(0,212,255,0) 60%) }
.k-accent-top.gold { background:linear-gradient(90deg,var(--gold) 0%,rgba(240,180,41,0) 60%) }
.k-accent-top.red { background:linear-gradient(90deg,var(--red) 0%,rgba(255,26,60,0) 60%) }
.k-accent-top.violet { background:linear-gradient(90deg,var(--violet) 0%,rgba(139,92,246,0) 60%) }
/* ─────────────────────────────────────────────────────────
FORENSICS PAGE (DRAMATIC)
───────────────────────────────────────────────────────── */
#page-forensics-content {
padding:80px 60px 60px;
}
.forensics-hero {
background:linear-gradient(135deg, rgba(255,26,60,.08) 0%, rgba(255,26,60,.02) 100%);
border:1px solid rgba(255,26,60,.2);
border-radius:8px; padding:40px 48px; margin-bottom:40px;
position:relative; overflow:hidden;
}
.forensics-hero::before {
content:'CLASSIFIED';
position:absolute; top:-10px; right:-20px;
font-family:var(--bb); font-size:120px; color:rgba(255,26,60,.04);
letter-spacing:4px; user-select:none; pointer-events:none;
line-height:1;
}
.fh-label {
font-family:var(--jb); font-size:9px; color:var(--red); letter-spacing:3px;
text-transform:uppercase; margin-bottom:12px; display:flex; align-items:center; gap:8px;
}
.fh-label::before { content:''; width:3px; height:3px; background:var(--red); border-radius:50%; animation:pulse 1s infinite }
.fh-title { font-family:var(--bb); font-size:42px; color:var(--t0); line-height:1.1; margin-bottom:16px }
.fh-title .red { color:var(--red); text-shadow:var(--gred) }
.fh-desc { font-family:var(--jb); font-size:11px; color:var(--t2); line-height:1.8; max-width:600px }
.f-grid { display:flex; flex-direction:column; gap:12px }
.f-card {
background:var(--panel); border:1px solid var(--rim);
border-radius:6px; padding:20px 24px;
display:grid; grid-template-columns:auto 1fr auto;
gap:16px; align-items:start;
transition:all .2s; cursor:pointer; position:relative; overflow:hidden;
}
.f-card::before {
content:''; position:absolute; left:0; top:0; bottom:0; width:3px;
background:var(--rim2); transition:background .2s;
}
.f-card:hover { border-color:rgba(255,26,60,.3); background:rgba(255,26,60,.03) }
.f-card:hover::before { background:var(--red) }
.f-card.CRITICAL::before { background:var(--red) }
.f-card.HIGH::before { background:var(--orange) }
.f-card.MEDIUM::before { background:var(--gold) }
.sev {
padding:4px 10px; border-radius:3px;
font-family:var(--jb); font-size:9px; font-weight:700; letter-spacing:.5px;
white-space:nowrap; align-self:start;
}
.sev.CRITICAL { background:rgba(255,26,60,.15); color:var(--red); border:1px solid rgba(255,26,60,.3) }
.sev.HIGH { background:rgba(255,107,0,.12); color:var(--orange); border:1px solid rgba(255,107,0,.25) }
.sev.MEDIUM { background:rgba(240,180,41,.1); color:var(--gold); border:1px solid rgba(240,180,41,.2) }
.f-title { font-size:12px; color:var(--t3); line-height:1.6 }
.f-title strong { color:var(--t0); display:block; font-size:13px; margin-bottom:4px }
.f-meta { font-family:var(--jb); font-size:8px; color:var(--t4); align-self:start; text-align:right; white-space:nowrap }
/* ─────────────────────────────────────────────────────────
FUNDING PAGE
───────────────────────────────────────────────────────── */
#page-funding-content { padding:80px 60px 60px }
.funding-hero-bar {
display:grid; grid-template-columns:repeat(3,1fr);
background:var(--deep); border:1px solid var(--rim);
border-radius:8px; overflow:hidden; margin-bottom:40px;
}
.fhb {
padding:28px 32px; border-right:1px solid var(--rim);
position:relative;
}
.fhb:last-child { border-right:none }
.fhb-n { font-family:var(--bb); font-size:44px; letter-spacing:1px; margin-bottom:4px }
.fhb-l { font-family:var(--jb); font-size:9px; color:var(--t4); letter-spacing:1px; text-transform:uppercase }
/* ─────────────────────────────────────────────────────────
NETWORK PAGE
───────────────────────────────────────────────────────── */
#page-network-content { padding:80px 60px 60px }
#the-graph {
width:100%; height:600px; border-radius:8px;
background:var(--deep); border:1px solid var(--rim);
position:relative; overflow:hidden;
}
#graph-legend-abs {
position:absolute; top:20px; left:20px; z-index:10;
background:rgba(1,2,10,.9); border:1px solid var(--rim);
border-radius:6px; padding:14px 18px; backdrop-filter:blur(12px);
}
#graph-node-info {
position:absolute; bottom:20px; right:20px; z-index:10;
background:rgba(1,2,10,.95); border:1px solid var(--rim2);
border-radius:6px; padding:16px 20px; max-width:300px;
display:none; backdrop-filter:blur(12px);
}
/* ─────────────────────────────────────────────────────────
CHAT PAGE
───────────────────────────────────────────────────────── */
#page-chat-content { padding:80px 60px 60px }
.chat-wrap {
max-width:800px; margin:0 auto;
}
.chat-hero {
text-align:center; margin-bottom:40px;
}
.chat-hero h1 { font-family:var(--bb); font-size:64px; color:var(--ice); text-shadow:var(--gice); letter-spacing:2px; margin-bottom:8px }
.chat-hero p { font-family:var(--jb); font-size:11px; color:var(--t2); letter-spacing:.3px }
.chat-frame {
background:var(--deep); border:1px solid var(--rim2);
border-radius:8px; overflow:hidden;
}
.chat-msgs {
height:420px; overflow-y:auto; padding:24px;
display:flex; flex-direction:column; gap:16px;
}
.chat-msgs::-webkit-scrollbar { width:3px }
.chat-msgs::-webkit-scrollbar-thumb { background:var(--rim2) }
.cm {
max-width:78%; padding:14px 18px; border-radius:6px; line-height:1.7; font-size:12px;
position:relative;
}
.cm.user { align-self:flex-end; background:rgba(0,212,255,.1); border:1px solid rgba(0,212,255,.2); color:var(--t0) }
.cm.dabi { align-self:flex-start; background:var(--panel); border:1px solid var(--rim); color:var(--t3) }
.cm.dabi .cm-label { font-family:var(--jb); font-size:8px; color:var(--ice); margin-bottom:8px; letter-spacing:.5px }
.cm.error { align-self:flex-start; background:rgba(255,26,60,.06); border:1px solid rgba(255,26,60,.2); color:rgba(255,26,60,.8) }
.cm .cm-conf { font-family:var(--jb); font-size:8px; color:var(--t4); margin-top:8px; text-align:right }
.chat-suggestions-row {
display:flex; gap:8px; flex-wrap:wrap; padding:12px 24px;
border-top:1px solid var(--rim);
}
.sug-chip {
background:var(--card); border:1px solid var(--rim2);
color:var(--t2); padding:5px 12px; border-radius:20px;
font-family:var(--jb); font-size:10px; cursor:pointer;
transition:all .15s;
}
.sug-chip:hover { border-color:var(--ice); color:var(--ice) }
.chat-input-row {
display:flex; gap:0; border-top:1px solid var(--rim);
}
#ci {
flex:1; background:transparent; border:none; outline:none;
padding:16px 20px; color:var(--t1); font-family:var(--sy); font-size:13px;
}
#ci::placeholder { color:var(--t4) }
#cs {
background:var(--ice); color:var(--void); border:none; outline:none;
padding:16px 24px; font-family:var(--jb); font-size:11px; font-weight:700;
cursor:pointer; letter-spacing:1px; text-transform:uppercase;
transition:all .15s;
}
#cs:hover { background:var(--ice2) }
/* ─────────────────────────────────────────────────────────
TABLES
───────────────────────────────────────────────────────── */
.tw { overflow:auto; max-height:400px }
.tw::-webkit-scrollbar { width:3px; height:3px }
.tw::-webkit-scrollbar-thumb { background:var(--rim2) }
table.dt { width:100%; border-collapse:collapse; font-size:11px }
table.dt th {
padding:8px 12px; text-align:left; font-family:var(--jb); font-size:8px;
color:var(--t4); letter-spacing:1px; text-transform:uppercase;
border-bottom:1px solid var(--rim); position:sticky; top:0; background:var(--panel);
}
table.dt td { padding:9px 12px; border-bottom:1px solid rgba(28,37,64,.4); vertical-align:middle }
table.dt tr:hover td { background:rgba(0,212,255,.025) }
.mono { font-family:var(--jb) }
.eur { font-family:var(--jb); color:var(--lime); text-align:right; font-size:10px }
.chip {
display:inline-block; padding:2px 8px; border-radius:3px;
font-family:var(--jb); font-size:8px; letter-spacing:.3px;
}
.chip.ice { background:rgba(0,212,255,.1); color:var(--ice); border:1px solid rgba(0,212,255,.2) }
.chip.city { background:rgba(57,255,20,.08); color:var(--lime); border:1px solid rgba(57,255,20,.15) }
.chip.red { background:rgba(255,26,60,.1); color:var(--red); border:1px solid rgba(255,26,60,.2) }
/* ─────────────────────────────────────────────────────────
LOADING / UTILS
───────────────────────────────────────────────────────── */
.loading { display:flex; align-items:center; justify-content:center; gap:10px; padding:48px; color:var(--t4); font-family:var(--jb); font-size:10px; letter-spacing:1px }
.sp { width:16px; height:16px; border:2px solid var(--rim2); border-top-color:var(--ice); border-radius:50%; animation:spin .8s linear infinite }
@keyframes spin { to{transform:rotate(360deg)} }
/* SEARCH */
.search-row { display:flex; gap:8px; margin-bottom:20px }
.search-row input, .search-row select {
background:var(--panel); border:1px solid var(--rim); border-radius:4px;
padding:9px 14px; color:var(--t1); font-family:var(--sy); font-size:12px;
outline:none; transition:border-color .2s;
}
.search-row input { flex:1 }
.search-row input::placeholder { color:var(--t4) }
.search-row input:focus, .search-row select:focus { border-color:var(--ice) }
.search-row select { cursor:pointer; color:var(--t2) }
/* ─────────────────────────────────────────────────────────
COUNTER ANIMATION
───────────────────────────────────────────────────────── */
.count-up { transition:all .1s }
/* ─────────────────────────────────────────────────────────
MOBILE
───────────────────────────────────────────────────────── */
@media(max-width:768px) {
#hero, #dash-body, #page-forensics-content, #page-funding-content, #page-network-content, #page-chat-content { padding-left:20px; padding-right:20px }
.mega-stats { grid-template-columns:repeat(2,1fr) }
.g2, .g3 { grid-template-columns:1fr }
.hero-headline { font-size:clamp(42px,10vw,80px) }
.nav-links { display:none }
}
</style>
<script src="/static/oib_format.js" defer></script>
</head>
<body>
<!-- PARTICLE CANVAS -->
<canvas id="canvas-bg"></canvas>
<!-- ───────── NAVIGATION ───────── -->
<nav id="nav">
<div class="nav-logo">DABI<span>· PGŽ SPORT INTELLIGENCE</span></div>
<div class="nav-links">
<div class="nav-a on" onclick="G('dash')">Dashboard</div>
<div class="nav-a danger" onclick="G('forensics')">⚠ Forenzika</div>
<div class="nav-a" onclick="G('funding')">Potpore</div>
<div class="nav-a" onclick="G('clubs')">Klubovi</div>
<div class="nav-a" onclick="G('network')">Mreža</div>
<div class="nav-a" onclick="G('chat')">AI Chat</div>
</div>
<div class="nav-right">
<div class="live-pill"><div class="live-dot"></div>LIVE DATA</div>
</div>
</nav>
<!-- ═══════════════════════════════
PAGE: DASHBOARD
═══════════════════════════════ -->
<div id="p-dash" class="page on">
<section id="hero">
<div class="hero-eyebrow">Ri.NET AI OS · PGŽ Civic Intelligence Platform</div>
<h1 class="hero-headline">
DABI<br>
<span class="hl-ice">ZOVE</span><br>
<span class="hl-gold">SPORT</span>
</h1>
<p class="hero-sub">
<strong>246 saveza. 2,244 klubova. 33,355 sportaša.</strong><br>
Svaki euro javnog novca vidljiv. Svaka veza dokumentirana.<br>
AI koji zna više od revizora — i brže od novinara.
</p>
<div class="mega-stats" id="mega-stats">
<div class="mstat c">
<div class="mstat-n" id="ms-klubovi"></div>
<div class="mstat-l">Klubovi PGŽ</div>
<div class="mstat-sub" id="ms-oib">OIB verificiranih: …</div>
</div>
<div class="mstat g">
<div class="mstat-n" id="ms-proracun"></div>
<div class="mstat-l">Proračun Sport 2026</div>
<div class="mstat-sub">Grad + Županija + Min.</div>
</div>
<div class="mstat r">
<div class="mstat-n" id="ms-forensics"></div>
<div class="mstat-l">⚠ Forenzički Nalazi</div>
<div class="mstat-sub">CRITICAL — vidljivi javnosti</div>
</div>
<div class="mstat v">
<div class="mstat-n" id="ms-clanovi"></div>
<div class="mstat-l">Registriranih 2026</div>
<div class="mstat-sub">Aktivnih sportaša PGŽ</div>
</div>
</div>
<div class="hero-actions">
<button class="cta cta-danger" onclick="G('forensics')">⚠ Otvori forenzičke nalaze</button>
<button class="cta cta-pri" onclick="G('chat')">◆ Pitaj DABI AI</button>
<button class="cta cta-ghost" onclick="G('network')">◎ Graf mreže veza</button>
</div>
<div class="scroll-hint"><div class="scroll-arrow"></div>SCROLL</div>
</section>
<section id="dash-body">
<div class="section-label">Najfinanciranije organizacije <span class="tag-pill">2026</span></div>
<div class="g2">
<div class="k">
<div class="k-accent-top ice"></div>
<div class="k-head">
<div class="k-title">Top Potpore — Grad Rijeka & PGŽ</div>
<button class="cta cta-ghost" style="font-size:9px;padding:4px 12px" onclick="G('funding')">Sve →</button>
</div>
<div class="tw"><table class="dt" id="dt-top">
<thead><tr><th>Klub</th><th>Sport</th><th style="text-align:right">EUR</th></tr></thead>
<tbody><tr><td colspan="3"><div class="loading"><div class="sp"></div>Učitavam…</div></td></tr></tbody>
</table></div>
</div>
<div class="k">
<div class="k-accent-top gold"></div>
<div class="k-head"><div class="k-title">Distribucija po Vrsti Sporta</div></div>
<div class="k-body" style="height:300px; display:flex; align-items:center">
<canvas id="sport-donut"></canvas>
</div>
</div>
</div>
<div class="section-label">Alarmi <span class="tag-pill" style="background:rgba(255,26,60,.1);border-color:rgba(255,26,60,.3);color:var(--red)">CRITICAL</span></div>
<div class="k" style="margin-bottom:40px">
<div class="k-accent-top red"></div>
<div class="k-head">
<div class="k-title" style="color:var(--red)">Forenzički Alarmi — Top 4 CRITICAL</div>
<button class="cta cta-danger" style="font-size:9px;padding:4px 12px" onclick="G('forensics')">Sve nalaze →</button>
</div>
<div id="foren-preview" style="padding:4px 0">
<div class="loading"><div class="sp"></div></div>
</div>
</div>
<div class="section-label">Statistike baze znanja <span class="tag-pill">LIVE</span></div>
<div class="g3" id="stat-cards">
<div class="k"><div class="k-accent-top ice"></div><div class="k-body" id="sc1"><div class="loading"><div class="sp"></div></div></div></div>
<div class="k"><div class="k-accent-top gold"></div><div class="k-body" id="sc2"><div class="loading"><div class="sp"></div></div></div></div>
<div class="k"><div class="k-accent-top violet"></div><div class="k-body" id="sc3"><div class="loading"><div class="sp"></div></div></div></div>
</div>
</section>
</div>
<!-- ═══════════════════════════════
PAGE: FORENSICS
═══════════════════════════════ -->
<div id="p-forensics" class="page">
<div id="page-forensics-content">
<div class="forensics-hero">
<div class="fh-label">⚑ Forenzička analiza · OIB-verificirani nalazi</div>
<h1 class="fh-title">184 OSOBA.<br><span class="red">660M EUR</span> VEZA.</h1>
<p class="fh-desc">
Automatska analiza sukoba interesa u PGŽ sport ekosustavu.<br>
USKOK optuženik u sportu. Zero-employee tvrtke s milijunskim prihodima.<br>
Svaki nalaz temeljen na: FINA RGFI + Sudreg + javna nabava + upisnik udruga.
</p>
</div>
<div style="display:flex;gap:8px;margin-bottom:28px;flex-wrap:wrap">
<button class="cta cta-danger" onclick="filterF('')">Sve</button>
<button class="cta cta-ghost" onclick="filterF('CRITICAL')">CRITICAL</button>
<button class="cta cta-ghost" onclick="filterF('HIGH')">HIGH</button>
<button class="cta cta-ghost" onclick="filterF('MEDIUM')">MEDIUM</button>
</div>
<div class="f-grid" id="f-list">
<div class="loading"><div class="sp"></div>Analiziram forenzičku bazu…</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════
PAGE: FUNDING
═══════════════════════════════ -->
<div id="p-funding" class="page">
<div id="page-funding-content">
<div class="funding-hero-bar">
<div class="fhb">
<div class="fhb-n" style="color:var(--gold)">€2.8M</div>
<div class="fhb-l">Ukupni sport proračun 2026</div>
</div>
<div class="fhb">
<div class="fhb-n" style="color:var(--ice)">€528K</div>
<div class="fhb-l">PGŽ direktno + Grad Rijeka</div>
</div>
<div class="fhb">
<div class="fhb-n" style="color:var(--lime)" id="fhb-records"></div>
<div class="fhb-l">Evidentirani transferi</div>
</div>
</div>
<div class="k" style="margin-bottom:24px">
<div class="k-accent-top ice"></div>
<div class="k-head">
<div class="k-title">Potpore po Klubovima</div>
<select id="fy" onchange="loadFunding()" style="background:var(--card);border:1px solid var(--rim);color:var(--t2);padding:4px 10px;border-radius:3px;font-size:11px;font-family:var(--jb);cursor:pointer">
<option value="2026">2026</option>
<option value="2024">2024</option>
<option value="2023">2023</option>
</select>
</div>
<div class="k-body" style="height:280px">
<canvas id="f-bar"></canvas>
</div>
</div>
<div class="k">
<div class="k-accent-top gold"></div>
<div class="tw"><table class="dt" id="f-tbl">
<thead><tr><th>Klub</th><th>Sport</th><th>Grad</th><th>Godina</th><th style="text-align:right">EUR</th></tr></thead>
<tbody id="f-tbody"><tr><td colspan="5"><div class="loading"><div class="sp"></div></div></td></tr></tbody>
</table></div>
</div>
</div>
</div>
<!-- ═══════════════════════════════
PAGE: CLUBS
═══════════════════════════════ -->
<div id="p-clubs" class="page">
<div style="padding:80px 60px 60px">
<div class="search-row">
<input id="cs-q" type="text" placeholder="Pretraži klubove…" oninput="searchC()">
<select id="cs-city" onchange="searchC()">
<option value="">Svi gradovi</option>
<option value="Rijeka">Rijeka</option>
<option value="Opatija">Opatija</option>
<option value="Krk">Krk</option>
<option value="Crikvenica">Crikvenica</option>
<option value="Bakar">Bakar</option>
</select>
</div>
<div class="k">
<div class="k-accent-top ice"></div>
<div class="tw" style="max-height:520px"><table class="dt" id="clubs-t">
<thead><tr><th>Naziv</th><th>Grad</th><th>Tip</th><th>OIB</th><th>Reg. broj</th></tr></thead>
<tbody id="clubs-tb"><tr><td colspan="5"><div class="loading"><div class="sp"></div></div></td></tr></tbody>
</table></div>
</div>
</div>
</div>
<!-- ═══════════════════════════════
PAGE: NETWORK
═══════════════════════════════ -->
<div id="p-network" class="page">
<div id="page-network-content">
<div class="section-label" style="margin-top:0">Entity Network <span class="tag-pill">3D FORCE GRAPH</span></div>
<p style="font-family:var(--jb);font-size:10px;color:var(--t4);margin-bottom:20px;letter-spacing:.3px">
184 osoba · 170 klubova · 279 tvrtki · 660M EUR cross-connections. Klikni čvor za detalje.
</p>
<div id="the-graph">
<div id="graph-legend-abs">
<div style="font-family:var(--jb);font-size:8px;color:var(--t4);letter-spacing:1px;margin-bottom:10px;text-transform:uppercase">Legenda</div>
<div style="display:flex;flex-direction:column;gap:6px">
<div style="display:flex;align-items:center;gap:8px;font-size:10px;color:var(--t2)"><div style="width:10px;height:10px;border-radius:50%;background:var(--ice)"></div>Osoba</div>
<div style="display:flex;align-items:center;gap:8px;font-size:10px;color:var(--t2)"><div style="width:10px;height:10px;border-radius:50%;background:var(--lime)"></div>Klub/Savez</div>
<div style="display:flex;align-items:center;gap:8px;font-size:10px;color:var(--t2)"><div style="width:10px;height:10px;border-radius:50%;background:var(--violet)"></div>Tvrtka</div>
<div style="display:flex;align-items:center;gap:8px;font-size:10px;color:var(--t2)"><div style="width:10px;height:10px;border-radius:50%;background:var(--red)"></div>⚠ Forenzički</div>
</div>
</div>
<div id="graph-node-info">
<div style="font-family:var(--jb);font-size:8px;color:var(--t4);letter-spacing:1px;margin-bottom:8px">ODABRANI ČVOR</div>
<div id="gni-name" style="font-size:14px;font-weight:700;color:var(--t0);margin-bottom:3px"></div>
<div id="gni-type" style="font-family:var(--jb);font-size:9px;color:var(--t4);margin-bottom:10px"></div>
<div id="gni-detail" style="font-size:11px;color:var(--t3);line-height:1.7"></div>
</div>
<div class="loading" id="gload" style="position:absolute;inset:0;background:var(--deep)">
<div class="sp"></div>Gradim 3D mrežu…
</div>
</div>
<div style="margin-top:12px;text-align:right">
<button class="cta cta-ghost" onclick="loadNetwork()">↺ Osvježi</button>
</div>
</div>
</div>
<!-- ═══════════════════════════════
PAGE: CHAT
═══════════════════════════════ -->
<div id="p-chat" class="page">
<div id="page-chat-content">
<div class="chat-wrap">
<div class="chat-hero">
<h1>PITAJ DABI</h1>
<p>Sport Intelligence AI · Čakavski novinarski stil · PGŽ domenski model</p>
</div>
<div class="chat-frame">
<div class="chat-msgs" id="chat-msgs">
<div class="cm dabi">
<div class="cm-label">DABI · SPORT PERSONA · ONLINE</div>
Bok! Ja san DABI — čakavski AI novinar za PGŽ sport. Znan saveze, predsjednike, potpore, forenziku. Ako niman podatak u bazi, rećen ti to direktno — ne izmišljan ništa.
</div>
</div>
<div class="chat-suggestions-row">
<div class="sug-chip" onclick="ask(this)">Predsjednik HNK Rijeka?</div>
<div class="sug-chip" onclick="ask(this)">Ivan Sušanj?</div>
<div class="sug-chip" onclick="ask(this)">Proračun PGŽ sport 2026?</div>
<div class="sug-chip" onclick="ask(this)">Slavica Grgurić-Pajnić?</div>
<div class="sug-chip" onclick="ask(this)">Sukob interesa u sportu?</div>
<div class="sug-chip" onclick="ask(this)">Tajnik ŠK Kraljevica?</div>
</div>
<div class="chat-input-row">
<input id="ci" type="text" placeholder="Pitaj o sportu PGŽ…" onkeydown="if(event.key==='Enter')sendMsg()">
<button id="cs" onclick="sendMsg()">POŠALJI</button>
</div>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════
JAVASCRIPT
═══════════════════════════════ -->
<script>
// ──── CONFIG ────
const API = 'https://api.rinet.one/api/v1';
const DABI = `${API}/dabi/chat`;
// ──── CANVAS PARTICLES ────
(function(){
const c = document.getElementById('canvas-bg');
const cx = c.getContext('2d');
let W, H, pts = [];
function resize() {
W = c.width = window.innerWidth;
H = c.height = window.innerHeight;
}
function mkPts(n) {
pts = [];
for(let i=0;i<n;i++) pts.push({
x: Math.random()*W, y: Math.random()*H,
vx: (Math.random()-.5)*.3, vy: (Math.random()-.5)*.3,
r: Math.random()*1.5+.3,
a: Math.random()*.8+.2
});
}
function draw() {
cx.clearRect(0,0,W,H);
pts.forEach(p=>{
p.x+=p.vx; p.y+=p.vy;
if(p.x<0)p.x=W; if(p.x>W)p.x=0;
if(p.y<0)p.y=H; if(p.y>H)p.y=0;
cx.beginPath();
cx.arc(p.x,p.y,p.r,0,Math.PI*2);
cx.fillStyle=`rgba(0,212,255,${p.a*.4})`;
cx.fill();
});
// Connect nearby
for(let i=0;i<pts.length;i++){
for(let j=i+1;j<pts.length;j++){
const dx=pts[i].x-pts[j].x, dy=pts[i].y-pts[j].y;
const d=Math.sqrt(dx*dx+dy*dy);
if(d<120){
cx.beginPath();
cx.moveTo(pts[i].x,pts[i].y);
cx.lineTo(pts[j].x,pts[j].y);
cx.strokeStyle=`rgba(0,212,255,${(.12*(1-d/120))})`;
cx.lineWidth=.5;
cx.stroke();
}
}
}
requestAnimationFrame(draw);
}
window.addEventListener('resize',()=>{ resize(); mkPts(80) });
resize(); mkPts(80); draw();
})();
// ──── ROUTING ────
const pages = ['dash','forensics','funding','clubs','network','chat'];
let curPage = 'dash';
function G(id) {
pages.forEach(p=>{
document.getElementById('p-'+p).classList.remove('on');
});
document.querySelectorAll('.nav-a').forEach(a=>{
a.classList.remove('on');
});
document.getElementById('p-'+id).classList.add('on');
document.querySelectorAll('.nav-a').forEach(a=>{
if(a.getAttribute('onclick')&&a.getAttribute('onclick').includes(`'${id}'`)) a.classList.add('on');
});
curPage = id;
if(id==='network' && !window._netLoaded) loadNetwork();
if(id==='clubs' && !window._clubsLoaded) loadClubs();
if(id==='funding' && !window._fundingLoaded) loadFunding();
if(id==='forensics' && !window._forensicsLoaded) loadForensics();
window.scrollTo(0,0);
}
// ──── UTILS ────
const fmtEur = n => n==null?'':'€'+Number(n).toLocaleString('hr-HR',{maximumFractionDigits:0});
const fmtN = n => n==null?'':Number(n).toLocaleString('hr-HR');
const $ = id => document.getElementById(id);
function animCount(el, target, suffix='', dur=1800) {
const start = Date.now();
const t = parseInt(String(target).replace(/[^0-9]/g,'')) || 0;
const prefix = String(target).replace(/[0-9,.\s]/g,'').replace(/\d.*/,'');
const tick = () => {
const p = Math.min(1,(Date.now()-start)/dur);
const ease = 1-Math.pow(1-p,4);
const cur = Math.round(ease*t);
el.textContent = prefix + fmtN(cur) + suffix;
if(p<1) requestAnimationFrame(tick);
else el.textContent = prefix + fmtN(t) + suffix;
};
tick();
}
// ──── DASHBOARD INIT ────
async function initDash() {
try {
const d = await fetch(`${API}/sport/dashboard`).then(r=>r.json());
animCount($('ms-klubovi'), d.klubovi);
animCount($('ms-clanovi'), d.registriranih_2026);
animCount($('ms-forensics'), d.forensics);
$('ms-oib').textContent = 'OIB verificiranih: ' + fmtN(d.klubovi_oib);
const pb = d.proracun_2026?.[0];
if(pb){
const tot = (pb.ukupno||0)+(pb.ministarstvo||0);
$('ms-proracun').textContent = '€'+(tot/1000000).toFixed(1)+'M';
}
// Sport donut chart
if(d.by_sport_top5) {
const tops = d.by_sport_top5.slice(0,7);
const ctx = $('sport-donut').getContext('2d');
new Chart(ctx, {
type:'doughnut',
data:{
labels: tops.map(x=>x.sport),
datasets:[{
data: tops.map(x=>x.n),
backgroundColor:['#00d4ff','#f0b429','#8b5cf6','#ff6b00','#39ff14','#ff1a3c','#4f8fff'],
borderWidth:2, borderColor:'#0d1220',
hoverBorderWidth:3
}]
},
options:{
responsive:true, maintainAspectRatio:false,
plugins:{ legend:{ position:'right', labels:{color:'#9aa5be',font:{size:10,family:'JetBrains Mono'},boxWidth:10,padding:10} } },
cutout:'65%'
}
});
}
// Stat cards
$('sc1').innerHTML = `
<div style="font-family:var(--jb);font-size:9px;color:var(--t4);letter-spacing:1px;margin-bottom:16px">SPORTAŠI & ČLANOVI</div>
${statRow('Članova 2026', fmtN(d.clanstvo_2026))}
${statRow('Registriranih', fmtN(d.registriranih_2026))}
${statRow('Saveza', fmtN(d.savezi))}
`;
$('sc2').innerHTML = `
<div style="font-family:var(--jb);font-size:9px;color:var(--t4);letter-spacing:1px;margin-bottom:16px">DOKUMENTI & PROPISI</div>
${statRow('Dokumenata (OCR)', fmtN(d.documents))}
${statRow('Pravilnici', fmtN(d.pravilnici))}
${statRow('Zakoni', fmtN(d.laws))}
`;
$('sc3').innerHTML = `
<div style="font-family:var(--jb);font-size:9px;color:var(--t4);letter-spacing:1px;margin-bottom:16px">AI INTELLIGENCE</div>
${statRow('Baza znanja', '5.3M faktova')}
${statRow('Vektoriziranih', '18.5M točaka')}
${statRow('DABI accuracy', '92.7%')}
`;
} catch(e) { console.error('Dashboard init:', e) }
loadTopFunding();
loadForensicsPreview();
}
function statRow(l,v) {
return `<div style="display:flex;justify-content:space-between;align-items:center;padding:7px 0;border-bottom:1px solid rgba(28,37,64,.4)">
<span style="font-size:11px;color:var(--t2)">${l}</span>
<span style="font-family:var(--jb);font-size:11px;color:var(--ice)">${v}</span>
</div>`;
}
// ──── TOP FUNDING ────
async function loadTopFunding() {
try {
const data = await fetch(`${API}/sport/funding/v2?limit=8&year=2026`).then(r=>r.json());
const tb = $('dt-top').querySelector('tbody');
tb.innerHTML = data.map(r=>`
<tr>
<td style="font-weight:700;color:var(--t0)">${r.club_name||''}</td>
<td><span class="chip ice">${r.sport||''}</span></td>
<td class="eur">${fmtEur(r.total)}</td>
</tr>`).join('');
} catch(e) { console.error(e) }
}
// ──── FORENSICS PREVIEW ────
async function loadForensicsPreview() {
try {
const data = await fetch(`${API}/sport/forensics?limit=4`).then(r=>r.json());
$('foren-preview').innerHTML = data.slice(0,4).map(f=>`
<div class="f-card ${f.severity}" onclick="G('forensics')" style="cursor:pointer">
<span class="sev ${f.severity}">${f.severity}</span>
<div class="f-title">
<strong>${(f.title||'').substring(0,90)}${(f.title||'').length>90?'…':''}</strong>
<span style="font-family:var(--jb);font-size:9px;color:var(--t4)">${f.finding_type||''} · ${f.created_at||''}</span>
</div>
<div class="f-meta">${f.verification_status||''}</div>
</div>`).join('');
} catch(e) { $('foren-preview').innerHTML='<div style="padding:16px;color:var(--t4);font-size:11px">Greška učitavanja</div>' }
}
// ──── FORENSICS ────
let _forensicsAll = [];
async function loadForensics() {
window._forensicsLoaded = true;
try {
const data = await fetch(`${API}/sport/forensics?limit=60`).then(r=>r.json());
_forensicsAll = data;
renderForensics(data);
} catch(e) { $('f-list').innerHTML=`<div style="padding:24px;color:var(--red)">Greška: ${e.message}</div>` }
}
function renderForensics(data) {
$('f-list').innerHTML = data.map(f=>`
<div class="f-card ${f.severity}">
<span class="sev ${f.severity}">${f.severity||''}</span>
<div class="f-title">
<strong>${f.title||''}</strong>
<span style="font-family:var(--jb);font-size:9px;color:var(--t4)">${f.finding_type||''} · ${f.verification_status||''}</span>
</div>
<div class="f-meta" style="color:var(--t4)">${f.created_at||''}</div>
</div>`).join('') || '<div style="padding:24px;color:var(--t4);text-align:center;font-family:var(--jb);font-size:11px">Nema nalaza</div>';
}
function filterF(sev) {
renderForensics(sev ? _forensicsAll.filter(f=>f.severity===sev) : _forensicsAll);
}
// ──── FUNDING ────
let _fBarChart;
async function loadFunding() {
window._fundingLoaded = true;
const yr = $('fy')?.value||2026;
try {
const data = await fetch(`${API}/sport/funding/v2?limit=50&year=${yr}`).then(r=>r.json());
$('fhb-records').textContent = fmtN(data.length);
const top14 = data.slice(0,14);
const ctx = $('f-bar').getContext('2d');
if(_fBarChart) _fBarChart.destroy();
_fBarChart = new Chart(ctx, {
type:'bar',
data:{
labels: top14.map(x=>(x.club_name||'').substring(0,18)),
datasets:[{
label:`Potpore ${yr}`,
data: top14.map(x=>x.total||0),
backgroundColor:'rgba(0,212,255,.3)',
borderColor:'rgba(0,212,255,.9)',
borderWidth:1.5, borderRadius:3
}]
},
options:{
responsive:true, maintainAspectRatio:false,
scales:{
x:{ticks:{color:'#9aa5be',font:{size:9,family:'JetBrains Mono'},maxRotation:45},grid:{color:'rgba(28,37,64,.5)'}},
y:{ticks:{color:'#9aa5be',font:{size:9,family:'JetBrains Mono'},callback:v=>'€'+fmtN(v)},grid:{color:'rgba(28,37,64,.5)'}}
},
plugins:{legend:{labels:{color:'#9aa5be',font:{size:10}}}}
}
});
$('f-tbody').innerHTML = data.map(r=>`
<tr>
<td style="font-weight:700;color:var(--t0)">${r.club_name||''}</td>
<td>${r.sport||''}</td>
<td><span class="chip city">${r.city||''}</span></td>
<td class="mono" style="color:var(--t4)">${r.year||''}</td>
<td class="eur">${fmtEur(r.total)}</td>
</tr>`).join('');
} catch(e) { console.error(e) }
}
// ──── CLUBS ────
let _cTimer;
async function loadClubs(q='', city='') {
window._clubsLoaded = true;
$('clubs-tb').innerHTML = '<tr><td colspan="5"><div class="loading"><div class="sp"></div>Učitavam…</div></td></tr>';
try {
let url = `${API}/sport/registar?limit=60`;
if(q) url+=`&q=${encodeURIComponent(q)}`;
if(city) url+=`&grad=${encodeURIComponent(city)}`;
const data = await fetch(url).then(r=>r.json());
$('clubs-tb').innerHTML = data.map(r=>`
<tr>
<td style="font-weight:700;color:var(--t0)">${r.naziv||r.naziv_pravne_osobe||''}</td>
<td><span class="chip city">${r.grad||''}</span></td>
<td style="color:var(--t4);font-size:10px">${r.tip_udruge||r.tip_subjekta||''}</td>
<td class="mono" style="font-size:9px;color:var(--t4)">${r.oib?formatOib(r.oib,{klub_id:r.id,savez_id:r.savez_id}):''}</td>
<td class="mono" style="font-size:9px;color:var(--t4)">${r.reg_broj||''}</td>
</tr>`).join('');
} catch(e) { $('clubs-tb').innerHTML=`<tr><td colspan="5" style="color:var(--red);padding:12px">Greška: ${e.message}</td></tr>` }
}
function searchC(){ clearTimeout(_cTimer); _cTimer=setTimeout(()=>loadClubs($('cs-q').value,$('cs-city').value),350) }
// ──── NETWORK ────
async function loadNetwork() {
window._netLoaded = true;
$('gload').style.display='flex';
try {
const data = await fetch(`${API}/sport/network?limit=100`).then(r=>r.json());
const nodesMap={}, links=[];
const forensicPpl = new Set(['SAMIR BARAĆ','MIROSLAV MARIĆ','DOROTEA PESIC-BUKOVAC','OMNI-PRO']);
data.forEach(rel=>{
const {person1_name:p1,person2_name:p2,shared_entity:ent,shared_entity_type:et,relationship_type:rt}=rel;
if(!p1||!ent) return;
if(!nodesMap[p1]) nodesMap[p1]={id:p1,name:p1,type:'person',val:4,forensic:forensicPpl.has(p1)};
const isCompany = et&&(et.includes('Tvrtka')||et.includes('dru'));
if(!nodesMap[ent]) nodesMap[ent]={id:ent,name:ent.substring(0,28)+'…',type:isCompany?'company':'club',val:6};
if(p2&&!nodesMap[p2]) nodesMap[p2]={id:p2,name:p2,type:'person',val:4,forensic:forensicPpl.has(p2)};
links.push({source:p1,target:ent,type:rt});
if(p2) links.push({source:p2,target:ent,type:rt});
});
const nodes=Object.values(nodesMap);
const g=$('the-graph');
const Graph = ForceGraph3D()(g)
.graphData({nodes,links})
.backgroundColor('transparent')
.nodeLabel(n=>`<div style="background:rgba(1,2,10,.9);border:1px solid #1c2540;padding:6px 10px;border-radius:4px;font-family:JetBrains Mono,monospace;font-size:11px;color:#e8eaf2">${n.name}<br><span style="color:#5a6480;font-size:9px">${n.type.toUpperCase()}</span></div>`)
.nodeColor(n=>{ if(n.forensic||n.type==='company'&&n.name.includes('OMNI')) return '#ff1a3c'; if(n.type==='person') return '#00d4ff'; if(n.type==='company') return '#8b5cf6'; return '#39ff14'; })
.nodeVal(n=>n.forensic?8:n.val)
.linkColor(l=>'rgba(0,212,255,.15)')
.linkWidth(.5)
.onNodeClick(n=>{
$('gni-name').textContent=n.name;
$('gni-type').textContent=n.type.toUpperCase()+(n.forensic?' · ⚠ FORENZIČKI SUBJEKT':'');
$('gni-detail').innerHTML=n.forensic?'<span style="color:var(--red)">Provjeri forenzičke nalaze za ovu osobu</span>':` ID: ${n.id}`;
$('graph-node-info').style.display='block';
})
.width(g.offsetWidth).height(g.offsetHeight);
$('gload').style.display='none';
} catch(e){ $('gload').innerHTML=`<div style="color:var(--red);font-size:11px">Greška: ${e.message}</div>` }
}
// ──── CHAT ────
async function sendMsg(q) {
const input=$('ci');
const msg=q||input.value.trim();
if(!msg) return;
input.value='';
const box=$('chat-msgs');
box.innerHTML+=`<div class="cm user">${msg}</div>`;
box.innerHTML+=`<div class="cm dabi" id="_typing"><div class="cm-label">DABI · SPORT · PROCESIRA</div><div class="sp" style="width:14px;height:14px;display:inline-block;vertical-align:middle"></div></div>`;
box.scrollTop=box.scrollHeight;
try {
const resp=await fetch(DABI,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:msg,persona:'sport'})});
const j=await resp.json();
const t=$('_typing'); if(t)t.remove();
const ans=j?.data?.response||j?.response||j?.error||'Nema odgovora.';
const conf=j?.data?.confidence??j?.confidence??null;
const src=j?.data?.source||j?.source||'';
const isNull=ans.includes('nemam')||ans.includes('nije dostupno');
box.innerHTML+=`<div class="cm ${isNull?'error':'dabi'}">
<div class="cm-label">DABI · ${src||'AI'}</div>
${ans}
${conf!=null?`<div class="cm-conf">CONFIDENCE: ${Math.round(conf*100)}%</div>`:''}
</div>`;
box.scrollTop=box.scrollHeight;
} catch(e){
const t=$('_typing'); if(t)t.remove();
box.innerHTML+=`<div class="cm error">Greška: ${e.message}</div>`;
box.scrollTop=box.scrollHeight;
}
}
function ask(el){ sendMsg(el.textContent) }
// ──── BOOT ────
document.addEventListener('DOMContentLoaded',()=>{
initDash();
loadClubs();
});
</script>
</body>
</html>