Fix/web UI display preview resolution (#69)

* fix(web-ui): Use config display dimensions instead of hardcoded 128x32 in Live Display Preview

- Updated /api/display/current endpoint to calculate display dimensions from config
- Modified HTML template to use config dimensions as fallbacks instead of hardcoded 128x32
- Display preview now shows correct resolution based on cols*chain_length x rows*parallel
- Maintains backward compatibility with existing API responses

* feat(web-ui): Add missing NCAAM Hockey sports manager to web interface

- Added NCAAM Hockey import and OnDemandRunner support
- Updated sports configuration UI to include NCAAM Hockey
- Fixed MLB and MiLB config keys to match template (_scoreboard suffix)
- All sports managers now properly represented in web interface:
  - NFL, MLB, MiLB, NHL, NBA, NCAA FB, NCAA Baseball, NCAAM Basketball, NCAAM Hockey, Soccer
- Maintains backward compatibility with existing configurations

* fix(web-ui): Improve on-demand button error handling and prevent crashes

- Enhanced error handling in OnDemandRunner with better logging and fallback modes
- Added robust display manager initialization with fallback support
- Improved error reporting via WebSocket to client for real-time feedback
- Added input validation for on-demand mode parameters
- Enhanced client-side error handling with better user notifications
- Added safety checks to prevent multiple on-demand instances
- Fixed display manager initialization issues that caused crashes
- Improved error recovery and graceful degradation
This commit is contained in:
Chuck
2025-09-25 19:08:21 -04:00
committed by GitHub
parent abceb8205c
commit 1bc4e531ae
2 changed files with 106 additions and 20 deletions

View File

@@ -30,6 +30,7 @@ from src.nfl_managers import NFLLiveManager, NFLRecentManager, NFLUpcomingManage
from src.ncaa_fb_managers import NCAAFBLiveManager, NCAAFBRecentManager, NCAAFBUpcomingManager
from src.ncaa_baseball_managers import NCAABaseballLiveManager, NCAABaseballRecentManager, NCAABaseballUpcomingManager
from src.ncaam_basketball_managers import NCAAMBasketballLiveManager, NCAAMBasketballRecentManager, NCAAMBasketballUpcomingManager
from src.ncaam_hockey_managers import NCAAMHockeyLiveManager, NCAAMHockeyRecentManager, NCAAMHockeyUpcomingManager
from PIL import Image
import io
import signal
@@ -307,8 +308,15 @@ class OnDemandRunner:
try:
# Suppress the startup test pattern to avoid random lines flash during on-demand
display_manager = DisplayManager(self.config, suppress_test_pattern=True)
except Exception:
display_manager = DisplayManager({'display': {'hardware': {}}}, force_fallback=True, suppress_test_pattern=True)
logger.info("DisplayManager initialized successfully for on-demand")
except Exception as e:
logger.warning(f"Failed to initialize DisplayManager with config, using fallback: {e}")
try:
display_manager = DisplayManager({'display': {'hardware': {}}}, force_fallback=True, suppress_test_pattern=True)
logger.info("DisplayManager initialized in fallback mode for on-demand")
except Exception as fallback_error:
logger.error(f"Failed to initialize DisplayManager even in fallback mode: {fallback_error}")
raise RuntimeError(f"Cannot initialize display manager for on-demand: {fallback_error}")
display_monitor.start()
def _is_service_active(self) -> bool:
@@ -325,17 +333,26 @@ class OnDemandRunner:
# If already running same mode, no-op
if self.running and self.mode == mode:
logger.info(f"On-demand mode {mode} is already running")
return
# Switch from previous
if self.running:
logger.info(f"Stopping previous on-demand mode {self.mode} to start {mode}")
self.stop()
self._ensure_infra()
self.mode = mode
self.running = True
self.force_clear_next = True
# Use SocketIO bg task for cooperative sleeping
self.thread = socketio.start_background_task(self._run_loop)
try:
self._ensure_infra()
self.mode = mode
self.running = True
self.force_clear_next = True
# Use SocketIO bg task for cooperative sleeping
self.thread = socketio.start_background_task(self._run_loop)
logger.info(f"On-demand mode {mode} started successfully")
except Exception as e:
logger.error(f"Failed to start on-demand mode {mode}: {e}")
self.running = False
self.mode = None
raise RuntimeError(f"Failed to start on-demand mode: {e}")
def stop(self):
"""Stop on-demand display and clear the screen."""
@@ -436,6 +453,8 @@ class OnDemandRunner:
cls = {'live': NCAABaseballLiveManager, 'recent': NCAABaseballRecentManager, 'upcoming': NCAABaseballUpcomingManager}[variant]
elif kind == 'ncaam_basketball':
cls = {'live': NCAAMBasketballLiveManager, 'recent': NCAAMBasketballRecentManager, 'upcoming': NCAAMBasketballUpcomingManager}[variant]
elif kind == 'ncaam_hockey':
cls = {'live': NCAAMHockeyLiveManager, 'recent': NCAAMHockeyRecentManager, 'upcoming': NCAAMHockeyUpcomingManager}[variant]
else:
raise ValueError(f"Unsupported sport kind: {kind}")
mgr = cls(cfg, display_manager, self.cache_manager)
@@ -465,9 +484,15 @@ class OnDemandRunner:
try:
manager, display_fn, update_fn, update_interval = self._build_manager(mode)
logger.info(f"On-demand manager for {mode} initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize on-demand manager for mode {mode}: {e}")
self.running = False
# Emit error to client
try:
socketio.emit('ondemand_error', {'mode': mode, 'error': str(e)})
except Exception:
pass
return
last_update = 0.0
@@ -506,6 +531,11 @@ class OnDemandRunner:
except Exception as loop_err:
logger.error(f"Error in on-demand loop for {mode}: {loop_err}")
# Emit error to client
try:
socketio.emit('ondemand_error', {'mode': mode, 'error': str(loop_err)})
except Exception:
pass
# small backoff to avoid tight error loop
try:
socketio.sleep(0.5)
@@ -934,14 +964,23 @@ def api_ondemand_start():
mode = (data or {}).get('mode')
if not mode:
return jsonify({'status': 'error', 'message': 'Missing mode'}), 400
# Validate mode format
if not isinstance(mode, str) or not mode.strip():
return jsonify({'status': 'error', 'message': 'Invalid mode format'}), 400
# Refuse if service is running
if on_demand_runner._is_service_active():
return jsonify({'status': 'error', 'message': 'Service is active. Stop it first to use On-Demand.'}), 400
logger.info(f"Starting on-demand mode: {mode}")
on_demand_runner.start(mode)
return jsonify({'status': 'success', 'message': f'On-Demand started: {mode}', 'on_demand': on_demand_runner.status()})
except RuntimeError as rte:
logger.error(f"Runtime error starting on-demand {mode}: {rte}")
return jsonify({'status': 'error', 'message': str(rte)}), 400
except Exception as e:
logger.error(f"Unexpected error starting on-demand {mode}: {e}")
return jsonify({'status': 'error', 'message': f'Error starting on-demand: {e}'}), 500
@app.route('/api/ondemand/stop', methods=['POST'])
@@ -1521,6 +1560,36 @@ def view_logs():
def get_current_display():
"""Get current display image as base64."""
try:
# Get display dimensions from config if not available in current_display_data
if not current_display_data or not current_display_data.get('width') or not current_display_data.get('height'):
try:
config = config_manager.load_config()
display_config = config.get('display', {}).get('hardware', {})
rows = display_config.get('rows', 32)
cols = display_config.get('cols', 64)
chain_length = display_config.get('chain_length', 1)
parallel = display_config.get('parallel', 1)
# Calculate total display dimensions
total_width = cols * chain_length
total_height = rows * parallel
# Update current_display_data with config dimensions if missing
if not current_display_data:
current_display_data = {}
if not current_display_data.get('width'):
current_display_data['width'] = total_width
if not current_display_data.get('height'):
current_display_data['height'] = total_height
except Exception as config_error:
# Fallback to default dimensions if config fails
if not current_display_data:
current_display_data = {}
if not current_display_data.get('width'):
current_display_data['width'] = 128
if not current_display_data.get('height'):
current_display_data['height'] = 32
return jsonify(current_display_data)
except Exception as e:
return jsonify({'status': 'error', 'message': str(e), 'image': None}), 500