Sentinel countdown ring timer with live stats
- SVG progress ring shows time until next scan (magenta arc) - Countdown ticks every second: "245s → 244s → ... → scanning..." - Ring fills as time progresses, resets on scan - Turns green and shows "scanning..." when timer hits 0 - Stats grid: Scans count, AI Bans count, Last Run time, Interval - Backend API returns elapsed_since_scan and next_scan_in Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
418da99fa7
commit
3cdfc01835
@ -749,14 +749,74 @@ async function loadThreats() {
|
||||
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);
|
||||
var sStats = document.createElement('div');
|
||||
sStats.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:10px;color:#7a7872;display:flex;gap:16px;margin-bottom:8px';
|
||||
|
||||
// Countdown + metrics row
|
||||
var ss = sentinel.stats || {};
|
||||
sStats.textContent = 'Scans: ' + (ss.scans||0) + ' | AI Bans: ' + (ss.bans||0) + ' | Last: ' + (ss.last_run||'not yet') + ' | Interval: ' + (sentinel.interval||300) + 's';
|
||||
sentinelCard.appendChild(sStats);
|
||||
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);
|
||||
|
||||
// Start countdown timer
|
||||
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'; }
|
||||
}
|
||||
}, 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';
|
||||
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;
|
||||
sentinelCard.appendChild(sErr);
|
||||
}
|
||||
@ -6369,6 +6429,7 @@ def _sentinel_scan():
|
||||
import subprocess, collections
|
||||
|
||||
_sentinel_stats["last_run"] = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
_sentinel_stats["last_run_ts"] = time.time()
|
||||
_sentinel_stats["scans"] += 1
|
||||
|
||||
# Read new lines since last scan
|
||||
@ -6547,11 +6608,18 @@ def _sentinel_loop():
|
||||
@app.route("/api/admin/sentinel")
|
||||
@admin_required
|
||||
def admin_sentinel_status():
|
||||
last_ts = _sentinel_stats.get("last_run_ts", 0)
|
||||
now = time.time()
|
||||
elapsed = now - last_ts if last_ts else 0
|
||||
next_in = max(0, SENTINEL_INTERVAL - elapsed)
|
||||
return jsonify({
|
||||
"stats": _sentinel_stats,
|
||||
"recent_verdicts": list(reversed(_sentinel_results[-20:])),
|
||||
"model": SENTINEL_MODEL,
|
||||
"interval": SENTINEL_INTERVAL
|
||||
"interval": SENTINEL_INTERVAL,
|
||||
"elapsed_since_scan": round(elapsed, 1),
|
||||
"next_scan_in": round(next_in, 1),
|
||||
"server_time": now
|
||||
})
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user