From 7948089f040cba983a77feb6c50e0b136e2e60b5 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Mar 2026 04:07:15 -0500 Subject: [PATCH] Fix sentinel countdown: sync to actual scan schedule, not page load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sentinel thread sets next_scan_ts = time.time() + interval BEFORE sleeping - API returns next_scan_in derived from real next_scan_ts, not estimated - Frontend calculates server clock offset and counts down to the actual target timestamp — refresh shows the same remaining time, not a reset - Shows ✓ in green when scan fires, resumes countdown on next poll Co-Authored-By: Claude Opus 4.6 (1M context) --- llm_team_ui.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/llm_team_ui.py b/llm_team_ui.py index 117bf93..82f7f20 100644 --- a/llm_team_ui.py +++ b/llm_team_ui.py @@ -775,13 +775,21 @@ async function loadThreats() { sentinelCard.appendChild(sHeader); - // Start countdown + // Countdown synced to server's next_scan_ts + // Store the absolute target time so refresh doesn't reset if (window._sentinelTimer) clearInterval(window._sentinelTimer); - window._sentinelCountdown = nextIn; + var serverNow = sentinel.server_time || (Date.now()/1000); + var nextScanTs = serverNow + nextIn; + window._sentinelTargetTs = nextScanTs; + window._sentinelServerOffset = serverNow - (Date.now()/1000); // clock difference window._sentinelTimer = setInterval(function(){ - window._sentinelCountdown = Math.max(0, window._sentinelCountdown - 1); + var localNow = (Date.now()/1000) + (window._sentinelServerOffset||0); + var remaining = Math.max(0, (window._sentinelTargetTs||0) - localNow); var el = document.getElementById('sentinel-countdown'); - if (el) { el.textContent = Math.ceil(window._sentinelCountdown) || '...'; if (window._sentinelCountdown <= 0) { el.textContent = '✓'; el.style.color = '#4ade80'; clearInterval(window._sentinelTimer); } } + if (el) { + if (remaining > 0) { el.textContent = Math.ceil(remaining); el.style.color = '#d946ef'; } + else { el.textContent = '✓'; el.style.color = '#4ade80'; } + } }, 1000); if (ss.last_error) { @@ -6390,7 +6398,7 @@ SENTINEL_MODEL = "qwen2.5:latest" SENTINEL_INTERVAL = 300 # 5 minutes _sentinel_last_pos = 0 _sentinel_results = [] # last 50 analyses -_sentinel_stats = {"scans": 0, "bans": 0, "last_run": None, "last_error": None} +_sentinel_stats = {"scans": 0, "bans": 0, "last_run": None, "last_error": None, "next_scan_ts": 0} def _sentinel_log_entry(msg): """Write to sentinel log file.""" @@ -6573,6 +6581,7 @@ def _sentinel_loop(): _sentinel_log_entry("SENTINEL_START model=" + SENTINEL_MODEL + " interval=" + str(SENTINEL_INTERVAL) + "s") while True: + _sentinel_stats["next_scan_ts"] = time.time() + SENTINEL_INTERVAL time.sleep(SENTINEL_INTERVAL) try: _sentinel_scan() @@ -6585,18 +6594,16 @@ 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) + next_ts = _sentinel_stats.get("next_scan_ts", 0) + next_in = max(0, next_ts - now) return jsonify({ "stats": _sentinel_stats, "recent_verdicts": list(reversed(_sentinel_results[-20:])), "model": SENTINEL_MODEL, "interval": SENTINEL_INTERVAL, - "elapsed_since_scan": round(elapsed, 1), "next_scan_in": round(next_in, 1), - "server_time": now + "server_time": round(now, 1) })