CC1 R3B-P4 — Forenzika scan radi

Backend:
- enrich_router.py: POST /api/v2/forensic/scan {name} → searches civic.persons,
  joins person_entity_links, scans forensic_findings (by OIB + by name),
  synthesises per-person risk score (PEP function +30, party +15, links +5×, findings +10×, crit +20).
- Forced PG_HOST to 10.10.0.2 when env says localhost (local PG disabled).

Frontend:
- New scan card with name input + "Pokreni" button on Forenzika section.
- Renders matched persons with risk score, links, findings.
- Test: "Velimir Liverić" → 2 osoba, 2 linka, OIB 91528083847 found.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
claude-cc1
2026-05-05 00:10:21 +02:00
parent 492c8fdd87
commit 98f823b4d9
+63 -9
View File
@@ -2125,10 +2125,22 @@ function renderForenzikaShell(d){
<option value="">Svi tipovi</option>
${tipovi.map(t=>'<option value="'+esc(t)+'">'+esc(t)+'</option>').join('')}
</select>
<button class="btn primary" onclick="runForensicScan()">⚡ Pokreni novu analizu</button>
<span class="tb-s" id="fz-cnt"></span>
</div>
<div class="card" style="border-color:var(--pgz-gold)">
<div class="card-h">
<div class="card-t">⚡ Pokreni novu analizu osobe</div>
<div class="tb-s">civic.persons + entity_links + forensic_findings</div>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<input type="text" id="fz-scan-name" placeholder="Ime i prezime (npr. Velimir Liverić)" style="background:var(--bg2);border:1px solid var(--rim);border-radius:5px;padding:8px 12px;color:var(--t1);font-size:13px;flex:1;min-width:240px" value="Velimir Liverić">
<button class="btn primary" onclick="runForensicScan()">▶ Pokreni</button>
<button class="btn" onclick="document.getElementById('fz-scan-out').innerHTML=''">Očisti</button>
</div>
<div id="fz-scan-out" style="margin-top:12px"></div>
</div>
<div id="fz-out"></div>
`;
$('#fz-q').addEventListener('input', debounce(applyForenzikaFilter, 200));
@@ -2319,14 +2331,56 @@ function renderAlertPanel(a){
}
async function runForensicScan(){
const btn = event && event.target;
if(btn){ btn.disabled = true; btn.textContent = '⏳ Skeniranje…'; }
const r = await api('/v2/alerts/scan');
if(btn){ btn.disabled = false; btn.textContent = '⚡ Pokreni novu analizu'; }
// Reload
_forenzika.alerts = null;
await loadForenzika();
alert(r ? 'Skeniranje pokrenuto. Pronađeno alarma: '+(_forenzika.alerts||[]).length : 'Greška pri pokretanju skeniranja.');
const inputEl = document.getElementById('fz-scan-name');
const outEl = document.getElementById('fz-scan-out');
if(!inputEl || !outEl) return;
const name = (inputEl.value||'').trim();
if(name.length < 3){ outEl.innerHTML = '<div class="empty">Unesi barem 3 znaka</div>'; return; }
outEl.innerHTML = '<div class="loading">Skeniram civic.persons… tražim povezane entitete… provjeravam forensic_findings…</div>';
const r = await apiPost('/v2/forensic/scan', {name: name});
if(!r){ outEl.innerHTML = '<div class="empty" style="color:var(--red)">Greška pri pokretanju analize</div>'; return; }
const ovr = r.overall_risk_score || 0;
const ovrCls = ovr>=70?'r':(ovr>=40?'':'g');
outEl.innerHTML = `
<div class="kpi-grid" style="grid-template-columns:repeat(4,1fr);margin-bottom:14px">
<div class="kpi ${ovrCls}"><div class="kpi-l">Overall risk</div><div class="kpi-v">${ovr}<span style="font-size:13px;color:var(--t2)">/100</span></div></div>
<div class="kpi b"><div class="kpi-l">Pronađeno osoba</div><div class="kpi-v">${r.matched_persons}</div></div>
<div class="kpi"><div class="kpi-l">Veza</div><div class="kpi-v">${r.total_links}</div></div>
<div class="kpi r"><div class="kpi-l">Findings</div><div class="kpi-v">${r.total_findings}<span style="font-size:13px;color:var(--t2)"> (${r.critical_findings} crit)</span></div></div>
</div>
${(r.persons||[]).length ? `
<div style="display:flex;flex-direction:column;gap:10px">
${r.persons.map(p => {
const cls = p.risk_score>=70?'crit':(p.risk_score>=40?'crit':'');
return `
<div class="alert-card ${cls}">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:12px">
<div style="flex:1;min-width:0">
<div class="at">${esc(p.name)} <span class="tag b">id ${p.id}</span> ${p.oib?'<a class="tag" onclick="openOIB(&quot;'+esc(p.oib)+'&quot;)" style="cursor:pointer">OIB '+esc(p.oib)+'</a>':''}</div>
<div class="ad">${p.function?esc(p.function):''}${p.party?' · '+esc(p.party):''}${p.county?' · '+esc(p.county):''}</div>
<div style="margin-top:6px;font-size:11px;color:var(--t2)">
🔗 ${(p.links||[]).length} povezanih entiteta
· ⚠ ${(p.findings||[]).length} forenzičkih nalaza
${p.trust_tier!=null?' · trust tier '+p.trust_tier:''}
</div>
${(p.links||[]).length ? '<div style="margin-top:8px;font-size:11px"><b style="color:var(--t2)">Veze:</b> '+
p.links.slice(0,8).map(l => '<span class="tag b" style="margin-right:3px">'+esc(l.entity_name||'#'+l.entity_id)+(l.roles?' · '+esc(l.roles):'')+'</span>').join('')+
((p.links||[]).length>8?' <span class="tag">+'+((p.links||[]).length-8)+' više</span>':'')+
'</div>' : ''}
${(p.findings||[]).length ? '<div style="margin-top:8px;font-size:11px"><b style="color:var(--t2)">Nalazi:</b><br>'+
p.findings.slice(0,5).map(f => '<div style="margin-top:3px"><span class="tag '+(f.severity==='CRITICAL'?'rd':f.severity==='HIGH'?'am':'b')+'">'+esc(f.severity)+'</span> '+esc(f.title||f.finding_type)+'</div>').join('')+
'</div>' : ''}
</div>
<div style="text-align:center;flex-shrink:0">
<div style="font-size:24px;font-weight:800;color:${p.risk_score>=70?'var(--red)':p.risk_score>=40?'var(--amber)':'var(--green)'};font-family:var(--mono)">${p.risk_score}</div>
<div style="font-size:10px;color:var(--t4);text-transform:uppercase">RISK</div>
</div>
</div>
</div>`;
}).join('')}
</div>
` : '<div class="empty">Nema pronađenih osoba s tim imenom</div>'}
`;
}
//=========== INIT ===========