diff --git a/templates/index_v2.html b/templates/index_v2.html index 166303a2..f05028f4 100644 --- a/templates/index_v2.html +++ b/templates/index_v2.html @@ -2132,7 +2132,11 @@ body: JSON.stringify({ mode }) }); const data = await res.json(); - showNotification(data.message || 'Requested on-demand start', data.status || 'success'); + if(data.status === 'success'){ + showNotification(data.message || 'On-demand started successfully', 'success'); + } else { + showNotification(data.message || 'Failed to start on-demand', 'error'); + } refreshOnDemandStatus(); }catch(err){ showNotification('Error starting on-demand: ' + err, 'error'); @@ -2268,9 +2272,14 @@ startPreviewPolling(); }); - socket.on('display_update', function(data) { - updateDisplayPreview(data); - }); + socket.on('display_update', function(data) { + updateDisplayPreview(data); + }); + + socket.on('ondemand_error', function(data) { + showNotification(`On-demand error: ${data.error}`, 'error'); + refreshOnDemandStatus(); + }); } async function updateApiMetrics(){ @@ -2381,19 +2390,25 @@ img.src = `data:image/png;base64,${data.image}`; const meta = document.getElementById('previewMeta'); if (meta) { - meta.textContent = `${data.width || 128} x ${data.height || 32} @ ${scale}x`; + // Use config dimensions as fallback instead of hardcoded values + const configWidth = {{ main_config.get('display', {}).get('hardware', {}).get('cols', 64) * main_config.get('display', {}).get('hardware', {}).get('chain_length', 1) }}; + const configHeight = {{ main_config.get('display', {}).get('hardware', {}).get('rows', 32) * main_config.get('display', {}).get('hardware', {}).get('parallel', 1) }}; + meta.textContent = `${data.width || configWidth} x ${data.height || configHeight} @ ${scale}x`; } // Once image loads, size the canvas to match - const width = (data.width || 128) * scale; - const height = (data.height || 32) * scale; + // Use config dimensions as fallback instead of hardcoded values + const configWidth = {{ main_config.get('display', {}).get('hardware', {}).get('cols', 64) * main_config.get('display', {}).get('hardware', {}).get('chain_length', 1) }}; + const configHeight = {{ main_config.get('display', {}).get('hardware', {}).get('rows', 32) * main_config.get('display', {}).get('hardware', {}).get('parallel', 1) }}; + const width = (data.width || configWidth) * scale; + const height = (data.height || configHeight) * scale; img.style.width = width + 'px'; img.style.height = height + 'px'; ledCanvas.width = width; ledCanvas.height = height; canvas.width = width; canvas.height = height; - drawGrid(canvas, data.width || 128, data.height || 32, scale); + drawGrid(canvas, data.width || configWidth, data.height || configHeight, scale); renderLedDots(); } else { stage.style.display = 'none'; @@ -3528,24 +3543,26 @@ const cfg = currentConfig; const leaguePrefixes = { 'nfl_scoreboard': 'nfl', - 'mlb': 'mlb', - 'milb': 'milb', + 'mlb_scoreboard': 'mlb', + 'milb_scoreboard': 'milb', 'nhl_scoreboard': 'nhl', 'nba_scoreboard': 'nba', 'ncaa_fb_scoreboard': 'ncaa_fb', 'ncaa_baseball_scoreboard': 'ncaa_baseball', 'ncaam_basketball_scoreboard': 'ncaam_basketball', + 'ncaam_hockey_scoreboard': 'ncaam_hockey', 'soccer_scoreboard': 'soccer' }; const leagues = [ { key: 'nfl_scoreboard', label: 'NFL' }, - { key: 'mlb', label: 'MLB' }, - { key: 'milb', label: 'MiLB' }, + { key: 'mlb_scoreboard', label: 'MLB' }, + { key: 'milb_scoreboard', label: 'MiLB' }, { key: 'nhl_scoreboard', label: 'NHL' }, { key: 'nba_scoreboard', label: 'NBA' }, { key: 'ncaa_fb_scoreboard', label: 'NCAA FB' }, { key: 'ncaa_baseball_scoreboard', label: 'NCAA Baseball' }, { key: 'ncaam_basketball_scoreboard', label: 'NCAAM Basketball' }, + { key: 'ncaam_hockey_scoreboard', label: 'NCAAM Hockey' }, { key: 'soccer_scoreboard', label: 'Soccer' } ]; const container = document.getElementById('sports-config'); diff --git a/web_interface_v2.py b/web_interface_v2.py index 913f91eb..edc1304b 100644 --- a/web_interface_v2.py +++ b/web_interface_v2.py @@ -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