From 7efb5da893defc81e0e85cb3f1ffa2386fceaa61 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Sat, 6 Jun 2026 15:43:48 -0700 Subject: [PATCH] Rate monitor: daily-budget ring around the Spotify avatar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The payload has carried daily_budget {used, limit, exhausted} forever and the dashboard rendered none of it. The avatar disc now wears a conic progress rim that fills as the day's real-API budget is spent — green to 70%, amber to 95%, red after — and flips purple once the worker has bridged to Spotify Free for the rest of the day (using_free now included in the emit payload). Tooltip carries the exact used/limit numbers. --- web_server.py | 3 +++ webui/static/api-monitor.js | 22 ++++++++++++++++++++++ webui/static/style.css | 24 ++++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/web_server.py b/web_server.py index b40c2509..ebb88424 100644 --- a/web_server.py +++ b/web_server.py @@ -35033,6 +35033,9 @@ def _emit_rate_monitor_loop(): } if svc_key == 'spotify' and enr.get('daily_budget'): entry['worker']['daily_budget'] = enr['daily_budget'] + # Budget ring styling: purple when the worker has + # bridged to Spotify Free after spending the budget. + entry['worker']['using_free'] = bool(enr.get('using_free')) except Exception as e: logger.debug("enrichment worker status build failed: %s", e) diff --git a/webui/static/api-monitor.js b/webui/static/api-monitor.js index 3ff57de0..1433bb93 100644 --- a/webui/static/api-monitor.js +++ b/webui/static/api-monitor.js @@ -383,6 +383,28 @@ function _renderEqualizerBars(grid, data) { } bar.classList.toggle('cooldown', cooling); + // Daily-budget ring (Spotify is the only service with a real daily + // cap): a conic rim inside the avatar disc fills as the budget is + // spent — green → amber → red, purple once the worker has bridged + // to Spotify Free for the rest of the day. + const budget = worker.daily_budget; + if (budget && budget.limit > 0) { + const bPct = Math.min(1, (budget.used || 0) / budget.limit); + const bridged = !!worker.using_free && !!budget.exhausted; + bar.classList.add('has-budget'); + bar.style.setProperty('--eq-budget', String(Math.max(bPct, 0.02))); + bar.style.setProperty('--eq-budget-color', + bridged ? '#a78bfa' : bPct < 0.7 ? '#4ade80' : bPct < 0.95 ? '#fbbf24' : '#ef4444'); + const avatar = bar.querySelector('.rate-eq-avatar'); + if (avatar) { + avatar.title = bridged + ? `Daily budget spent — running on Spotify Free (${budget.used}/${budget.limit})` + : `Daily API budget: ${budget.used}/${budget.limit}`; + } + } else { + bar.classList.remove('has-budget'); + } + _eqDisplay[svc] = { value, pct, peak, peakAt, rlTotal, cooling }; } } diff --git a/webui/static/style.css b/webui/static/style.css index 213ed7c8..a112b21f 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -63083,6 +63083,30 @@ body.reduce-effects .dash-card::after { text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); } +/* Daily-budget ring — a conic progress rim hugging the avatar disc's inner + * edge, filling as the day's API budget is spent. Color comes from JS + * (green → amber → red, purple when bridged to Spotify Free). Drawn inside + * the disc because the avatar clips overflow. */ +.rate-eq-avatar::after { + content: ''; + position: absolute; + inset: 0; + border-radius: 50%; + background: conic-gradient( + var(--eq-budget-color, #4ade80) calc(var(--eq-budget, 0) * 360deg), + transparent 0); + -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 3.5px), #000 calc(100% - 2.5px)); + mask: radial-gradient(farthest-side, transparent calc(100% - 3.5px), #000 calc(100% - 2.5px)); + opacity: 0; + transition: opacity 0.4s; + pointer-events: none; + z-index: 2; +} + +.rate-eq.has-budget .rate-eq-avatar::after { + opacity: 0.95; +} + .rate-eq.active .rate-eq-avatar { animation: rate-eq-avatar-halo 2.8s ease-in-out infinite; }