diff --git a/templates/index_v2.html b/templates/index_v2.html index 550f99b8..0d9c4b5c 100644 --- a/templates/index_v2.html +++ b/templates/index_v2.html @@ -1125,9 +1125,9 @@

Sports Configuration

Configure which sports leagues to display and their settings.

-
- Loading sports configuration... -
+
+ Loading sports configuration... +
@@ -1407,6 +1407,11 @@ Loading features configuration...
+ +

API Calls (24h window)

+
+
Loading API metrics...
+
@@ -1772,6 +1777,7 @@ initializeEditor(); updateSystemStats(); loadNewsManagerData(); + updateApiMetrics(); // UI controls for grid & scale const scaleRange = document.getElementById('scaleRange'); @@ -1836,6 +1842,7 @@ // Update stats every 30 seconds setInterval(updateSystemStats, 30000); + setInterval(updateApiMetrics, 60000); }); // Socket.IO connection @@ -1882,6 +1889,27 @@ }); } + async function updateApiMetrics(){ + try { + const res = await fetch('/api/metrics'); + const data = await res.json(); + if (data.status !== 'success') return; + const el = document.getElementById('api-metrics'); + const w = Math.round((data.window_seconds || 86400) / 3600); + const f = data.forecast || {}; + const u = data.used || {}; + el.innerHTML = ` +
Window: ${w} hours
+
Weather: ${u.weather || 0} used / ${f.weather || 0} forecast
+
Stocks: ${u.stocks || 0} used / ${f.stocks || 0} forecast
+
Sports: ${u.sports || 0} used / ${f.sports || 0} forecast
+
News: ${u.news || 0} used / ${f.news || 0} forecast
+ `; + } catch (e) { + // ignore + } + } + // Fallback polling when websocket is disconnected let __previewPollTimer = null; function startPreviewPolling(){ @@ -2986,6 +3014,11 @@ const html = leagues.map(l => { const sec = cfg[l.key] || {}; const fav = (sec.favorite_teams || []).join(', '); + const recentToShow = sec.recent_games_to_show ?? 1; + const upcomingToShow = sec.upcoming_games_to_show ?? 1; + const liveUpd = sec.live_update_interval ?? 30; + const recentUpd = sec.recent_update_interval ?? 3600; + const upcomingUpd = sec.upcoming_update_interval ?? 3600; return `
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
`; }).join(''); @@ -3032,12 +3089,22 @@ const favoritesOnly = document.querySelector(`.sp-favorites-only[data-league="${key}"]`)?.checked || false; const favs = document.querySelector(`.sp-favorites[data-league="${key}"]`)?.value || ''; const favorite_teams = favs.split(',').map(s => s.trim()).filter(Boolean); + const liveUpd = parseInt(document.querySelector(`.sp-live-update[data-league="${key}"]`)?.value || '30'); + const recentUpd = parseInt(document.querySelector(`.sp-recent-update[data-league="${key}"]`)?.value || '3600'); + const upcomingUpd = parseInt(document.querySelector(`.sp-upcoming-update[data-league="${key}"]`)?.value || '3600'); + const recentCount = parseInt(document.querySelector(`.sp-recent-count[data-league="${key}"]`)?.value || '1'); + const upcomingCount = parseInt(document.querySelector(`.sp-upcoming-count[data-league="${key}"]`)?.value || '1'); fragment[key] = { enabled, live_priority: livePriority, show_odds: showOdds, show_favorite_teams_only: favoritesOnly, - favorite_teams + favorite_teams, + live_update_interval: liveUpd, + recent_update_interval: recentUpd, + upcoming_update_interval: upcomingUpd, + recent_games_to_show: recentCount, + upcoming_games_to_show: upcomingCount }; }); await saveConfigJson(fragment); diff --git a/web_interface_v2.py b/web_interface_v2.py index 2489c28a..00d3b9db 100644 --- a/web_interface_v2.py +++ b/web_interface_v2.py @@ -469,6 +469,82 @@ def get_system_status_api(): """Get system status as JSON.""" return jsonify(get_system_status()) +# --- API Call Metrics (simple in-memory counters) --- +api_counters = { + 'weather': {'used': 0}, + 'stocks': {'used': 0}, + 'sports': {'used': 0}, + 'news': {'used': 0}, +} +api_window_start = time.time() +api_window_seconds = 24 * 3600 + +def increment_api_counter(kind: str, count: int = 1): + global api_window_start + now = time.time() + if now - api_window_start > api_window_seconds: + # Reset window + api_window_start = now + for v in api_counters.values(): + v['used'] = 0 + if kind in api_counters: + api_counters[kind]['used'] = api_counters[kind].get('used', 0) + count + +@app.route('/api/metrics') +def get_metrics(): + """Expose lightweight API usage counters and simple forecasts based on config.""" + try: + config = config_manager.load_config() + forecast = {} + # Weather forecasted calls per 24h + try: + w_int = int(config.get('weather', {}).get('update_interval', 1800)) + forecast['weather'] = max(1, int(api_window_seconds / max(1, w_int))) + except Exception: + forecast['weather'] = 0 + # Stocks + try: + s_int = int(config.get('stocks', {}).get('update_interval', 600)) + forecast['stocks'] = max(1, int(api_window_seconds / max(1, s_int))) + except Exception: + forecast['stocks'] = 0 + # Sports (aggregate of enabled leagues using their recent update intervals) + sports_leagues = [ + ('nhl_scoreboard','recent_update_interval'), + ('nba_scoreboard','recent_update_interval'), + ('mlb','recent_update_interval'), + ('milb','recent_update_interval'), + ('soccer_scoreboard','recent_update_interval'), + ('nfl_scoreboard','recent_update_interval'), + ('ncaa_fb_scoreboard','recent_update_interval'), + ('ncaa_baseball_scoreboard','recent_update_interval'), + ('ncaam_basketball_scoreboard','recent_update_interval'), + ] + sports_calls = 0 + for key, interval_key in sports_leagues: + sec = config.get(key, {}) + if sec.get('enabled', False): + ival = int(sec.get(interval_key, 3600)) + sports_calls += max(1, int(api_window_seconds / max(1, ival))) + forecast['sports'] = sports_calls + + # News manager + try: + n_int = int(config.get('news_manager', {}).get('update_interval', 300)) + forecast['news'] = max(1, int(api_window_seconds / max(1, n_int))) + except Exception: + forecast['news'] = 0 + + return jsonify({ + 'status': 'success', + 'window_seconds': api_window_seconds, + 'since': api_window_start, + 'forecast': forecast, + 'used': {k: v.get('used', 0) for k, v in api_counters.items()} + }) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 500 + # Add all the routes from the original web interface for compatibility @app.route('/save_schedule', methods=['POST']) def save_schedule_route():