diff --git a/static/app.html b/static/app.html
new file mode 100644
index 0000000..f850d8b
--- /dev/null
+++ b/static/app.html
@@ -0,0 +1,1207 @@
+
+
+
+
+
+PGŽ SPORT — Operativna aplikacija
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Dashboard
+
Pregled stanja
+
+
+
+
+
DR
+
+
Damir Radulić
+
PGŽ admin
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/static/sport2.html b/static/sport2.html
index 4edf8a8..3b935a4 100644
--- a/static/sport2.html
+++ b/static/sport2.html
@@ -204,6 +204,15 @@ a.tag:hover,.tag[onclick]:hover{transform:translateY(-1px);filter:brightness(1.1
.iframe-map{width:100%;height:140px;border:0;border-radius:5px;background:var(--bg3)}
+.ac-wrap{position:relative}
+.ac-drop{display:none;position:absolute;top:100%;left:0;right:0;min-width:240px;background:var(--bg2);border:1px solid var(--rim2);border-radius:5px;box-shadow:0 6px 20px rgba(0,0,0,.5);z-index:100;max-height:300px;overflow-y:auto;margin-top:4px}
+.ac-item{padding:8px 12px;cursor:pointer;border-bottom:1px solid var(--rim);transition:background .12s}
+.ac-item:last-child{border-bottom:0}
+.ac-item:hover{background:var(--bg3)}
+.ac-item .ac-l{font-weight:600;color:var(--t0);font-size:12.5px}
+.ac-item .ac-s{font-size:10.5px;color:var(--t2);margin-top:2px}
+.ac-empty{padding:12px;text-align:center;color:var(--t4);font-size:11px;font-style:italic}
+
@media (max-width:768px){
.sb{transform:translateX(-100%);transition:transform .25s}
.sb.open{transform:translateX(0)}
@@ -1754,12 +1763,53 @@ async function loadMreza(){
resp = await r.json();
}catch(e){ console.error('graph fetch error', e); }
if(!resp || !resp.data){ root.innerHTML='Greška pri dohvatu graf-podataka
'; return; }
- _mreza.data = resp.data;
- _mreza.allNodes = (resp.data.nodes||[]).slice();
- _mreza.allEdges = (resp.data.edges||[]).slice();
+ const nodes = (resp.data.nodes||[]).slice();
+ const edges = (resp.data.edges||[]).slice();
+ // Augment with PGŽ savez central anchor (Nogometni savez PGŽ — id=10 in pgz_sport.savezi)
+ const anchorId = 'pgz-savez-nogometni';
+ if(!nodes.find(n => n.id === anchorId)){
+ nodes.push({
+ id: anchorId,
+ label: 'Nogometni savez PGŽ',
+ type: 'pgz_savez',
+ size: 40,
+ color: '#F4C430',
+ meta: {oib: '12345678901', city: 'Rijeka', risk: 0, pgz_savez_id: 10}
+ });
+ // Connect anchor to top-3 person & top-3 entity nodes (most central)
+ const topPersons = nodes.filter(n => n.type==='person').sort((a,b)=>(b.size||0)-(a.size||0)).slice(0,3);
+ const topEntities = nodes.filter(n => n.type==='entity').sort((a,b)=>(b.size||0)-(a.size||0)).slice(0,3);
+ for(const t of [...topPersons, ...topEntities]){
+ edges.push({source:anchorId, target:t.id, color:'#F4C43055', size:0.6});
+ }
+ }
+ _mreza.data = {nodes, edges};
+ _mreza.allNodes = nodes;
+ _mreza.allEdges = edges;
+ _mreza.anchorId = anchorId;
}
renderMrezaShell();
renderMrezaGraph();
+ // After render settles, center camera on anchor
+ setTimeout(() => centerMrezaOnAnchor(), 1500);
+}
+
+function centerMrezaOnAnchor(){
+ if(!_mreza.graph) return;
+ const anchor = (_mreza.allNodes||[]).find(n => n.id === _mreza.anchorId);
+ if(!anchor) return;
+ // 3d-force-graph stores position on the same node objects after sim runs
+ const liveNode = _mreza.graph.graphData().nodes.find(n => n.id === anchor.id);
+ if(!liveNode) return;
+ const dist = 200;
+ const distRatio = 1 + dist/Math.max(1, Math.hypot(liveNode.x||1, liveNode.y||1, liveNode.z||1));
+ try{
+ _mreza.graph.cameraPosition(
+ { x:(liveNode.x||0)*distRatio, y:(liveNode.y||0)*distRatio, z:(liveNode.z||0)*distRatio },
+ liveNode,
+ 1500
+ );
+ }catch(e){ console.warn('center anchor', e); }
}
function renderMrezaShell(){
@@ -1775,22 +1825,23 @@ function renderMrezaShell(){
Tvrtki / entiteta
${(_mreza.allNodes||[]).filter(n=>n.type==='entity'||n.type==='supplier').length}
-
`;
- $('#mr-osoba').addEventListener('input', debounce(applyMrezaFilter, 200));
- $('#mr-klub').addEventListener('input', debounce(applyMrezaFilter, 200));
- $('#mr-tvrtka').addEventListener('input', debounce(applyMrezaFilter, 200));
+ // Wire autocomplete + filter on the 3 search inputs
+ ['#mr-osoba', '#mr-klub', '#mr-tvrtka'].forEach(sel => {
+ const el = $(sel);
+ if(!el) return;
+ el.addEventListener('input', debounce(() => { applyMrezaFilter(); fetchSuggest(el); }, 200));
+ el.addEventListener('keydown', e => {
+ if(e.key === 'Enter'){ e.preventDefault(); pickFirstSuggest(el); }
+ if(e.key === 'Escape'){ closeSuggest(el); }
+ });
+ el.addEventListener('blur', () => setTimeout(() => closeSuggest(el), 200));
+ });
$('#mr-tip').addEventListener('change', applyMrezaFilter);
}
+async function fetchSuggest(inputEl){
+ const q = (inputEl.value||'').trim();
+ const drop = document.getElementById(inputEl.id + '-drop');
+ if(!drop) return;
+ if(q.length < 2){ drop.innerHTML = ''; drop.style.display='none'; return; }
+ const type = inputEl.dataset.acType || '';
+ const r = await api('/v2/search/suggest?q='+encodeURIComponent(q)+'&type='+type+'&limit=10');
+ if(!r){ drop.innerHTML=''; drop.style.display='none'; return; }
+ const results = r.results || [];
+ if(!results.length){ drop.innerHTML='Nema rezultata
'; drop.style.display='block'; return; }
+ drop.innerHTML = results.map(s => `
+
+
${esc(s.label)}
+
${esc(s.sub||s.type||'')}
+
+ `).join('');
+ drop.style.display = 'block';
+ drop.dataset.firstId = results[0].id;
+ drop.dataset.firstLabel = results[0].label;
+}
+
+function pickSuggest(inputElId, itemEl){
+ const inputEl = document.getElementById(inputElId);
+ const id = itemEl.dataset.id;
+ const label = itemEl.dataset.label;
+ if(inputEl){ inputEl.value = label; }
+ closeSuggest(inputEl);
+ centerMrezaOnSuggestion(id, label);
+ applyMrezaFilter();
+}
+
+function pickFirstSuggest(inputEl){
+ const drop = document.getElementById(inputEl.id + '-drop');
+ if(drop && drop.dataset.firstId){
+ inputEl.value = drop.dataset.firstLabel || '';
+ centerMrezaOnSuggestion(drop.dataset.firstId, drop.dataset.firstLabel);
+ }
+ closeSuggest(inputEl);
+ applyMrezaFilter();
+}
+
+function closeSuggest(inputEl){
+ if(!inputEl) return;
+ const drop = document.getElementById(inputEl.id + '-drop');
+ if(drop){ drop.style.display='none'; }
+}
+
+function centerMrezaOnSuggestion(suggId, label){
+ // Try to find an existing node by label (case-insensitive partial match)
+ const lc = (label||'').toLowerCase();
+ const live = _mreza.graph ? _mreza.graph.graphData().nodes : (_mreza.allNodes||[]);
+ let target = live.find(n => (n.label||'').toLowerCase() === lc);
+ if(!target) target = live.find(n => (n.label||'').toLowerCase().includes(lc));
+ if(!target && _mreza.graph){
+ // Add a new injected node + edge from anchor for visual context
+ const anchorId = _mreza.anchorId;
+ const newNode = {id: suggId, label: label, type: 'injected', size: 18, color: '#00c8e8', meta: {injected: true}};
+ const data = _mreza.graph.graphData();
+ data.nodes.push(newNode);
+ if(anchorId) data.links.push({source: anchorId, target: suggId, color:'#00c8e855', size:0.8});
+ _mreza.graph.graphData(data);
+ _mreza.allNodes.push(newNode);
+ if(anchorId) _mreza.allEdges.push({source: anchorId, target: suggId, color:'#00c8e855', size:0.8});
+ setTimeout(() => centerMrezaOnSuggestion(suggId, label), 800);
+ return;
+ }
+ if(target && _mreza.graph){
+ const dist = 120;
+ const x = target.x||0, y = target.y||0, z = target.z||0;
+ const r = 1 + dist/Math.max(1, Math.hypot(x||1,y||1,z||1));
+ try{ _mreza.graph.cameraPosition({x:x*r, y:y*r, z:z*r}, target, 1200); }catch(e){}
+ }
+}
+
function applyMrezaFilter(){
const osoba = ($('#mr-osoba').value||'').toLowerCase().trim();
const klub = ($('#mr-klub').value||'').toLowerCase().trim();
@@ -2326,10 +2459,49 @@ function renderAlertPanel(a){
Kreirano
${a.created_at?fmtDate(a.created_at):'—'}
+ ${forensicEnrichBlock(a.id)}
`;
openPanel('Alarm #'+a.id, html);
}
+function forensicEnrichBlock(findingId){
+ return `
+
+
+
✨ Obogati podatke (Wikipedia)
+
+
+
+
Ekstrakcija imena iz nalaza, lookup na Wikipedia HR i sprema u DB. Drugi puta će biti vidljivo bez ponovnog skidanja.
+
+
+ `;
+}
+
+async function enrichForensicFinding(findingId){
+ const out = document.getElementById('fenrich-out-'+findingId);
+ if(out) out.innerHTML = 'Ekstraktiram ime, lookup Wikipedia HR…
';
+ const r = await apiPost('/v2/forensic/findings/'+findingId+'/enrich');
+ if(!r){ if(out) out.innerHTML = 'Greška
'; return; }
+ const w = r.wiki || null;
+ const html = `
+
+ 🟢 Persisted
+ ${r.used_query?'query: '+esc(r.used_query)+'':''}
+
+ ${w ? `
+
+
📚 Wikipedia HR
+
${esc(w.title||'')}
+ ${w.description?'
'+esc(w.description)+'
':''}
+ ${w.extract?'
'+esc(w.extract)+'
':''}
+ ${w.url?'
':''}
+
+ ` : 'Nije pronađen Wikipedia HR članak za ekstrahirana imena.
Pokušaji: '+esc(JSON.stringify(r.queried||[]))+'
'}
+ `;
+ if(out) out.innerHTML = html;
+}
+
async function runForensicScan(){
const inputEl = document.getElementById('fz-scan-name');
const outEl = document.getElementById('fz-scan-out');