mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
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:
@@ -2132,7 +2132,11 @@
|
|||||||
body: JSON.stringify({ mode })
|
body: JSON.stringify({ mode })
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
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();
|
refreshOnDemandStatus();
|
||||||
}catch(err){
|
}catch(err){
|
||||||
showNotification('Error starting on-demand: ' + err, 'error');
|
showNotification('Error starting on-demand: ' + err, 'error');
|
||||||
@@ -2268,9 +2272,14 @@
|
|||||||
startPreviewPolling();
|
startPreviewPolling();
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('display_update', function(data) {
|
socket.on('display_update', function(data) {
|
||||||
updateDisplayPreview(data);
|
updateDisplayPreview(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on('ondemand_error', function(data) {
|
||||||
|
showNotification(`On-demand error: ${data.error}`, 'error');
|
||||||
|
refreshOnDemandStatus();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateApiMetrics(){
|
async function updateApiMetrics(){
|
||||||
@@ -2381,19 +2390,25 @@
|
|||||||
img.src = `data:image/png;base64,${data.image}`;
|
img.src = `data:image/png;base64,${data.image}`;
|
||||||
const meta = document.getElementById('previewMeta');
|
const meta = document.getElementById('previewMeta');
|
||||||
if (meta) {
|
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
|
// Once image loads, size the canvas to match
|
||||||
const width = (data.width || 128) * scale;
|
// Use config dimensions as fallback instead of hardcoded values
|
||||||
const height = (data.height || 32) * scale;
|
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.width = width + 'px';
|
||||||
img.style.height = height + 'px';
|
img.style.height = height + 'px';
|
||||||
ledCanvas.width = width;
|
ledCanvas.width = width;
|
||||||
ledCanvas.height = height;
|
ledCanvas.height = height;
|
||||||
canvas.width = width;
|
canvas.width = width;
|
||||||
canvas.height = height;
|
canvas.height = height;
|
||||||
drawGrid(canvas, data.width || 128, data.height || 32, scale);
|
drawGrid(canvas, data.width || configWidth, data.height || configHeight, scale);
|
||||||
renderLedDots();
|
renderLedDots();
|
||||||
} else {
|
} else {
|
||||||
stage.style.display = 'none';
|
stage.style.display = 'none';
|
||||||
@@ -3528,24 +3543,26 @@
|
|||||||
const cfg = currentConfig;
|
const cfg = currentConfig;
|
||||||
const leaguePrefixes = {
|
const leaguePrefixes = {
|
||||||
'nfl_scoreboard': 'nfl',
|
'nfl_scoreboard': 'nfl',
|
||||||
'mlb': 'mlb',
|
'mlb_scoreboard': 'mlb',
|
||||||
'milb': 'milb',
|
'milb_scoreboard': 'milb',
|
||||||
'nhl_scoreboard': 'nhl',
|
'nhl_scoreboard': 'nhl',
|
||||||
'nba_scoreboard': 'nba',
|
'nba_scoreboard': 'nba',
|
||||||
'ncaa_fb_scoreboard': 'ncaa_fb',
|
'ncaa_fb_scoreboard': 'ncaa_fb',
|
||||||
'ncaa_baseball_scoreboard': 'ncaa_baseball',
|
'ncaa_baseball_scoreboard': 'ncaa_baseball',
|
||||||
'ncaam_basketball_scoreboard': 'ncaam_basketball',
|
'ncaam_basketball_scoreboard': 'ncaam_basketball',
|
||||||
|
'ncaam_hockey_scoreboard': 'ncaam_hockey',
|
||||||
'soccer_scoreboard': 'soccer'
|
'soccer_scoreboard': 'soccer'
|
||||||
};
|
};
|
||||||
const leagues = [
|
const leagues = [
|
||||||
{ key: 'nfl_scoreboard', label: 'NFL' },
|
{ key: 'nfl_scoreboard', label: 'NFL' },
|
||||||
{ key: 'mlb', label: 'MLB' },
|
{ key: 'mlb_scoreboard', label: 'MLB' },
|
||||||
{ key: 'milb', label: 'MiLB' },
|
{ key: 'milb_scoreboard', label: 'MiLB' },
|
||||||
{ key: 'nhl_scoreboard', label: 'NHL' },
|
{ key: 'nhl_scoreboard', label: 'NHL' },
|
||||||
{ key: 'nba_scoreboard', label: 'NBA' },
|
{ key: 'nba_scoreboard', label: 'NBA' },
|
||||||
{ key: 'ncaa_fb_scoreboard', label: 'NCAA FB' },
|
{ key: 'ncaa_fb_scoreboard', label: 'NCAA FB' },
|
||||||
{ key: 'ncaa_baseball_scoreboard', label: 'NCAA Baseball' },
|
{ key: 'ncaa_baseball_scoreboard', label: 'NCAA Baseball' },
|
||||||
{ key: 'ncaam_basketball_scoreboard', label: 'NCAAM Basketball' },
|
{ key: 'ncaam_basketball_scoreboard', label: 'NCAAM Basketball' },
|
||||||
|
{ key: 'ncaam_hockey_scoreboard', label: 'NCAAM Hockey' },
|
||||||
{ key: 'soccer_scoreboard', label: 'Soccer' }
|
{ key: 'soccer_scoreboard', label: 'Soccer' }
|
||||||
];
|
];
|
||||||
const container = document.getElementById('sports-config');
|
const container = document.getElementById('sports-config');
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from src.nfl_managers import NFLLiveManager, NFLRecentManager, NFLUpcomingManage
|
|||||||
from src.ncaa_fb_managers import NCAAFBLiveManager, NCAAFBRecentManager, NCAAFBUpcomingManager
|
from src.ncaa_fb_managers import NCAAFBLiveManager, NCAAFBRecentManager, NCAAFBUpcomingManager
|
||||||
from src.ncaa_baseball_managers import NCAABaseballLiveManager, NCAABaseballRecentManager, NCAABaseballUpcomingManager
|
from src.ncaa_baseball_managers import NCAABaseballLiveManager, NCAABaseballRecentManager, NCAABaseballUpcomingManager
|
||||||
from src.ncaam_basketball_managers import NCAAMBasketballLiveManager, NCAAMBasketballRecentManager, NCAAMBasketballUpcomingManager
|
from src.ncaam_basketball_managers import NCAAMBasketballLiveManager, NCAAMBasketballRecentManager, NCAAMBasketballUpcomingManager
|
||||||
|
from src.ncaam_hockey_managers import NCAAMHockeyLiveManager, NCAAMHockeyRecentManager, NCAAMHockeyUpcomingManager
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import io
|
import io
|
||||||
import signal
|
import signal
|
||||||
@@ -307,8 +308,15 @@ class OnDemandRunner:
|
|||||||
try:
|
try:
|
||||||
# Suppress the startup test pattern to avoid random lines flash during on-demand
|
# Suppress the startup test pattern to avoid random lines flash during on-demand
|
||||||
display_manager = DisplayManager(self.config, suppress_test_pattern=True)
|
display_manager = DisplayManager(self.config, suppress_test_pattern=True)
|
||||||
except Exception:
|
logger.info("DisplayManager initialized successfully for on-demand")
|
||||||
display_manager = DisplayManager({'display': {'hardware': {}}}, force_fallback=True, suppress_test_pattern=True)
|
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()
|
display_monitor.start()
|
||||||
|
|
||||||
def _is_service_active(self) -> bool:
|
def _is_service_active(self) -> bool:
|
||||||
@@ -325,17 +333,26 @@ class OnDemandRunner:
|
|||||||
|
|
||||||
# If already running same mode, no-op
|
# If already running same mode, no-op
|
||||||
if self.running and self.mode == mode:
|
if self.running and self.mode == mode:
|
||||||
|
logger.info(f"On-demand mode {mode} is already running")
|
||||||
return
|
return
|
||||||
# Switch from previous
|
# Switch from previous
|
||||||
if self.running:
|
if self.running:
|
||||||
|
logger.info(f"Stopping previous on-demand mode {self.mode} to start {mode}")
|
||||||
self.stop()
|
self.stop()
|
||||||
|
|
||||||
self._ensure_infra()
|
try:
|
||||||
self.mode = mode
|
self._ensure_infra()
|
||||||
self.running = True
|
self.mode = mode
|
||||||
self.force_clear_next = True
|
self.running = True
|
||||||
# Use SocketIO bg task for cooperative sleeping
|
self.force_clear_next = True
|
||||||
self.thread = socketio.start_background_task(self._run_loop)
|
# 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):
|
def stop(self):
|
||||||
"""Stop on-demand display and clear the screen."""
|
"""Stop on-demand display and clear the screen."""
|
||||||
@@ -436,6 +453,8 @@ class OnDemandRunner:
|
|||||||
cls = {'live': NCAABaseballLiveManager, 'recent': NCAABaseballRecentManager, 'upcoming': NCAABaseballUpcomingManager}[variant]
|
cls = {'live': NCAABaseballLiveManager, 'recent': NCAABaseballRecentManager, 'upcoming': NCAABaseballUpcomingManager}[variant]
|
||||||
elif kind == 'ncaam_basketball':
|
elif kind == 'ncaam_basketball':
|
||||||
cls = {'live': NCAAMBasketballLiveManager, 'recent': NCAAMBasketballRecentManager, 'upcoming': NCAAMBasketballUpcomingManager}[variant]
|
cls = {'live': NCAAMBasketballLiveManager, 'recent': NCAAMBasketballRecentManager, 'upcoming': NCAAMBasketballUpcomingManager}[variant]
|
||||||
|
elif kind == 'ncaam_hockey':
|
||||||
|
cls = {'live': NCAAMHockeyLiveManager, 'recent': NCAAMHockeyRecentManager, 'upcoming': NCAAMHockeyUpcomingManager}[variant]
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unsupported sport kind: {kind}")
|
raise ValueError(f"Unsupported sport kind: {kind}")
|
||||||
mgr = cls(cfg, display_manager, self.cache_manager)
|
mgr = cls(cfg, display_manager, self.cache_manager)
|
||||||
@@ -465,9 +484,15 @@ class OnDemandRunner:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
manager, display_fn, update_fn, update_interval = self._build_manager(mode)
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Failed to initialize on-demand manager for mode {mode}: {e}")
|
logger.error(f"Failed to initialize on-demand manager for mode {mode}: {e}")
|
||||||
self.running = False
|
self.running = False
|
||||||
|
# Emit error to client
|
||||||
|
try:
|
||||||
|
socketio.emit('ondemand_error', {'mode': mode, 'error': str(e)})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return
|
return
|
||||||
|
|
||||||
last_update = 0.0
|
last_update = 0.0
|
||||||
@@ -506,6 +531,11 @@ class OnDemandRunner:
|
|||||||
|
|
||||||
except Exception as loop_err:
|
except Exception as loop_err:
|
||||||
logger.error(f"Error in on-demand loop for {mode}: {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
|
# small backoff to avoid tight error loop
|
||||||
try:
|
try:
|
||||||
socketio.sleep(0.5)
|
socketio.sleep(0.5)
|
||||||
@@ -934,14 +964,23 @@ def api_ondemand_start():
|
|||||||
mode = (data or {}).get('mode')
|
mode = (data or {}).get('mode')
|
||||||
if not mode:
|
if not mode:
|
||||||
return jsonify({'status': 'error', 'message': 'Missing mode'}), 400
|
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
|
# Refuse if service is running
|
||||||
if on_demand_runner._is_service_active():
|
if on_demand_runner._is_service_active():
|
||||||
return jsonify({'status': 'error', 'message': 'Service is active. Stop it first to use On-Demand.'}), 400
|
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)
|
on_demand_runner.start(mode)
|
||||||
return jsonify({'status': 'success', 'message': f'On-Demand started: {mode}', 'on_demand': on_demand_runner.status()})
|
return jsonify({'status': 'success', 'message': f'On-Demand started: {mode}', 'on_demand': on_demand_runner.status()})
|
||||||
except RuntimeError as rte:
|
except RuntimeError as rte:
|
||||||
|
logger.error(f"Runtime error starting on-demand {mode}: {rte}")
|
||||||
return jsonify({'status': 'error', 'message': str(rte)}), 400
|
return jsonify({'status': 'error', 'message': str(rte)}), 400
|
||||||
except Exception as e:
|
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
|
return jsonify({'status': 'error', 'message': f'Error starting on-demand: {e}'}), 500
|
||||||
|
|
||||||
@app.route('/api/ondemand/stop', methods=['POST'])
|
@app.route('/api/ondemand/stop', methods=['POST'])
|
||||||
@@ -1521,6 +1560,36 @@ def view_logs():
|
|||||||
def get_current_display():
|
def get_current_display():
|
||||||
"""Get current display image as base64."""
|
"""Get current display image as base64."""
|
||||||
try:
|
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)
|
return jsonify(current_display_data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'status': 'error', 'message': str(e), 'image': None}), 500
|
return jsonify({'status': 'error', 'message': str(e), 'image': None}), 500
|
||||||
|
|||||||
Reference in New Issue
Block a user