HNS sprint: 3-tab drill-down + parallel deep scraper dispatch
HNS-1 verify: smoke test 93409 OK, gap 854 uncovered, throughput ~60/min HNS-2 dispatch: scripts/hns_dispatch.sh + 5 parallel workers shard'd po roster ID; coverage 265→1098 distinct_seasons (93.7% of 1172 roster), 125→971 distinct_matches; total seasons 3170→13371, matches 23515→150071 HNS-3 UI: 6-tab panel collapsed na 3 (🏆 Karijera / 📅 Utakmice / 👤 Profil); novi /api/v2/clan/{id}/hns-matches?limit=30 + /clan/{id}/hns-profile (bio + summary + HNS deep link); prof-grid 3-col card s gold jersey badge; OIB RBAC-mask. Test Josip Zec 449: 72 sez/30 utak. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+89
-46
@@ -205,6 +205,30 @@ a.tag:hover,.tag[onclick]:hover{transform:translateY(-1px);filter:brightness(1.1
|
||||
|
||||
.utlogo{width:22px;height:22px;border-radius:50%;background:var(--bg3);object-fit:contain;vertical-align:middle;margin-right:6px}
|
||||
|
||||
/* HNS-3 (2026-05-05) — Profil tab styles (Palantir Gotham aesthetic) */
|
||||
.prof-top{display:flex;align-items:flex-start;gap:18px;padding:14px;background:var(--bg2);border:1px solid var(--rim);border-radius:6px;margin-bottom:14px}
|
||||
.prof-name-block{flex:1;min-width:0}
|
||||
.prof-name{font-size:20px;font-weight:800;color:var(--t0);letter-spacing:.3px;margin-bottom:4px}
|
||||
.prof-sub{color:var(--t2);font-size:12px;margin-bottom:6px}
|
||||
.prof-club{font-size:12.5px;color:var(--t1);display:flex;align-items:center;flex-wrap:wrap;gap:6px}
|
||||
.prof-dres{flex-shrink:0;width:96px;height:96px;border:2px solid var(--pgz-gold);border-radius:8px;display:flex;flex-direction:column;align-items:center;justify-content:center;background:linear-gradient(135deg,var(--bg3),var(--bg2))}
|
||||
.prof-dres-num{font-size:38px;font-weight:900;color:var(--pgz-gold);line-height:1;font-family:var(--mono)}
|
||||
.prof-dres-l{font-size:9.5px;color:var(--t3);letter-spacing:1.5px;font-weight:700;margin-top:4px}
|
||||
.prof-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:6px}
|
||||
.prof-cell{padding:9px 12px;background:var(--bg2);border:1px solid var(--rim);border-radius:5px}
|
||||
.prof-cell .l{font-size:10px;color:var(--t3);text-transform:uppercase;letter-spacing:.5px;font-weight:700;margin-bottom:3px}
|
||||
.prof-cell .v{font-size:13px;color:var(--t0);font-weight:600;word-break:break-word}
|
||||
.prof-cell .v b{color:var(--pgz-gold);font-family:var(--mono)}
|
||||
@media (max-width:760px){
|
||||
.prof-grid{grid-template-columns:repeat(2,1fr)}
|
||||
.prof-top{flex-direction:column-reverse;align-items:stretch}
|
||||
.prof-dres{width:100%;height:78px;flex-direction:row;gap:14px}
|
||||
.prof-dres-num{font-size:32px}
|
||||
}
|
||||
@media (max-width:420px){
|
||||
.prof-grid{grid-template-columns:1fr}
|
||||
}
|
||||
|
||||
.score{display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:4px;background:var(--bg3);font-size:11px;font-weight:600}
|
||||
.score.high{background:rgba(0,232,143,.15);color:var(--green)}
|
||||
.score.mid{background:rgba(245,158,11,.15);color:var(--amber)}
|
||||
@@ -1973,7 +1997,7 @@ async function openSportas(id){
|
||||
const d = Object.assign({}, dRaw);
|
||||
if(dV2 && dV2.profile){
|
||||
const p = dV2.profile;
|
||||
['visina_cm','tezina_kg','dominantna_noga','broj_dresa','biografija','mjesto_rodenja','mjesto_rodjenja','datum_rodenja','datum_rodjenja','spol','sport','aktivan']
|
||||
['visina_cm','tezina_kg','dominantna_noga','broj_dresa','biografija','mjesto_rodenja','mjesto_rodjenja','datum_rodenja','datum_rodjenja','spol','sport','aktivan','pozicija','klub_naziv','klub_id','dob_age','hns_igrac_id','profile_url','reprezentativac','stipendiran','datum_pristupa','email','telefon','oib','slug','slika_url']
|
||||
.forEach(k => { if((d[k]===null||d[k]===undefined||d[k]==='') && p[k]!==null && p[k]!==undefined && p[k]!=='') d[k]=p[k]; });
|
||||
}
|
||||
const stats = d.stats || {};
|
||||
@@ -2040,40 +2064,39 @@ async function openSportas(id){
|
||||
<div class="pp-stat"><div class="v">${fmtNum(stats.sezone_aktivne||sezone.length)}</div><div class="l">Sezona</div></div>
|
||||
</div>
|
||||
|
||||
<!-- HNS-3 (2026-05-05) — 3 explicit tabs: HNS Karijera / Utakmice (last 30) / Profil -->
|
||||
<div class="tabs">
|
||||
<div class="tab active" onclick="switchPlayerTab(this,'p-sez')">🏆 HNS Karijera (${sezone.length})</div>
|
||||
<div class="tab" onclick="switchPlayerTab(this,'p-utak')">📅 Posljednje utakmice (${utakmice.length})</div>
|
||||
<div class="tab" onclick="switchPlayerTab(this,'p-kat')">🏷️ Kategorije (${kategorije.length})</div>
|
||||
<div class="tab" onclick="switchPlayerTab(this,'p-bio')">Bio</div>
|
||||
<div class="tab" onclick="switchPlayerTab(this,'p-god')">Godišnjaci (${godisnjaci.length})</div>
|
||||
<div class="tab" onclick="switchPlayerTab(this,'p-nag')">Nagrade (${nagrade.length})</div>
|
||||
<div class="tab" onclick="switchPlayerTab(this,'p-utak')">📅 Utakmice (poslj. ${Math.min(utakmice.length,30)})</div>
|
||||
<div class="tab" onclick="switchPlayerTab(this,'p-prof')">👤 Profil</div>
|
||||
</div>
|
||||
|
||||
<div id="p-sez" class="ptab">
|
||||
<div class="pp-section-h">🏆 HNS Karijera <span class="cnt">${sezone.length} sezon${sezone.length===1?'a':(sezone.length<5&&sezone.length>1?'e':'a')}</span></div>
|
||||
${sezone.length ? `<div style="overflow-x:auto"><table>
|
||||
<thead><tr><th>Sezona</th><th>Natjecanje</th><th>Klub</th><th class="num">Nas.</th><th class="num">Gol.</th><th class="num">Asis.</th><th class="num">Žuti</th><th class="num">Crv.</th><th></th></tr></thead>
|
||||
<tbody>${sezone.map(s => `
|
||||
<thead><tr><th>Sezona</th><th>Klub</th><th>Natjecanje</th><th class="num">Nastupi</th><th class="num">Golovi</th><th class="num">Asis.</th><th class="num">Žuti</th><th class="num">Crv.</th><th class="num">Min.</th><th></th></tr></thead>
|
||||
<tbody>${[...sezone].sort((a,b)=>String(b.sezona||'').localeCompare(String(a.sezona||''))).map(s => `
|
||||
<tr class="no-click">
|
||||
<td><b>${esc(s.sezona||'')}</b></td>
|
||||
<td>${esc(s.natjecanje||'')}</td>
|
||||
<td>${esc(s.klub_naziv||'')}</td>
|
||||
<td>${esc(s.natjecanje||'')}</td>
|
||||
<td class="num">${fmtNum(s.nastupi)}</td>
|
||||
<td class="num"><b style="color:var(--pgz-gold)">${fmtNum(s.pogoci ?? s.golovi)}</b></td>
|
||||
<td class="num">${fmtNum(s.asistencije)}</td>
|
||||
<td class="num">${fmtNum(s.zuti_kartoni ?? s.zuti)}</td>
|
||||
<td class="num">${fmtNum(s.crveni_kartoni ?? s.crveni)}</td>
|
||||
<td class="num" style="color:var(--amber)">${fmtNum(s.zuti_kartoni ?? s.zuti)}</td>
|
||||
<td class="num" style="color:var(--red)">${fmtNum(s.crveni_kartoni ?? s.crveni)}</td>
|
||||
<td class="num">${fmtNum(s.minute)}</td>
|
||||
<td>${(s.natjecanje_url||s.source_url)?'<a href="'+esc(s.natjecanje_url||s.source_url)+'" target="_blank">↗</a>':''}</td>
|
||||
</tr>`).join('')}
|
||||
</tbody>
|
||||
</table></div>` : '<div class="empty">Nema sezonskih podataka</div>'}
|
||||
</table></div>` : '<div class="empty">Nema sezonskih podataka iz HNS Semafora</div>'}
|
||||
</div>
|
||||
|
||||
<div id="p-utak" class="ptab" style="display:none">
|
||||
<div class="pp-section-h">📅 Posljednje utakmice <span class="cnt">${utakmice.length} zabilježeno</span></div>
|
||||
${utakmice.length ? `<div style="overflow-x:auto;max-height:500px;overflow-y:auto"><table>
|
||||
<thead><tr><th>Datum</th><th>Natjecanje</th><th colspan="3">Susret</th><th>Pozicija</th><th class="num">Gol.</th><th class="num">Min.</th><th></th></tr></thead>
|
||||
<tbody>${utakmice.map(u => {
|
||||
<div class="pp-section-h">📅 Utakmice <span class="cnt">posljednjih ${Math.min(utakmice.length,30)} ${utakmice.length===1?'utakmica':(utakmice.length<5&&utakmice.length>1?'utakmice':'utakmica')}</span></div>
|
||||
${utakmice.length ? `<div style="overflow-x:auto;max-height:560px;overflow-y:auto"><table>
|
||||
<thead><tr><th>Datum</th><th>Natjecanje</th><th colspan="3">Susret</th><th>Pozicija</th><th class="num">Min.</th><th class="num">Gol.</th><th class="num">Asis.</th><th class="num">Žuti</th><th class="num">Crv.</th><th></th></tr></thead>
|
||||
<tbody>${utakmice.slice(0,30).map(u => {
|
||||
const dom = u.klub_dom || u.domacin || '';
|
||||
const gost = u.klub_gost || u.gost || '';
|
||||
const golovi = (u.pogodaka != null ? u.pogodaka : u.golovi);
|
||||
@@ -2086,8 +2109,11 @@ async function openSportas(id){
|
||||
<td><b>${esc(u.rezultat||'-')}</b></td>
|
||||
<td>${u.klub_gost_logo?'<img src="'+esc(u.klub_gost_logo)+'" class="utlogo" onerror="this.style.display=\'none\'">':''}${esc(gost)}</td>
|
||||
<td>${esc(u.pozicija||'—')}</td>
|
||||
<td class="num"><b style="color:var(--pgz-gold)">${fmtNum(golovi)}</b></td>
|
||||
<td class="num">${fmtNum(minute)}</td>
|
||||
<td class="num"><b style="color:var(--pgz-gold)">${fmtNum(golovi)}</b></td>
|
||||
<td class="num">${fmtNum(u.asistencije)}</td>
|
||||
<td class="num" style="color:var(--amber)">${fmtNum(u.zuti)}</td>
|
||||
<td class="num" style="color:var(--red)">${fmtNum(u.crveni)}</td>
|
||||
<td>${u.source_url?'<a href="'+esc(u.source_url)+'" target="_blank">↗</a>':''}</td>
|
||||
</tr>`;
|
||||
}).join('')}
|
||||
@@ -2095,9 +2121,46 @@ async function openSportas(id){
|
||||
</table></div>` : '<div class="empty">Nema podataka o utakmicama</div>'}
|
||||
</div>
|
||||
|
||||
<div id="p-kat" class="ptab" style="display:none">
|
||||
<div class="pp-section-h">🏷️ Kategorije <span class="cnt">${kategorije.length} ${kategorije.length===1?'zapis':(kategorije.length<5&&kategorije.length>1?'zapisa':'zapisa')}</span></div>
|
||||
${kategorije.length ? `<div style="overflow-x:auto;max-height:500px;overflow-y:auto"><table>
|
||||
<div id="p-prof" class="ptab" style="display:none">
|
||||
<div class="pp-section-h">👤 Profil <span class="cnt">${esc(d.ime||'')} ${esc(d.prezime||'')}</span></div>
|
||||
|
||||
<!-- Top: name + dres + active club + HNS deep link -->
|
||||
<div class="prof-top">
|
||||
<div class="prof-name-block">
|
||||
<div class="prof-name">${esc(d.ime||'')} ${esc(d.prezime||'')}</div>
|
||||
<div class="prof-sub">
|
||||
${dob?'📅 '+fmtDate(dob)+(d.dob_age?' · '+d.dob_age+' god.':(dob?(' · '+(new Date().getFullYear()-Number(String(dob).slice(0,4)))+' god.'):'')):'—'}
|
||||
${(d.mjesto_rodjenja||d.mjesto_rodenja)?' · '+esc(d.mjesto_rodjenja||d.mjesto_rodenja):''}
|
||||
</div>
|
||||
<div class="prof-club">
|
||||
${d.klub_id ? '<a class="link-chip" onclick="closePanel();setTimeout(()=>openKlub('+d.klub_id+'),250)">🏟️ '+esc(d.klub_naziv_full||d.klub_naziv||d.klub_naziv_godisnjak||'—')+'</a>' : '🏟️ '+esc(d.klub_naziv_full||d.klub_naziv||d.klub_naziv_godisnjak||'—')}
|
||||
${d.aktivan?'<span class="tag gr" style="margin-left:8px">AKTIVAN</span>':'<span class="tag rd" style="margin-left:8px">NEAKTIVAN</span>'}
|
||||
</div>
|
||||
${hnsUrl?'<div style="margin-top:10px"><a class="pp-link hns" href="'+esc(hnsUrl)+'" target="_blank" rel="noopener">⚽ HNS Semafor profil ↗</a></div>':''}
|
||||
</div>
|
||||
${d.broj_dresa?'<div class="prof-dres"><div class="prof-dres-num">'+esc(d.broj_dresa)+'</div><div class="prof-dres-l">DRES</div></div>':''}
|
||||
</div>
|
||||
|
||||
<!-- Bio grid -->
|
||||
<div class="prof-grid">
|
||||
<div class="prof-cell"><div class="l">Pozicija</div><div class="v">${txt(d.pozicija)}</div></div>
|
||||
<div class="prof-cell"><div class="l">Dominantna noga</div><div class="v">${txt(d.dominantna_noga)}</div></div>
|
||||
<div class="prof-cell"><div class="l">Visina</div><div class="v">${d.visina_cm?'<b>'+d.visina_cm+'</b> cm':'—'}</div></div>
|
||||
<div class="prof-cell"><div class="l">Težina</div><div class="v">${d.tezina_kg?'<b>'+d.tezina_kg+'</b> kg':'—'}</div></div>
|
||||
<div class="prof-cell"><div class="l">Datum rođenja</div><div class="v">${fmtDate(dob)}</div></div>
|
||||
<div class="prof-cell"><div class="l">Spol</div><div class="v">${txt(d.spol)}</div></div>
|
||||
<div class="prof-cell"><div class="l">OIB</div><div class="v">${d.oib?(canSeeFullOib({klub_id:d.klub_id,savez_id:d.savez_id})?'<a class="link-chip" onclick="openOIB("'+esc(d.oib)+'")">'+esc(d.oib)+'</a>':maskOib(d.oib)):'—'}</div></div>
|
||||
<div class="prof-cell"><div class="l">Email</div><div class="v">${d.email?'<a href="mailto:'+esc(d.email)+'">'+esc(d.email)+'</a>':'—'}</div></div>
|
||||
<div class="prof-cell"><div class="l">Telefon</div><div class="v">${txt(d.telefon)}</div></div>
|
||||
<div class="prof-cell"><div class="l">HNS ID</div><div class="v">${hnsId?esc(hnsId):'—'}</div></div>
|
||||
<div class="prof-cell"><div class="l">Datum pristupa</div><div class="v">${fmtDate(d.datum_pristupa)}</div></div>
|
||||
<div class="prof-cell"><div class="l">Status</div><div class="v">${d.aktivan?'<span class="tag gr">AKTIVAN</span>':'<span class="tag rd">NEAKTIVAN</span>'}${d.reprezentativac?' <span class="tag gd">REPR</span>':''}${d.stipendiran?' <span class="tag am">STIP</span>':''}</div></div>
|
||||
</div>
|
||||
|
||||
${d.biografija ? '<div class="card" style="margin-top:14px"><div class="card-t">Biografija</div><div style="font-size:12px;line-height:1.5;color:var(--t1);margin-top:6px">'+esc(d.biografija)+'</div></div>' : ''}
|
||||
|
||||
${kategorije.length ? `<div class="pp-section-h" style="margin-top:18px">🏷️ Kategorije <span class="cnt">${kategorije.length}</span></div>
|
||||
<div style="overflow-x:auto;max-height:260px;overflow-y:auto"><table>
|
||||
<thead><tr><th>Sezona</th><th>Kategorija</th><th>Klub</th><th>Izvor</th><th></th></tr></thead>
|
||||
<tbody>${kategorije.map(k => `
|
||||
<tr class="no-click">
|
||||
@@ -2108,37 +2171,17 @@ async function openSportas(id){
|
||||
<td>${k.source_url?'<a href="'+esc(k.source_url)+'" target="_blank" rel="noopener">↗</a>':''}</td>
|
||||
</tr>`).join('')}
|
||||
</tbody>
|
||||
</table></div>` : '<div class="empty">Nema M2M kategorija (clan_kategorije)</div>'}
|
||||
</div>
|
||||
</table></div>` : ''}
|
||||
|
||||
<div id="p-bio" class="ptab" style="display:none">
|
||||
${godisnjaci.length ? `<div class="pp-section-h" style="margin-top:18px">📚 Godišnjaci <span class="cnt">${godisnjaci.length}</span></div>
|
||||
<div class="kv">
|
||||
<div class="k">OIB</div><div class="v">${d.oib?(canSeeFullOib({klub_id:d.klub_id,savez_id:d.savez_id})?'<a class="link-chip" onclick="openOIB("'+esc(d.oib)+'")">'+esc(d.oib)+'</a>':maskOib(d.oib)):'—'}</div>
|
||||
<div class="k">Datum rođenja</div><div class="v">${fmtDate(dob)}</div>
|
||||
<div class="k">Mjesto rođenja</div><div class="v">${txt(d.mjesto_rodjenja||d.mjesto_rodenja)}</div>
|
||||
<div class="k">Spol</div><div class="v">${txt(d.spol)}</div>
|
||||
<div class="k">Visina</div><div class="v">${d.visina_cm?d.visina_cm+' cm':'—'}</div>
|
||||
<div class="k">Težina</div><div class="v">${d.tezina_kg?d.tezina_kg+' kg':'—'}</div>
|
||||
<div class="k">Dom. noga</div><div class="v">${txt(d.dominantna_noga)}</div>
|
||||
<div class="k">Status</div><div class="v">${d.aktivan?'AKTIVAN':'NEAKTIVAN'}</div>
|
||||
<div class="k">Datum pristupa</div><div class="v">${fmtDate(d.datum_pristupa)}</div>
|
||||
<div class="k">Email</div><div class="v">${d.email?'<a href="mailto:'+esc(d.email)+'">'+esc(d.email)+'</a>':'—'}</div>
|
||||
<div class="k">Telefon</div><div class="v">${txt(d.telefon)}</div>
|
||||
<div class="k">Profil</div><div class="v">${(d.profile_url||d.scrape_url)?'<a href="'+esc(d.profile_url||d.scrape_url)+'" target="_blank">↗ vanjski profil</a>':'—'}</div>
|
||||
</div>
|
||||
${d.biografija ? '<div class="card" style="margin-top:14px"><div class="card-t">Biografija</div><div style="font-size:12px;line-height:1.5;color:var(--t1);margin-top:6px">'+esc(d.biografija)+'</div></div>' : ''}
|
||||
</div>
|
||||
|
||||
<div id="p-god" class="ptab" style="display:none">
|
||||
${godisnjaci.length ? `<div class="kv">
|
||||
<div class="k">Prvi godišnjak</div><div class="v">${esc(d.godisnjak_prvi||godisnjaci[0])}</div>
|
||||
<div class="k">Zadnji godišnjak</div><div class="v">${esc(d.godisnjak_zadnji||godisnjaci[godisnjaci.length-1])}</div>
|
||||
<div class="k">Sve godine</div><div class="v">${godisnjaci.map(g=>'<span class="tag b">'+esc(g)+'</span>').join(' ')}</div>
|
||||
</div>` : '<div class="empty">Nema podataka o godišnjacima</div>'}
|
||||
</div>
|
||||
</div>` : ''}
|
||||
|
||||
<div id="p-nag" class="ptab" style="display:none">
|
||||
${nagrade.length ? `<div style="overflow-x:auto"><table>
|
||||
${nagrade.length ? `<div class="pp-section-h" style="margin-top:18px">🏅 Nagrade <span class="cnt">${nagrade.length}</span></div>
|
||||
<div style="overflow-x:auto"><table>
|
||||
<thead><tr><th>Godina</th><th>Nagrada</th><th>Razina</th></tr></thead>
|
||||
<tbody>${nagrade.map(n => `
|
||||
<tr class="no-click">
|
||||
@@ -2147,7 +2190,7 @@ async function openSportas(id){
|
||||
<td>${esc(n.razina||'')}</td>
|
||||
</tr>`).join('')}
|
||||
</tbody>
|
||||
</table></div>` : '<div class="empty">Nema zabilježenih nagrada</div>'}
|
||||
</table></div>` : ''}
|
||||
</div>
|
||||
|
||||
${enrichBlock('sportas', d.id)}
|
||||
|
||||
Reference in New Issue
Block a user