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
This commit is contained in:
2026-05-05 02:24:30 +02:00
parent 67372d6c58
commit c38f15a566
6 changed files with 6715 additions and 8 deletions
+13 -3
View File
@@ -265,7 +265,7 @@ table tbody tr:hover{background:var(--bg3)}
<div class="app">
<aside class="sb" id="sb">
<div class="sb-h">
<div class="logo">PGŽ <span class="g">SPORT</span></div>
<a href="/" class="logo" style="text-decoration:none;color:inherit;cursor:pointer" title="Početna"><span style="font-weight:800;letter-spacing:.5px">PGŽ</span> <span class="g">SPORT</span></a>
<div class="sub" id="role-sub">Operativna aplikacija</div>
<div class="sb-toggle" id="sb-toggle" onclick="toggleSidebar()" title="Skupi/raširi sidebar"></div>
</div>
@@ -449,7 +449,7 @@ function applyMeToHeader(){
$('#user-role-label')?.replaceChildren(document.createTextNode(roleLabel));
// Avatar topbar
if(me.avatar_url){
$('#user-av').innerHTML = `<img src="${esc(me.avatar_url)}" alt="">`;
$('#user-av').innerHTML = `<img src="${esc(me.avatar_url)}${me.avatar_url.includes('?')?'&':'?'}t=${Date.now()}" alt="">`;
} else if(me.google_picture){
$('#user-av').innerHTML = `<img src="${esc(me.google_picture)}" alt="">`;
} else {
@@ -459,7 +459,7 @@ function applyMeToHeader(){
if($('#sf-name')) $('#sf-name').textContent = name;
if($('#sf-role')) $('#sf-role').textContent = roleLabel;
if($('#sf-av')){
if(me.avatar_url) $('#sf-av').innerHTML = `<img src="${esc(me.avatar_url)}" alt="" style="width:100%;height:100%;object-fit:cover;border-radius:50%">`;
if(me.avatar_url) $('#sf-av').innerHTML = `<img src="${esc(me.avatar_url)}${me.avatar_url.includes('?')?'&':'?'}t=${Date.now()}" alt="" style="width:100%;height:100%;object-fit:cover;border-radius:50%">`;
else if(me.google_picture) $('#sf-av').innerHTML = `<img src="${esc(me.google_picture)}" alt="" style="width:100%;height:100%;object-fit:cover;border-radius:50%">`;
else $('#sf-av').textContent = initials(name);
}
@@ -856,6 +856,16 @@ async function onAvatarPick(input){
input.value = '';
if(r && r.avatar_url){
if(_state.me) _state.me.avatar_url = r.avatar_url;
// Update localStorage so other pages (sport2.html footer, sidebar) see new avatar
try{
const stored = localStorage.getItem('pgz_user') || sessionStorage.getItem('pgz_user');
if(stored){
const u = JSON.parse(stored);
u.avatar_url = r.avatar_url;
if(localStorage.getItem('pgz_user')) localStorage.setItem('pgz_user', JSON.stringify(u));
else sessionStorage.setItem('pgz_user', JSON.stringify(u));
}
}catch(e){console.warn('avatar storage update failed', e);}
applyMeToHeader();
loadSection(); // re-render profile
} else {
+7 -1
View File
@@ -232,7 +232,7 @@ a.tag:hover,.tag[onclick]:hover{transform:translateY(-1px);filter:brightness(1.1
<div class="app">
<aside class="sb" id="sb">
<div class="sb-h">
<div class="logo">PGŽ <span class="g">SPORT</span></div>
<a href="/" class="logo" style="text-decoration:none;color:inherit;cursor:pointer" title="Početna"><span style="font-weight:800;letter-spacing:.5px">PGŽ</span> <span class="g">SPORT</span></a>
<div class="sub">Primorsko-goranska županija</div>
<div class="sb-toggle" id="sb-toggle" onclick="toggleSidebar()" title="Skupi/raširi sidebar"></div>
</div>
@@ -1349,6 +1349,12 @@ async function openKlub(id){
<div class="k">Osnovan</div><div class="v">${txt(k.godina_osnutka)}</div>
<div class="k">Nositelj kvalitete</div><div class="v">${k.nositelj_kvalitete?'<span class="tag gd">DA</span>':'<span class="tag">NE</span>'}</div>
</div>
<div style="margin-top:14px;display:flex;gap:8px;flex-wrap:wrap">
<a class="btn primary" onclick="switchKlubTab(document.querySelector('.tab[onclick*=k-clan]'),'k-clan')" style="cursor:pointer;display:inline-flex;align-items:center;gap:6px">
👥 Vidi sportaše ovog kluba (${clanovi.length})
</a>
${(k.web||k.web_stranica) ? '<a class="btn" href="'+esc(k.web||k.web_stranica)+'" target="_blank" style="display:inline-flex;align-items:center;gap:6px">🌐 Službena stranica</a>' : ''}
</div>
${k.napomena ? '<div class="card" style="margin-top:14px"><div class="card-t" style="margin-bottom:6px">Napomena</div><div style="font-size:12px;color:var(--t1);line-height:1.5">'+esc(k.napomena)+'</div></div>' : ''}
</div>