milb upcoming game debug logging

This commit is contained in:
Chuck
2025-08-10 17:38:56 -05:00
parent ede82406fa
commit 10c1342bdb
10 changed files with 196 additions and 35 deletions

View File

@@ -1033,6 +1033,31 @@ The LEDMatrix system includes a comprehensive scoreboard display system with thr
- Automatic game switching - Automatic game switching
- Built-in caching to reduce API calls - Built-in caching to reduce API calls
- Test mode for development - Test mode for development
## API Usage Tracking
The LEDMatrix system includes a built-in API usage counter that tracks API calls made by various managers in a 24-hour rolling window. This feature helps monitor API usage and ensure compliance with rate limits.
### API Counter Features
- **Real-time Tracking**: Counts API calls for weather, stocks, sports, and news data
- **24-hour Window**: Rolling window that resets every 24 hours
- **Web Interface Integration**: View current usage in the Overview tab of the web interface
- **Forecast Display**: Shows predicted API usage based on current configuration
- **Automatic Reset**: Counters automatically reset when the 24-hour window expires
### Tracked API Calls
- **Weather**: OpenWeatherMap API calls (geocoding + weather data)
- **Stocks**: Yahoo Finance API calls for stock and crypto data
- **Sports**: ESPN API calls for various sports leagues (NHL, NBA, MLB, NFL, etc.)
- **News**: RSS feed and news API calls
### Accessing API Metrics
1. Open the web interface in your browser
2. Navigate to the **Overview** tab
3. Scroll down to the "API Calls (24h window)" section
4. Click "Refresh API Metrics" to update the display
The counter shows both actual usage and forecasted usage based on your current configuration settings.
## Caching System ## Caching System
The LEDMatrix system includes a robust caching mechanism to optimize API calls and reduce network traffic: The LEDMatrix system includes a robust caching mechanism to optimize API calls and reduce network traffic:

View File

