Compact sentinel card: single-line with mini ring + collapsible verdicts

- Entire sentinel status fits in one header row now
- Mini 28px countdown ring (was 64px) inline with title
- Scans/bans counts inline as text, not grid boxes
- Verdicts collapsed by default — click to expand
- Card padding reduced (8px vs 14px)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
root 2026-03-26 04:03:57 -05:00
parent 3cdfc01835
commit 357918013d

View File

@ -740,7 +740,7 @@ async function loadThreats() {
// Sentinel status card
var sentinelCard = document.createElement('div');
sentinelCard.style.cssText = 'background:rgba(0,0,0,0.3);border:2px solid rgba(217,70,239,0.3);border-radius:2px;padding:14px;margin-bottom:16px;backdrop-filter:blur(16px)';
sentinelCard.style.cssText = 'background:rgba(0,0,0,0.3);border:2px solid rgba(217,70,239,0.3);border-radius:2px;padding:8px 12px;margin-bottom:12px;backdrop-filter:blur(16px)';
var sHeader = document.createElement('div');
sHeader.style.cssText = 'display:flex;align-items:center;gap:8px;margin-bottom:8px';
var sDot = document.createElement('div');
@ -748,85 +748,61 @@ async function loadThreats() {
var sTitle = document.createElement('span');
sTitle.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:11px;text-transform:uppercase;letter-spacing:1.5px;color:#d946ef;font-weight:700';
sTitle.textContent = 'AI Sentinel — ' + (sentinel.model || '?');
sHeader.appendChild(sDot);sHeader.appendChild(sTitle);sentinelCard.appendChild(sHeader);
sHeader.appendChild(sDot);sHeader.appendChild(sTitle);
// Countdown + metrics row
// Inline stats + countdown all in one row
var ss = sentinel.stats || {};
var nextIn = sentinel.next_scan_in || 0;
var interval = sentinel.interval || 300;
var metricsRow = document.createElement('div');
metricsRow.style.cssText = 'display:grid;grid-template-columns:auto 1fr;gap:14px;align-items:center;margin-bottom:10px';
// Countdown ring
var ringWrap = document.createElement('div');
ringWrap.style.cssText = 'position:relative;width:64px;height:64px;flex-shrink:0';
var pct = interval > 0 ? ((interval - nextIn) / interval) : 0;
var deg = Math.round(pct * 360);
ringWrap.innerHTML = '<svg width="64" height="64" viewBox="0 0 64 64">'
+ '<circle cx="32" cy="32" r="28" fill="none" stroke="#2a2d35" stroke-width="4"/>'
+ '<circle cx="32" cy="32" r="28" fill="none" stroke="#d946ef" stroke-width="4" stroke-linecap="round"'
+ ' stroke-dasharray="' + (2 * Math.PI * 28).toFixed(1) + '"'
+ ' stroke-dashoffset="' + ((1 - pct) * 2 * Math.PI * 28).toFixed(1) + '"'
+ ' transform="rotate(-90 32 32)" style="transition:stroke-dashoffset 1s"/>'
+ '</svg>';
var countText = document.createElement('div');
countText.id = 'sentinel-countdown';
countText.style.cssText = 'position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;font-family:JetBrains Mono,monospace';
var countNum = document.createElement('div');
countNum.style.cssText = 'font-size:16px;font-weight:700;color:#d946ef;line-height:1';
countNum.textContent = Math.ceil(nextIn) + 's';
var countLabel = document.createElement('div');
countLabel.style.cssText = 'font-size:7px;color:#7a7872;text-transform:uppercase;letter-spacing:1px;margin-top:2px';
countLabel.textContent = 'next scan';
countText.appendChild(countNum); countText.appendChild(countLabel);
ringWrap.appendChild(countText);
metricsRow.appendChild(ringWrap);
// Stats grid
var statsGrid = document.createElement('div');
statsGrid.style.cssText = 'display:grid;grid-template-columns:repeat(4,1fr);gap:6px';
[{v:ss.scans||0,l:'Scans',c:'#d946ef'},{v:ss.bans||0,l:'AI Bans',c:'#e05252'},{v:ss.last_run||'',l:'Last Run',c:'#e8e6e3',small:true},{v:(sentinel.interval||300)+'s',l:'Interval',c:'#7a7872'}].forEach(function(m){
var box = document.createElement('div');
box.style.cssText = 'text-align:center';
var val = document.createElement('div');
val.style.cssText = 'font-family:JetBrains Mono,monospace;font-weight:700;color:'+m.c+';font-size:'+(m.small?'10px':'14px');
val.textContent = m.v;
var lab = document.createElement('div');
lab.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:7px;text-transform:uppercase;letter-spacing:1px;color:#7a7872;margin-top:2px';
lab.textContent = m.l;
box.appendChild(val);box.appendChild(lab);statsGrid.appendChild(box);
});
metricsRow.appendChild(statsGrid);
sentinelCard.appendChild(metricsRow);
// Mini ring
var ring = document.createElement('span');
ring.style.cssText = 'position:relative;width:28px;height:28px;flex-shrink:0;display:inline-block;vertical-align:middle;margin-left:auto';
ring.innerHTML = '<svg width="28" height="28" viewBox="0 0 28 28"><circle cx="14" cy="14" r="11" fill="none" stroke="#2a2d35" stroke-width="2.5"/><circle cx="14" cy="14" r="11" fill="none" stroke="#d946ef" stroke-width="2.5" stroke-linecap="round" stroke-dasharray="'+(2*Math.PI*11).toFixed(1)+'" stroke-dashoffset="'+((1-pct)*2*Math.PI*11).toFixed(1)+'" transform="rotate(-90 14 14)" style="transition:stroke-dashoffset 1s"/></svg>';
var countTxt = document.createElement('span');
countTxt.id = 'sentinel-countdown';
countTxt.style.cssText = 'position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-family:JetBrains Mono,monospace;font-size:8px;font-weight:700;color:#d946ef';
countTxt.textContent = Math.ceil(nextIn) + '';
ring.appendChild(countTxt);
sHeader.appendChild(ring);
// Start countdown timer
// Compact stats inline
var inlineStats = document.createElement('span');
inlineStats.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:9px;color:#7a7872;display:flex;gap:10px;margin-left:8px';
inlineStats.innerHTML = '<span><b style="color:#d946ef">'+(ss.scans||0)+'</b> scans</span><span><b style="color:#e05252">'+(ss.bans||0)+'</b> bans</span>';
sHeader.appendChild(inlineStats);
sentinelCard.appendChild(sHeader);
// Start countdown
if (window._sentinelTimer) clearInterval(window._sentinelTimer);
window._sentinelCountdown = nextIn;
window._sentinelTimer = setInterval(function(){
window._sentinelCountdown = Math.max(0, window._sentinelCountdown - 1);
var el = document.getElementById('sentinel-countdown');
if (el) el.querySelector('div').textContent = Math.ceil(window._sentinelCountdown) + 's';
if (window._sentinelCountdown <= 0) {
clearInterval(window._sentinelTimer);
var el2 = document.getElementById('sentinel-countdown');
if (el2) { el2.querySelector('div').textContent = 'scanning...'; el2.querySelector('div').style.color = '#4ade80'; }
}
if (el) { el.textContent = Math.ceil(window._sentinelCountdown) || '...'; if (window._sentinelCountdown <= 0) { el.textContent = ''; el.style.color = '#4ade80'; clearInterval(window._sentinelTimer); } }
}, 1000);
if (ss.last_error) {
var sErr = document.createElement('div');
sErr.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:9px;color:#e05252;border-left:2px solid #e05252;padding-left:8px;margin-bottom:8px';
sErr.textContent = 'Last error: ' + ss.last_error;
sErr.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:9px;color:#e05252;border-left:2px solid #e05252;padding-left:8px;margin-bottom:4px';
sErr.textContent = 'Error: ' + ss.last_error;
sentinelCard.appendChild(sErr);
}
// Recent AI verdicts
// Recent AI verdicts collapsible
var verdicts = sentinel.recent_verdicts || [];
if (verdicts.length) {
var vTitle = document.createElement('div');
vTitle.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:8px;text-transform:uppercase;letter-spacing:2px;color:#d946ef;margin:10px 0 6px;opacity:0.6';
vTitle.textContent = 'Recent AI Verdicts';
sentinelCard.appendChild(vTitle);
var vToggle = document.createElement('div');
vToggle.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:8px;text-transform:uppercase;letter-spacing:1.5px;color:#d946ef;margin:4px 0 0;opacity:0.5;cursor:pointer';
vToggle.textContent = '' + verdicts.length + ' recent verdicts';
var vList = document.createElement('div');
vList.style.display = 'none';
vToggle.onclick = function(){
if (vList.style.display === 'none') { vList.style.display = 'block'; vToggle.textContent = '' + verdicts.length + ' recent verdicts'; vToggle.style.opacity = '1'; }
else { vList.style.display = 'none'; vToggle.textContent = '' + verdicts.length + ' recent verdicts'; vToggle.style.opacity = '0.5'; }
};
sentinelCard.appendChild(vToggle);
verdicts.slice(0,8).forEach(function(v){
var vLine = document.createElement('div');
vLine.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:10px;color:#7a7872;padding:3px 0;border-bottom:1px solid rgba(42,45,53,0.3);display:flex;gap:8px';
@ -835,8 +811,9 @@ async function loadThreats() {
+ '<span style="min-width:120px">'+esc(v.ip||'?')+'</span>'
+ '<span style="color:#c084fc">'+esc(v.attack_type||'?')+'</span>'
+ '<span style="flex:1;opacity:0.6">'+esc(v.reason||'')+'</span>';
sentinelCard.appendChild(vLine);
vList.appendChild(vLine);
});
sentinelCard.appendChild(vList);
}
view.appendChild(sentinelCard);