From 09af31154e86223854bc8f15949c67cac6d143dc Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Sat, 6 Jun 2026 15:42:05 -0700 Subject: [PATCH] Rate monitor: rate-limit bans drain down as a visible countdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A banned service used to just tint red. The payload carries the seconds remaining (rl_remaining), so the bar now locks into a cooldown state: the live VU dims, a red column drains away as the ban ticks down (largest remaining seen is latched as the denominator — only remaining is sent), and an m:ss timer counts to recovery. The moment the ban expires the track flashes green ('recovered') and the VU takes the stage back. 'Back in 4:00', not 'something's red'. --- webui/static/api-monitor.js | 35 +++++++++++++++++- webui/static/style.css | 71 +++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/webui/static/api-monitor.js b/webui/static/api-monitor.js index fa653f5a..3ff57de0 100644 --- a/webui/static/api-monitor.js +++ b/webui/static/api-monitor.js @@ -237,6 +237,10 @@ function _renderEqualizerBars(grid, data) {
+
+
+
+
0
@@ -350,7 +354,36 @@ function _renderEqualizerBars(grid, data) { bar.classList.toggle('active', value > 0 || wStatus === 'running'); bar.classList.toggle('rate-limited', isRateLimited); - _eqDisplay[svc] = { value, pct, peak, peakAt }; + // Cooldown drain — a ban isn't just "red", it's a COUNTDOWN. The + // payload only carries seconds remaining, so latch the largest value + // seen this ban as the denominator; the red column then drains away + // as the ban ticks down and the timer shows m:ss until recovery. + const rlRemaining = isRateLimited ? Math.max(0, Math.round(d.rl_remaining || 0)) : 0; + let rlTotal = prev.rlTotal || 0; + const cooling = rlRemaining > 0; + if (cooling) { + rlTotal = Math.max(rlTotal, rlRemaining); + const cdFill = bar.querySelector('.rate-eq-cooldown-fill'); + if (cdFill) cdFill.style.height = `${(rlRemaining / rlTotal) * 100}%`; + const cdTime = bar.querySelector('.rate-eq-cooldown-time'); + if (cdTime) { + const m = Math.floor(rlRemaining / 60); + const s = String(rlRemaining % 60).padStart(2, '0'); + cdTime.textContent = `${m}:${s}`; + } + } else { + rlTotal = 0; + // Recovery moment: the ban just ended — flash the bar back alive. + if (prev.cooling) { + bar.classList.remove('recovered'); + void bar.offsetWidth; // restart the animation + bar.classList.add('recovered'); + window.setTimeout(() => bar.classList.remove('recovered'), 1300); + } + } + bar.classList.toggle('cooldown', cooling); + + _eqDisplay[svc] = { value, pct, peak, peakAt, rlTotal, cooling }; } } diff --git a/webui/static/style.css b/webui/static/style.css index a67d3318..213ed7c8 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -63265,6 +63265,77 @@ body.reduce-effects .dash-card::after { opacity: 0.75; } +/* Cooldown drain — while a service is rate-limit banned, the track shows a + * red column that DRAINS as the ban ticks down plus an m:ss countdown. + * "Back in 4:00", not just "something's red". JS drives the fill height + * (remaining/total) and timer text; .cooldown gates visibility. */ +.rate-eq-cooldown { + position: absolute; + inset: 0; + opacity: 0; + pointer-events: none; + transition: opacity 0.4s; + z-index: 3; +} + +.rate-eq.cooldown .rate-eq-cooldown { + opacity: 1; +} + +.rate-eq.cooldown .rate-eq-fill, +.rate-eq.cooldown .rate-eq-peak { + opacity: 0.18; /* the live VU yields the stage to the countdown */ +} + +.rate-eq-cooldown-fill { + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 100%; + background: linear-gradient(180deg, + rgba(239, 68, 68, 0.55) 0%, + rgba(239, 68, 68, 0.28) 60%, + rgba(239, 68, 68, 0.12) 100%); + border-radius: 0 0 11px 11px; + box-shadow: inset 0 1px 0 rgba(255, 120, 120, 0.5); + transition: height 0.9s linear; +} + +.rate-eq-cooldown-time { + position: absolute; + left: 0; + right: 0; + top: 50%; + transform: translateY(-50%); + text-align: center; + font-size: 11px; + font-weight: 700; + font-variant-numeric: tabular-nums; + color: rgba(255, 200, 200, 0.95); + text-shadow: 0 0 8px rgba(239, 68, 68, 0.8); +} + +/* Recovery flash — the moment a ban expires the track blinks back alive */ +.rate-eq.recovered .rate-eq-track { + animation: rate-eq-recovered 1.2s ease-out; +} + +@keyframes rate-eq-recovered { + 0% { + border-color: rgba(74, 222, 128, 0.9); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.04), + 0 0 26px rgba(74, 222, 128, 0.55); + } + 100% { + border-color: rgba(255, 255, 255, 0.06); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.04), + inset 0 -10px 24px rgba(0, 0, 0, 0.35); + } +} + /* Vertical shimmer scan — runs continuously when active */ .rate-eq-shimmer { position: absolute;