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():