diff --git a/llm_team_ui.py b/llm_team_ui.py
index 3d4fcd8..252430b 100644
--- a/llm_team_ui.py
+++ b/llm_team_ui.py
@@ -606,6 +606,7 @@ h1 span{color:var(--accent)}
Nginx Access
Security Raw
Threat Intel
+ Wall of Shame
@@ -706,6 +707,10 @@ async function loadLogs() {
await loadThreats();
return;
}
+ if (currentSource === 'shame') {
+ await loadWallOfShame();
+ return;
+ }
var r = await fetch('/api/admin/logs?source=' + currentSource + '&limit=' + limit);
var d = await r.json();
if (currentSource === 'runs') {
@@ -1167,6 +1172,18 @@ async function enrichIP(ip, card) {
errDiv.textContent = 'AI error: ' + d.ai_analysis.error;
panel.appendChild(errDiv);
}
+ // Saved indicator
+ if (d.saved) {
+ var savedDiv = document.createElement('div');
+ savedDiv.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:9px;color:#4ade80;margin-top:8px;text-transform:uppercase;letter-spacing:1px';
+ savedDiv.textContent = '✓ Saved to Wall of Shame database';
+ panel.appendChild(savedDiv);
+ } else if (d.save_error) {
+ var seDiv = document.createElement('div');
+ seDiv.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:9px;color:#e05252;margin-top:8px';
+ seDiv.textContent = 'Save error: ' + d.save_error;
+ panel.appendChild(seDiv);
+ }
} catch(e) {
panel.textContent = 'Error: ' + e.message;
panel.style.color = '#e05252';
@@ -1188,8 +1205,145 @@ async function banAction(ip, action) {
} catch(e) { alert('Error: ' + e.message); }
}
+async function loadWallOfShame() {
+ var view = document.getElementById('log-view');
+ view.textContent = '';
+ try {
+ var r = await fetch('/api/admin/wall-of-shame?sort=enriched_at&order=desc');
+ var d = await r.json();
+ var entries = d.entries || [];
+ if (!entries.length) {
+ var e = document.createElement('div'); e.className = 'empty';
+ e.textContent = 'No enriched IPs yet. Use the "Enrich" button on Threat Intel to scan IPs.';
+ view.appendChild(e); return;
+ }
+
+ // Stats bar
+ var stats = document.createElement('div');
+ stats.style.cssText = 'display:grid;grid-template-columns:repeat(5,1fr);gap:8px;margin-bottom:16px';
+ var total = entries.length;
+ var crit = entries.filter(function(e){return e.threat_level==='critical'}).length;
+ var high = entries.filter(function(e){return e.threat_level==='high'}).length;
+ var proxies = entries.filter(function(e){return e.is_proxy}).length;
+ var automated = entries.filter(function(e){return e.likely_automated}).length;
+ [{v:total,l:'Total Profiled',c:'#d946ef'},{v:crit,l:'Critical',c:'#e05252'},{v:high,l:'High',c:'#f59e0b'},{v:proxies,l:'Proxies',c:'#e05252'},{v:automated,l:'Automated',c:'#c084fc'}].forEach(function(s){
+ var box = document.createElement('div');
+ box.style.cssText = 'background:rgba(0,0,0,0.3);border:2px solid #2a2d35;border-radius:2px;padding:12px;text-align:center;backdrop-filter:blur(16px)';
+ var val = document.createElement('div');
+ val.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:20px;font-weight:700;color:'+s.c;
+ val.textContent = s.v;
+ var lab = document.createElement('div');
+ lab.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:8px;text-transform:uppercase;letter-spacing:1.5px;color:#7a7872;margin-top:4px';
+ lab.textContent = s.l;
+ box.appendChild(val); box.appendChild(lab); stats.appendChild(box);
+ });
+ view.appendChild(stats);
+
+ // Table
+ var table = document.createElement('div');
+ table.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:10px';
+
+ // Header
+ var hdr = document.createElement('div');
+ hdr.style.cssText = 'display:grid;grid-template-columns:130px 70px 100px 1fr 80px 60px;gap:8px;padding:8px 12px;border-bottom:2px solid #2a2d35;color:#7a7872;text-transform:uppercase;letter-spacing:1px;font-size:8px;font-weight:700';
+ ['IP','Threat','Type','Summary','Country','Ports'].forEach(function(h){
+ var cell = document.createElement('span'); cell.textContent = h; hdr.appendChild(cell);
+ });
+ table.appendChild(hdr);
+
+ entries.forEach(function(e) {
+ var row = document.createElement('div');
+ row.style.cssText = 'display:grid;grid-template-columns:130px 70px 100px 1fr 80px 60px;gap:8px;padding:8px 12px;border-bottom:1px solid rgba(42,45,53,0.3);align-items:center;cursor:pointer;transition:background 0.1s';
+ row.onmouseenter = function(){row.style.background='rgba(217,70,239,0.03)'};
+ row.onmouseleave = function(){row.style.background='transparent'};
+
+ // IP
+ var ipCell = document.createElement('span'); ipCell.style.cssText = 'font-weight:700;color:#e8e6e3';
+ ipCell.textContent = e.ip; row.appendChild(ipCell);
+
+ // Threat
+ var threatColors = {critical:'#e05252',high:'#f59e0b',medium:'#e2b55a',low:'#7a7872'};
+ var tCell = document.createElement('span'); tCell.style.cssText = 'font-weight:700;color:'+(threatColors[e.threat_level]||'#7a7872');
+ tCell.textContent = (e.threat_level||'?').toUpperCase(); row.appendChild(tCell);
+
+ // Type
+ var cCell = document.createElement('span'); cCell.style.color = '#c084fc';
+ cCell.textContent = e.classification || e.attack_type || '?'; row.appendChild(cCell);
+
+ // Summary
+ var sCell = document.createElement('span'); sCell.style.cssText = 'color:#7a7872;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
+ sCell.textContent = e.summary || ''; sCell.title = e.summary || ''; row.appendChild(sCell);
+
+ // Country
+ var coCell = document.createElement('span'); coCell.style.color = '#e8e6e3';
+ coCell.textContent = e.country_code || '?'; row.appendChild(coCell);
+
+ // Ports
+ var pCell = document.createElement('span'); pCell.style.color = '#e05252';
+ var ports = e.open_ports || [];
+ pCell.textContent = ports.length ? ports.join(',') : '-'; row.appendChild(pCell);
+
+ // Click to expand detail
+ var detail = document.createElement('div');
+ detail.style.cssText = 'display:none;grid-column:1/-1;padding:10px 0;border-bottom:1px solid rgba(217,70,239,0.15)';
+ row.onclick = function() {
+ if (detail.style.display === 'none') {
+ detail.style.display = 'block';
+ detail.textContent = '';
+ var grid = document.createElement('div');
+ grid.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:6px;font-size:10px';
+ var fields = [
+ ['ISP', e.isp], ['Org', e.org], ['ASN', e.asn],
+ ['City', (e.city||'?')+', '+(e.country||'?')],
+ ['Proxy', e.is_proxy?'YES':'No'], ['Hosting', e.is_hosting?'YES':'No'],
+ ['Confidence', ((e.confidence||0)*100).toFixed(0)+'%'],
+ ['Automated', e.likely_automated?'YES':'No'],
+ ['Blocklists', (e.blocklist_count||0)+'/'+(e.blocklist_total||0)],
+ ['Log Entries', e.log_count||0],
+ ['Scanned', e.enriched_at ? new Date(e.enriched_at).toLocaleString() : '?'],
+ ['Updated', e.updated_at ? new Date(e.updated_at).toLocaleString() : '?']
+ ];
+ fields.forEach(function(f) {
+ var box = document.createElement('div');
+ var label = document.createElement('span'); label.style.cssText = 'color:#7a7872;font-size:8px;text-transform:uppercase;letter-spacing:1px';
+ label.textContent = f[0]+': ';
+ var val = document.createElement('span');
+ val.style.color = (f[0]==='Proxy'&&e.is_proxy)||(f[0]==='Hosting'&&e.is_hosting)||(f[0]==='Automated'&&e.likely_automated) ? '#e05252' : '#e8e6e3';
+ val.textContent = f[1];
+ box.appendChild(label); box.appendChild(val); grid.appendChild(box);
+ });
+ detail.appendChild(grid);
+ if (e.pattern) {
+ var pat = document.createElement('div');
+ pat.style.cssText = 'margin-top:6px;color:#c084fc;font-size:10px';
+ pat.textContent = 'Pattern: ' + e.pattern; detail.appendChild(pat);
+ }
+ if (e.recommendation) {
+ var rec = document.createElement('div');
+ rec.style.cssText = 'margin-top:4px;color:#4ade80;border-left:2px solid #4ade80;padding-left:8px;font-size:10px';
+ rec.textContent = 'Rec: ' + e.recommendation; detail.appendChild(rec);
+ }
+ if (e.indicators && e.indicators.length) {
+ var ind = document.createElement('div');
+ ind.style.cssText = 'margin-top:4px;color:#7a7872;font-size:9px';
+ ind.textContent = 'Indicators: ' + e.indicators.join(' | '); detail.appendChild(ind);
+ }
+ } else {
+ detail.style.display = 'none';
+ }
+ };
+ table.appendChild(row);
+ table.appendChild(detail);
+ });
+ view.appendChild(table);
+ } catch(e) {
+ var err = document.createElement('div'); err.className = 'empty'; err.textContent = 'Error: ' + e.message;
+ view.appendChild(err);
+ }
+}
+
loadLogs();
-setInterval(function() { if (currentSource !== 'runs' && currentSource !== 'threats') loadLogs(); }, 10000);
+setInterval(function() { if (currentSource !== 'runs' && currentSource !== 'threats' && currentSource !== 'shame') loadLogs(); }, 10000);