diff --git a/llm_team_ui.py b/llm_team_ui.py
index cc1b620..439f10f 100644
--- a/llm_team_ui.py
+++ b/llm_team_ui.py
@@ -566,6 +566,28 @@ h1 span{color:var(--accent)}
.run-error{font-size:10px;color:var(--red);border-left:2px solid var(--red);padding-left:8px;margin:4px 0}
.empty{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--text2);padding:40px;text-align:center;opacity:0.5}
.filter-input{flex:1}
+.threat-card{background:rgba(0,0,0,0.25);border:2px solid var(--border);border-radius:2px;padding:12px 14px;margin-bottom:6px;display:flex;align-items:flex-start;gap:12px}
+.threat-card.critical{border-color:var(--red);background:rgba(224,82,82,0.04)}
+.threat-card.high{border-color:#f59e0b;background:rgba(245,158,11,0.03)}
+.threat-card.banned{opacity:0.5}
+.threat-ip{font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:700;min-width:130px;color:var(--text)}
+.threat-info{flex:1;min-width:0}
+.threat-row{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:4px}
+.threat-paths{font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--text2);margin-top:4px}
+.threat-actions{display:flex;gap:4px;flex-shrink:0}
+.ban-btn{font-family:'JetBrains Mono',monospace;font-size:9px;text-transform:uppercase;letter-spacing:0.5px;padding:5px 12px;border:2px solid;border-radius:2px;cursor:pointer;font-weight:700;background:transparent}
+.ban-btn.ban{color:var(--red);border-color:rgba(224,82,82,0.4)}
+.ban-btn.ban:hover{background:rgba(224,82,82,0.1)}
+.ban-btn.unban{color:var(--green);border-color:rgba(74,222,128,0.4)}
+.ban-btn.unban:hover{background:rgba(74,222,128,0.1)}
+.threat-summary{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:16px}
+.ts-box{background:rgba(0,0,0,0.3);border:2px solid var(--border);border-radius:2px;padding:12px;text-align:center;backdrop-filter:blur(16px)}
+.ts-val{font-family:'JetBrains Mono',monospace;font-size:20px;font-weight:700}
+.ts-val.red{color:var(--red)}
+.ts-val.green{color:var(--green)}
+.ts-val.amber{color:#f59e0b}
+.ts-label{font-family:'JetBrains Mono',monospace;font-size:8px;text-transform:uppercase;letter-spacing:1.5px;color:var(--text2);margin-top:4px}
+.tag{font-family:'JetBrains Mono',monospace;font-size:9px;text-transform:uppercase;letter-spacing:1px;padding:3px 8px;border:1px solid;border-radius:1px;font-weight:600}
@@ -582,7 +604,8 @@ h1 span{color:var(--accent)}
Run History
Nginx Errors
Nginx Access
- Security
+ Security Raw
+ Threat Intel
@@ -679,6 +702,10 @@ async function loadLogs() {
view.textContent = '';
var ld = document.createElement('div'); ld.className = 'empty'; ld.textContent = 'Loading...'; view.appendChild(ld);
try {
+ if (currentSource === 'threats') {
+ await loadThreats();
+ return;
+ }
var r = await fetch('/api/admin/logs?source=' + currentSource + '&limit=' + limit);
var d = await r.json();
if (currentSource === 'runs') {
@@ -693,8 +720,101 @@ async function loadLogs() {
}
}
+async function loadThreats() {
+ var view = document.getElementById('log-view');
+ try {
+ var r = await fetch('/api/admin/security');
+ var d = await r.json();
+ var ips = d.ips || [];
+ view.textContent = '';
+
+ // Summary stats
+ var summary = document.createElement('div');
+ summary.className = 'threat-summary';
+ var critical = ips.filter(function(i){return i.threat==='critical'}).length;
+ var high = ips.filter(function(i){return i.threat==='high'}).length;
+ var banned = d.total_banned || 0;
+ [{v:critical,l:'Critical IPs',c:'red'},{v:high,l:'High Threat',c:'amber'},{v:banned,l:'Currently Banned',c:'green'}].forEach(function(s){
+ var box = document.createElement('div'); box.className = 'ts-box';
+ var val = document.createElement('div'); val.className = 'ts-val ' + s.c; val.textContent = s.v;
+ var lab = document.createElement('div'); lab.className = 'ts-label'; lab.textContent = s.l;
+ box.appendChild(val); box.appendChild(lab); summary.appendChild(box);
+ });
+ view.appendChild(summary);
+
+ if (!ips.length) {
+ var e = document.createElement('div'); e.className = 'empty'; e.textContent = 'No external IP activity recorded';
+ view.appendChild(e); return;
+ }
+
+ ips.forEach(function(ip) {
+ var card = document.createElement('div');
+ card.className = 'threat-card ' + ip.threat + (ip.banned ? ' banned' : '');
+ card.id = 'ip-' + ip.ip.replace(/\./g, '-');
+
+ var ipEl = document.createElement('div'); ipEl.className = 'threat-ip'; ipEl.textContent = ip.ip;
+ card.appendChild(ipEl);
+
+ var info = document.createElement('div'); info.className = 'threat-info';
+ var row = document.createElement('div'); row.className = 'threat-row';
+
+ function addTag(text, cls) { var t = document.createElement('span'); t.className = 'tag ' + cls; t.textContent = text; row.appendChild(t); }
+
+ addTag(ip.threat, ip.threat === 'critical' ? 'tag-err' : ip.threat === 'high' ? 'tag-mode' : 'tag-time');
+ addTag(ip.hits + ' hits', 'tag-time');
+ if (ip.exploit_scans) addTag(ip.exploit_scans + ' scans', 'tag-err');
+ if (ip.login_fails) addTag(ip.login_fails + ' login fails', 'tag-err');
+ if (ip.banned) addTag('BANNED', 'tag-ok');
+ info.appendChild(row);
+
+ if (ip.paths && ip.paths.length) {
+ var paths = document.createElement('div'); paths.className = 'threat-paths';
+ paths.textContent = 'Paths: ' + ip.paths.join(', ');
+ info.appendChild(paths);
+ }
+ var lastSeen = document.createElement('div'); lastSeen.className = 'threat-paths';
+ lastSeen.textContent = 'Last: ' + ip.last_seen;
+ info.appendChild(lastSeen);
+
+ card.appendChild(info);
+
+ var actions = document.createElement('div'); actions.className = 'threat-actions';
+ if (ip.banned) {
+ var ubtn = document.createElement('button'); ubtn.className = 'ban-btn unban'; ubtn.textContent = 'Unban';
+ ubtn.onclick = function() { banAction(ip.ip, 'unban'); };
+ actions.appendChild(ubtn);
+ } else {
+ var bbtn = document.createElement('button'); bbtn.className = 'ban-btn ban'; bbtn.textContent = 'Ban 24h';
+ bbtn.onclick = function() { banAction(ip.ip, 'ban'); };
+ actions.appendChild(bbtn);
+ }
+ card.appendChild(actions);
+ view.appendChild(card);
+ });
+ } catch(e) {
+ view.textContent = '';
+ var err = document.createElement('div'); err.className = 'empty'; err.textContent = 'Error: ' + e.message;
+ view.appendChild(err);
+ }
+}
+
+async function banAction(ip, action) {
+ try {
+ var r = await fetch('/api/admin/security/ban', {
+ method: 'POST', headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({ip: ip, action: action})
+ });
+ var d = await r.json();
+ if (d.ok) {
+ var el = document.getElementById('ip-' + ip.replace(/\./g, '-'));
+ if (el) { el.style.transition = 'opacity 0.3s'; el.style.opacity = '0.3'; }
+ setTimeout(function() { loadThreats(); }, 500);
+ } else { alert('Error: ' + (d.error || 'unknown')); }
+ } catch(e) { alert('Error: ' + e.message); }
+}
+
loadLogs();
-setInterval(function() { if (currentSource !== 'runs') loadLogs(); }, 10000);
+setInterval(function() { if (currentSource !== 'runs' && currentSource !== 'threats') loadLogs(); }, 10000);