@@ -12,6 +12,13 @@ from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry from urllib3.util.retry import Retry
import pytz import pytz
# Import API counter function
try:
from web_interface_v2 import increment_api_counter
except ImportError:
def increment_api_counter(kind: str, count: int = 1):
pass
# Get logger # Get logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -248,6 +255,8 @@ class BaseMiLBManager:
draw = ImageDraw.Draw(image) draw = ImageDraw.Draw(image)
# For upcoming games, show date and time stacked in the center # For upcoming games, show date and time stacked in the center
self.logger.debug(f"[MiLB] Game status: {game_data.get('status')}, status_state: {game_data.get('status_state')}")
self.logger.debug(f"[MiLB] Full game data: {game_data}")
if game_data['status'] == 'status_scheduled': if game_data['status'] == 'status_scheduled':
# Ensure game_time_str is defined before use # Ensure game_time_str is defined before use
game_time_str = game_data.get('start_time', '') game_time_str = game_data.get('start_time', '')
@@ -261,49 +270,73 @@ class BaseMiLBManager:
# Draw on the current image # Draw on the current image
self.display_manager.draw = draw self.display_manager.draw = draw
self.display_manager._draw_bdf_text(status_text, status_x, status_y, color=(255, 255, 255), font=self.display_manager.calendar_font) self.display_manager._draw_bdf_text(status_text, status_x, status_y, color=(255, 255, 255), font=self.display_manager.calendar_font)
# Update the display
self.display_manager.update_display()
if not game_time_str or 'TBD' in game_time_str: if not game_time_str or 'TBD' in game_time_str:
game_date_str = "TBD" game_date_str = "TBD"
game_time_formatted_str = "" game_time_formatted_str = ""
self.logger.debug(f"[MiLB] Game time is TBD or empty: {game_time_str}")
else: else:
game_time = datetime.fromisoformat(game_time_str.replace('Z', '+00:00')) self.logger.debug(f"[MiLB] Processing game time: {game_time_str}")
timezone_str = self.config.get('timezone', 'UTC')
try: try:
tz = pytz.timezone(timezone_str) game_time = datetime.fromisoformat(game_time_str.replace('Z', '+00:00'))
except pytz.exceptions.UnknownTimeZoneError: timezone_str = self.config.get('timezone', 'UTC')
logger.warning(f"Unknown timezone: {timezone_str}, falling back to UTC") try:
tz = pytz.UTC tz = pytz.timezone(timezone_str)
if game_time.tzinfo is None: except pytz.exceptions.UnknownTimeZoneError:
game_time = game_time.replace(tzinfo=pytz.UTC) logger.warning(f"Unknown timezone: {timezone_str}, falling back to UTC")
local_time = game_time.astimezone(tz) tz = pytz.UTC
if game_time.tzinfo is None:
game_time = game_time.replace(tzinfo=pytz.UTC)
local_time = game_time.astimezone(tz)
# Check date format from config self.logger.debug(f"[MiLB] Local time: {local_time}")
use_short_date_format = self.config.get('display', {}).get('use_short_date_format', False)
if use_short_date_format:
game_date_str = local_time.strftime("%-m/%-d")
else:
game_date_str = self.display_manager.format_date_with_ordinal(local_time)
game_time_formatted_str = self._format_game_time(game_data['start_time']) # Check date format from config
use_short_date_format = self.config.get('display', {}).get('use_short_date_format', False)
if use_short_date_format:
game_date_str = local_time.strftime("%-m/%-d")
else:
game_date_str = self.display_manager.format_date_with_ordinal(local_time)
game_time_formatted_str = self._format_game_time(game_data['start_time'])
self.logger.debug(f"[MiLB] Formatted date: {game_date_str}, time: {game_time_formatted_str}")
except Exception as e:
self.logger.error(f"[MiLB] Error processing game time: {e}")
game_date_str = "TBD"
game_time_formatted_str = "TBD"
# Draw date and time using NHL-style fonts # Draw date and time using NHL-style fonts
date_font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8) try:
time_font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8) date_font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8)
time_font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8)
self.logger.debug(f"[MiLB] Fonts loaded successfully")
except Exception as e:
self.logger.error(f"[MiLB] Failed to load fonts: {e}")
# Fallback to default font
date_font = ImageFont.load_default()
time_font = ImageFont.load_default()
# Draw date in center # Draw date in center
date_width = draw.textlength(game_date_str, font=date_font) date_width = draw.textlength(game_date_str, font=date_font)
date_x = (width - date_width) // 2 date_x = (width - date_width) // 2
date_y = (height - date_font.size) // 2 - 3 date_y = (height - date_font.size) // 2 - 3
self.logger.debug(f"[MiLB] Drawing date '{game_date_str}' at ({date_x}, {date_y})")
self._draw_text_with_outline(draw, game_date_str, (date_x, date_y), date_font) self._draw_text_with_outline(draw, game_date_str, (date_x, date_y), date_font)
# Draw a simple test rectangle to verify drawing is working
draw.rectangle([date_x-2, date_y-2, date_x+date_width+2, date_y+date_font.size+2], outline=(255, 0, 0))
# Draw time below date # Draw time below date
time_width = draw.textlength(game_time_formatted_str, font=time_font) time_width = draw.textlength(game_time_formatted_str, font=time_font)
time_x = (width - time_width) // 2 time_x = (width - time_width) // 2
time_y = date_y + 10 time_y = date_y + 10
self.logger.debug(f"[MiLB] Drawing time '{game_time_formatted_str}' at ({time_x}, {time_y})")
self._draw_text_with_outline(draw, game_time_formatted_str, (time_x, time_y), time_font) self._draw_text_with_outline(draw, game_time_formatted_str, (time_x, time_y), time_font)
# Draw a simple test rectangle to verify drawing is working
draw.rectangle([time_x-2, time_y-2, time_x+time_width+2, time_y+time_font.size+2], outline=(0, 255, 0))
# For recent/final games, show scores and status # For recent/final games, show scores and status
elif game_data['status'] in ['status_final', 'final', 'completed']: elif game_data['status'] in ['status_final', 'final', 'completed']:
# Show "Final" at the top using NHL-style font # Show "Final" at the top using NHL-style font
@@ -316,8 +349,6 @@ class BaseMiLBManager:
# Draw on the current image # Draw on the current image
self.display_manager.draw = draw self.display_manager.draw = draw
self.display_manager._draw_bdf_text(status_text, status_x, status_y, color=(255, 255, 255), font=self.display_manager.calendar_font) self.display_manager._draw_bdf_text(status_text, status_x, status_y, color=(255, 255, 255), font=self.display_manager.calendar_font)
# Update the display
self.display_manager.update_display()
# Draw scores at the bottom using NHL-style font # Draw scores at the bottom using NHL-style font
away_score = str(game_data['away_score']) away_score = str(game_data['away_score'])
@@ -362,6 +393,7 @@ class BaseMiLBManager:
def _format_game_time(self, game_time: str) -> str: def _format_game_time(self, game_time: str) -> str:
"""Format game time for display.""" """Format game time for display."""
try: try:
self.logger.debug(f"[MiLB] Formatting game time: {game_time}")
# Get timezone from config # Get timezone from config
timezone_str = self.config.get('timezone', 'UTC') timezone_str = self.config.get('timezone', 'UTC')
try: try:
@@ -376,7 +408,9 @@ class BaseMiLBManager:
dt = dt.replace(tzinfo=pytz.UTC) dt = dt.replace(tzinfo=pytz.UTC)
local_dt = dt.astimezone(tz) local_dt = dt.astimezone(tz)
return local_dt.strftime("%I:%M%p").lstrip('0') formatted_time = local_dt.strftime("%I:%M%p").lstrip('0')
self.logger.debug(f"[MiLB] Formatted time: {formatted_time}")
return formatted_time
except Exception as e: except Exception as e:
logger.error(f"Error formatting game time: {e}") logger.error(f"Error formatting game time: {e}")
return "TBD" return "TBD"
@@ -392,7 +426,9 @@ class BaseMiLBManager:
try: try:
# Check if test mode is enabled # Check if test mode is enabled
if self.milb_config.get('test_mode', False): test_mode = self.milb_config.get('test_mode', False)
self.logger.debug(f"[MiLB] Test mode: {test_mode}")
if test_mode:
self.logger.info("Using test mode data for MiLB") self.logger.info("Using test mode data for MiLB")
return { return {
'test_game_1': { 'test_game_1': {
@@ -417,6 +453,8 @@ class BaseMiLBManager:
current_month = now.month current_month = now.month
in_season = 4 <= current_month <= 9 in_season = 4 <= current_month <= 9
self.logger.debug(f"[MiLB] Current month: {current_month}, in_season: {in_season}")
if not in_season: if not in_season:
self.logger.info("MiLB is currently in offseason (October-March). No games expected.") self.logger.info("MiLB is currently in offseason (October-March). No games expected.")
self.logger.info("Consider enabling test_mode for offseason testing.") self.logger.info("Consider enabling test_mode for offseason testing.")
@@ -440,6 +478,8 @@ class BaseMiLBManager:
response = self.session.get(url, headers=self.headers, timeout=10) response = self.session.get(url, headers=self.headers, timeout=10)
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
# Increment API counter for successful request
increment_api_counter('sports', 1)
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
self.logger.error(f"Error fetching data from {url}: {e}") self.logger.error(f"Error fetching data from {url}: {e}")
continue continue
@@ -488,6 +528,8 @@ class BaseMiLBManager:
self.logger.warning(f"Skipping game {game_pk} due to missing 'gameDate'.") self.logger.warning(f"Skipping game {game_pk} due to missing 'gameDate'.")
continue continue
self.logger.debug(f"[MiLB] Game {game_pk} gameDate: {event.get('gameDate')}")
is_favorite_game = (home_abbr in self.favorite_teams or away_abbr in self.favorite_teams) is_favorite_game = (home_abbr in self.favorite_teams or away_abbr in self.favorite_teams)
if not self.favorite_teams or is_favorite_game: if not self.favorite_teams or is_favorite_game:
@@ -523,6 +565,8 @@ class BaseMiLBManager:
'home_record': home_record_str 'home_record': home_record_str
} }
self.logger.debug(f"[MiLB] Created game data for {game_pk}: status={mapped_status}, status_state={mapped_status_state}, start_time={event.get('gameDate')}")
if status_state == 'Live': if status_state == 'Live':
linescore = event.get('linescore', {}) linescore = event.get('linescore', {})
game_data['inning'] = linescore.get('currentInning', 1) game_data['inning'] = linescore.get('currentInning', 1)
@@ -1545,7 +1589,9 @@ class MiLBUpcomingManager(BaseMiLBManager):
return return
# --- Optimization: Filter for favorite teams before processing --- # --- Optimization: Filter for favorite teams before processing ---
if self.milb_config.get("show_favorite_teams_only", False) and self.favorite_teams: show_favorite_only = self.milb_config.get("show_favorite_teams_only", False)
self.logger.debug(f"[MiLB] show_favorite_teams_only: {show_favorite_only}, favorite_teams: {self.favorite_teams}")
if show_favorite_only and self.favorite_teams:
games = { games = {
game_id: game for game_id, game in games.items() game_id: game for game_id, game in games.items()
if game.get('home_team') in self.favorite_teams or game.get('away_team') in self.favorite_teams if game.get('home_team') in self.favorite_teams or game.get('away_team') in self.favorite_teams
@@ -1572,9 +1618,11 @@ class MiLBUpcomingManager(BaseMiLBManager):
continue continue
try: try:
self.logger.debug(f"[MiLB] Parsing start_time: {game['start_time']}")
game_time = datetime.fromisoformat(game['start_time'].replace('Z', '+00:00')) game_time = datetime.fromisoformat(game['start_time'].replace('Z', '+00:00'))
if game_time.tzinfo is None: if game_time.tzinfo is None:
game_time = game_time.replace(tzinfo=timezone.utc) game_time = game_time.replace(tzinfo=timezone.utc)
self.logger.debug(f"[MiLB] Parsed game_time: {game_time}")
except (ValueError, TypeError) as e: except (ValueError, TypeError) as e:
self.logger.error(f"Could not parse start_time for game {game_id}: {game['start_time']}. Error: {e}") self.logger.error(f"Could not parse start_time for game {game_id}: {game['start_time']}. Error: {e}")
continue continue
@@ -1593,6 +1641,7 @@ class MiLBUpcomingManager(BaseMiLBManager):
if is_upcoming: if is_upcoming:
new_upcoming_games.append(game) new_upcoming_games.append(game)
self.logger.info(f"[MiLB] Added upcoming game: {game.get('away_team')} @ {game.get('home_team')} at {game_time}") self.logger.info(f"[MiLB] Added upcoming game: {game.get('away_team')} @ {game.get('home_team')} at {game_time}")
self.logger.debug(f"[MiLB] Game data for upcoming: {game}")
# Sort by game time (soonest first) and limit to upcoming_games_to_show # Sort by game time (soonest first) and limit to upcoming_games_to_show
new_upcoming_games.sort(key=lambda x: x.get('start_time', '')) new_upcoming_games.sort(key=lambda x: x.get('start_time', ''))
@@ -1622,6 +1671,7 @@ class MiLBUpcomingManager(BaseMiLBManager):
def display(self, force_clear: bool = False): def display(self, force_clear: bool = False):
"""Display upcoming games.""" """Display upcoming games."""
self.logger.debug(f"[MiLB] Display called with {len(self.upcoming_games)} upcoming games")
if not self.upcoming_games: if not self.upcoming_games:
current_time = time.time() current_time = time.time()
if current_time - self.last_warning_time > self.warning_cooldown: if current_time - self.last_warning_time > self.warning_cooldown:
@@ -1639,13 +1689,17 @@ class MiLBUpcomingManager(BaseMiLBManager):
self.current_game = self.upcoming_games[self.current_game_index] self.current_game = self.upcoming_games[self.current_game_index]
self.last_game_switch = current_time self.last_game_switch = current_time
force_clear = True # Force clear when switching games force_clear = True # Force clear when switching games
self.logger.debug(f"[MiLB] Switched to game {self.current_game_index}: {self.current_game.get('away_team')} @ {self.current_game.get('home_team')}")
# Create and display the game image # Create and display the game image
if self.current_game: if self.current_game:
self.logger.debug(f"[MiLB] Creating display for current game: {self.current_game.get('away_team')} @ {self.current_game.get('home_team')}")
game_image = self._create_game_display(self.current_game) game_image = self._create_game_display(self.current_game)
self.display_manager.image = game_image self.display_manager.image = game_image
self.display_manager.draw = ImageDraw.Draw(self.display_manager.image) self.display_manager.draw = ImageDraw.Draw(self.display_manager.image)
self.display_manager.update_display() self.display_manager.update_display()
else:
self.logger.debug(f"[MiLB] No current game to display")
except Exception as e: except Exception as e:
self.logger.error(f"[MiLB] Error displaying upcoming game: {e}", exc_info=True) self.logger.error(f"[MiLB] Error displaying upcoming game: {e}", exc_info=True)

View File

@@ -13,6 +13,14 @@ from urllib3.util.retry import Retry
import pytz import pytz
from src.odds_manager import OddsManager from src.odds_manager import OddsManager
# Import the API counter function from web interface
try:
from web_interface_v2 import increment_api_counter
except ImportError:
# Fallback if web interface is not available
def increment_api_counter(kind: str, count: int = 1):
pass
# Get logger # Get logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -447,6 +455,10 @@ class BaseMLBManager:
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
# Increment API counter for sports data call
increment_api_counter('sports', 1)
self.logger.info(f"Found {len(data.get('events', []))} total games for date {date}") self.logger.info(f"Found {len(data.get('events', []))} total games for date {date}")
for event in data.get('events', []): for event in data.get('events', []):

View File

@@ -13,6 +13,14 @@ from src.config_manager import ConfigManager
from src.odds_manager import OddsManager from src.odds_manager import OddsManager
import pytz import pytz
# Import the API counter function from web interface
try:
from web_interface_v2 import increment_api_counter
except ImportError:
# Fallback if web interface is not available
def increment_api_counter(kind: str, count: int = 1):
pass
# Constants # Constants
ESPN_NBA_SCOREBOARD_URL = "https://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard" ESPN_NBA_SCOREBOARD_URL = "https://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard"
@@ -283,6 +291,9 @@ class BaseNBAManager:
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
# Increment API counter for sports data call
increment_api_counter('sports', 1)
if use_cache: if use_cache:
self.cache_manager.set(cache_key, data) self.cache_manager.set(cache_key, data)

View File

@@ -16,6 +16,14 @@ from src.cache_manager import CacheManager
from requests.adapters import HTTPAdapter from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry from urllib3.util.retry import Retry
# Import the API counter function from web interface
try:
from web_interface_v2 import increment_api_counter
except ImportError:
# Fallback if web interface is not available
def increment_api_counter(kind: str, count: int = 1):
pass
# Configure logging # Configure logging
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -107,6 +115,9 @@ class NewsManager:
response = self.session.get(url, headers=headers, timeout=10) response = self.session.get(url, headers=headers, timeout=10)
response.raise_for_status() response.raise_for_status()
# Increment API counter for news data call
increment_api_counter('news', 1)
root = ET.fromstring(response.content) root = ET.fromstring(response.content)
headlines = [] headlines = []

View File

@@ -13,6 +13,14 @@ from src.config_manager import ConfigManager
from src.odds_manager import OddsManager from src.odds_manager import OddsManager
import pytz import pytz
# Import the API counter function from web interface
try:
from web_interface_v2 import increment_api_counter
except ImportError:
# Fallback if web interface is not available
def increment_api_counter(kind: str, count: int = 1):
pass
# Constants # Constants
NHL_API_BASE_URL = "https://api-web.nhle.com/v1/schedule/" NHL_API_BASE_URL = "https://api-web.nhle.com/v1/schedule/"
@@ -126,6 +134,10 @@ class BaseNHLManager:
response = requests.get(url) response = requests.get(url)
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
# Increment API counter for sports data call
increment_api_counter('sports', 1)
self.logger.info(f"[NHL] Successfully fetched data from NHL API for {date_str}") self.logger.info(f"[NHL] Successfully fetched data from NHL API for {date_str}")
# Save to cache if caching is enabled # Save to cache if caching is enabled

View File

@@ -15,6 +15,14 @@ from .cache_manager import CacheManager
from requests.adapters import HTTPAdapter from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry from urllib3.util.retry import Retry
# Import the API counter function from web interface
try:
from web_interface_v2 import increment_api_counter
except ImportError:
# Fallback if web interface is not available
def increment_api_counter(kind: str, count: int = 1):
pass
# Get logger without configuring # Get logger without configuring
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -197,6 +205,9 @@ class StockManager:
data = response.json() data = response.json()
# Increment API counter for stock/crypto data call
increment_api_counter('stocks', 1)
# Extract the relevant data from the response # Extract the relevant data from the response
chart_data = data.get('chart', {}).get('result', [{}])[0] chart_data = data.get('chart', {}).get('result', [{}])[0]
meta = chart_data.get('meta', {}) meta = chart_data.get('meta', {})

View File

@@ -14,6 +14,14 @@ from .cache_manager import CacheManager
from requests.adapters import HTTPAdapter from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry from urllib3.util.retry import Retry
# Import the API counter function from web interface
try:
from web_interface_v2 import increment_api_counter
except ImportError:
# Fallback if web interface is not available
def increment_api_counter(kind: str, count: int = 1):
pass
# Configure logging # Configure logging
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -125,6 +133,9 @@ class StockNewsManager:
return [] return []
data = response.json() data = response.json()
# Increment API counter for news data call
increment_api_counter('news', 1)
news_items = data.get('news', []) news_items = data.get('news', [])
processed_news = [] processed_news = []

View File

@@ -7,6 +7,14 @@ import freetype
from .weather_icons import WeatherIcons from .weather_icons import WeatherIcons
from .cache_manager import CacheManager from .cache_manager import CacheManager
# Import the API counter function from web interface
try:
from web_interface_v2 import increment_api_counter
except ImportError:
# Fallback if web interface is not available
def increment_api_counter(kind: str, count: int = 1):
pass
class WeatherManager: class WeatherManager:
def __init__(self, config: Dict[str, Any], display_manager): def __init__(self, config: Dict[str, Any], display_manager):
@@ -108,6 +116,9 @@ class WeatherManager:
response.raise_for_status() response.raise_for_status()
geo_data = response.json() geo_data = response.json()
# Increment API counter for geocoding call
increment_api_counter('weather', 1)
if not geo_data: if not geo_data:
print(f"Could not find coordinates for {city}, {state}") print(f"Could not find coordinates for {city}, {state}")
return return
@@ -123,6 +134,9 @@ class WeatherManager:
response.raise_for_status() response.raise_for_status()
one_call_data = response.json() one_call_data = response.json()
# Increment API counter for weather data call
increment_api_counter('weather', 1)
# Store current weather data # Store current weather data
self.weather_data = { self.weather_data = {
'main': { 'main': {

View File

@@ -881,6 +881,15 @@
</div> </div>
</div> </div>
<h4>API Calls (24h window)</h4>
<div id="api-metrics" class="stat-card" style="text-align:left;">
<div>Loading API metrics...</div>
<div style="margin-top:10px; font-size:12px; color:#666;">If empty, ensure the server is running and /api/metrics is reachable.</div>
<div style="margin-top:10px;">
<button class="btn btn-primary" onclick="updateApiMetrics()"><i class="fas fa-sync"></i> Refresh API Metrics</button>
</div>
</div>
<h4>Quick Actions</h4> <h4>Quick Actions</h4>
<div class="display-controls"> <div class="display-controls">
<button class="btn btn-primary" onclick="systemAction('restart_service')"> <button class="btn btn-primary" onclick="systemAction('restart_service')">
@@ -1407,15 +1416,6 @@
Loading features configuration... Loading features configuration...
</div> </div>
</div> </div>
<h4>API Calls (24h window)</h4>
<div id="api-metrics" class="stat-card" style="text-align:left;">
<div>Loading API metrics...</div>
<div style="margin-top:10px; font-size:12px; color:#666;">If empty, ensure the server is running and /api/metrics is reachable.</div>
<div style="margin-top:10px;">
<button class="btn btn-primary" onclick="updateApiMetrics()"><i class="fas fa-sync"></i> Refresh API Metrics</button>
</div>
</div>
</div> </div>
<!-- Music Tab --> <!-- Music Tab -->