mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
Feature/vegas scroll mode (#215)
* feat(display): add Vegas-style continuous scroll mode Implement an opt-in Vegas ticker mode that composes all enabled plugin content into a single continuous horizontal scroll. Includes a modular package (src/vegas_mode/) with double-buffered streaming, 125 FPS render pipeline using the existing ScrollHelper, live priority interruption support, and a web UI for configuration with drag-drop plugin ordering. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(vegas): add three-mode display system (SCROLL, FIXED_SEGMENT, STATIC) Adds a flexible display mode system for Vegas scroll mode that allows plugins to control how their content appears in the continuous scroll: - SCROLL: Content scrolls continuously (multi-item plugins like sports) - FIXED_SEGMENT: Fixed block that scrolls by (clock, weather) - STATIC: Scroll pauses, plugin displays, then resumes (alerts) Changes: - Add VegasDisplayMode enum to base_plugin.py with backward-compatible mapping from legacy get_vegas_content_type() - Add static pause handling to coordinator with scroll position save/restore - Add mode-aware content composition to stream_manager - Add vegas_mode info to /api/v3/plugins/installed endpoint - Add mode indicators to Vegas settings UI - Add comprehensive plugin developer documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas,widgets): address validation, thread safety, and XSS issues Vegas mode fixes: - config.py: align validation limits with UI (scroll_speed max 200, separator_width max 128) - coordinator.py: fix race condition by properly initializing _pending_config - plugin_adapter.py: remove unused import - render_pipeline.py: preserve deque type in reset() method - stream_manager.py: fix lock handling and swap_buffers to truly swap API fixes: - api_v3.py: normalize boolean checkbox values, validate numeric fields, ensure JSON arrays Widget fixes: - day-selector.js: remove escapeHtml from JSON.stringify to prevent corruption - password-input.js: use deterministic color class mapping for Tailwind JIT - radio-group.js: replace inline onchange with addEventListener to prevent XSS - select-dropdown.js: guard global registry access - slider.js: add escapeAttr for attributes, fix null dereference in setValue Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): improve exception handling and static pause state management coordinator.py: - _check_live_priority: use logger.exception for full traceback - _end_static_pause: guard scroll resume on interruption (stop/live priority) - _update_static_mode_plugins: log errors instead of silently swallowing render_pipeline.py: - compose_scroll_content: use specific exceptions and logger.exception - render_frame: use specific exceptions and logger.exception - hot_swap_content: use specific exceptions and logger.exception Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): add interrupt mechanism and improve config/exception handling - Add interrupt checker callback to Vegas coordinator for responsive handling of on-demand requests and wifi status during Vegas mode - Fix config.py update() to include dynamic duration fields - Fix is_plugin_included() consistency with get_ordered_plugins() - Update _apply_pending_config to propagate config to StreamManager - Change _fetch_plugin_content to use logger.exception for traceback - Replace bare except in _refresh_plugin_list with specific exceptions - Add aria-label accessibility to Vegas toggle checkbox - Fix XSS vulnerability in plugin metadata rendering with escapeHtml Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): improve logging, validation, lock handling, and config updates - display_controller.py: use logger.exception for Vegas errors with traceback - base_plugin.py: validate vegas_panel_count as positive integer with warning - coordinator.py: fix _apply_pending_config to avoid losing concurrent updates by clearing _pending_config while holding lock - plugin_adapter.py: remove broad catch-all, use narrower exception types (AttributeError, TypeError, ValueError, OSError, RuntimeError) and logger.exception for traceback preservation - api_v3.py: only update vegas_config['enabled'] when key is present in data to prevent incorrect disabling when checkbox is omitted Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): improve cycle advancement, logging, and accessibility - Add advance_cycle() method to StreamManager for clearing buffer between cycles - Call advance_cycle() in RenderPipeline.start_new_cycle() for fresh content - Use logger.exception() for interrupt check and static pause errors (full tracebacks) - Add id="vegas_scroll_label" to h3 for aria-labelledby reference - Call updatePluginConfig() after rendering plugin list for proper initialization Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): add thread-safety, preserve updates, and improve logging - display_controller.py: Use logger.exception() for Vegas import errors - plugin_adapter.py: Add thread-safe cache lock, remove unused exception binding - stream_manager.py: In-place merge in process_updates() preserves non-updated plugins - api_v3.py: Change vegas_scroll_enabled default from False to True Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): add debug logging and narrow exception types - stream_manager.py: Log when get_vegas_display_mode() is unavailable - stream_manager.py: Narrow exception type from Exception to (AttributeError, TypeError) - api_v3.py: Log exceptions when reading Vegas display metadata with plugin context Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): fix method call and improve exception logging - Fix _check_vegas_interrupt() calling nonexistent _check_wifi_status(), now correctly calls _check_wifi_status_message() - Update _refresh_plugin_list() exception handler to use logger.exception() with plugin_id and class name for remote debugging Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(web): replace complex toggle with standard checkbox for Vegas mode The Tailwind pseudo-element toggle (after:content-[''], etc.) wasn't rendering because these classes weren't in the CSS bundle. Replaced with a simple checkbox that matches other form controls in the template. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * debug(vegas): add detailed logging to _refresh_plugin_list Track why plugins aren't being found for Vegas scroll: - Log count of loaded plugins - Log enabled status for each plugin - Log content_type and display_mode checks - Log when plugin_manager lacks loaded_plugins Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): use correct attribute name for plugin manager StreamManager and VegasModeCoordinator were checking for plugin_manager.loaded_plugins but PluginManager stores active plugins in plugin_manager.plugins. This caused Vegas scroll to find zero plugins despite plugins being available. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): convert scroll_speed from px/sec to px/frame correctly The config scroll_speed is in pixels per second, but ScrollHelper in frame_based_scrolling mode interprets it as pixels per frame. Previously this caused the speed to be clamped to max 5.0 regardless of the configured value. Now properly converts: pixels_per_frame = scroll_speed * scroll_delay With defaults (50 px/s, 0.02s delay), this gives 1 px/frame = 50 px/s. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(vegas): add FPS logging every 5 seconds Logs actual FPS vs target FPS to help diagnose performance issues. Shows frame count in each 5-second interval. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): improve plugin content capture reliability - Call update_data() before capture to ensure fresh plugin data - Try display() without force_clear first, fallback if TypeError - Retry capture with force_clear=True if first attempt is blank - Use histogram-based blank detection instead of point sampling (more reliable for content positioned anywhere in frame) This should help capture content from plugins that don't implement get_vegas_content() natively. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): handle callable width/height on display_manager DisplayManager.width and .height may be methods or properties depending on the implementation. Use callable() check to call them if needed, ensuring display_width and display_height are always integers. Fixes potential TypeError when width/height are methods. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): use logger.exception for display mode errors Replace logger.error with logger.exception to capture full stack trace when get_vegas_display_mode() fails on a plugin. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): protect plugin list updates with buffer lock Move assignment of _ordered_plugins and index resets under _buffer_lock to prevent race conditions with _prefetch_content() which reads these variables under the same lock. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): catch all exceptions in get_vegas_display_mode Broaden exception handling from AttributeError/TypeError to Exception so any plugin error in get_vegas_display_mode() doesn't abort the entire plugin list refresh. The loop continues with the default FIXED_SEGMENT mode. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): refresh stream manager when config updates After updating stream_manager.config, force a refresh to pick up changes to plugin_order, excluded_plugins, and buffer_ahead settings. Also use logger.exception to capture full stack traces on config update errors. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * debug(vegas): add detailed logging for blank image detection * feat(vegas): extract full scroll content from plugins using ScrollHelper Plugins like ledmatrix-stocks and odds-ticker use ScrollHelper with a cached_image that contains their full scrolling content. Instead of falling back to single-frame capture, now check for scroll_helper.cached_image first to get the complete scrolling content for Vegas mode. * debug(vegas): add comprehensive INFO-level logging for plugin content flow - Log each plugin being processed with class name - Log which content methods are tried (native, scroll_helper, fallback) - Log success/failure of each method with image dimensions - Log brightness check results for blank image detection - Add visual separators in logs for easier debugging - Log plugin list refresh with enabled/excluded status Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(vegas): trigger scroll content generation when cache is empty When a plugin has a scroll_helper but its cached_image is not yet populated, try to trigger content generation by: 1. Calling _create_scrolling_display() if available (stocks pattern) 2. Calling display(force_clear=True) as a fallback This allows plugins like stocks to provide their full scroll content even when Vegas mode starts before the plugin has run its normal display cycle. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: improve exception handling in plugin_adapter scroll content retrieval Replace broad except Exception handlers with narrow exception types (AttributeError, TypeError, ValueError, OSError) and use logger.exception instead of logger.warning/info to capture full stack traces for better diagnosability. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: narrow exception handling in coordinator and plugin_adapter - coordinator.py: Replace broad Exception catch around get_vegas_display_mode() with (AttributeError, TypeError) and use logger.exception for stack traces - plugin_adapter.py: Narrow update_data() exception handler to (AttributeError, RuntimeError, OSError) and use logger.exception Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: improve Vegas mode robustness and API validation - display_controller: Guard against None plugin_manager in Vegas init - coordinator: Restore scrolling state in resume() to match pause() - api_v3: Validate Vegas numeric fields with range checks and 400 errors Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -104,7 +104,8 @@
|
||||
let html = `<div id="${fieldId}_widget" class="day-selector-widget" data-field-id="${fieldId}">`;
|
||||
|
||||
// Hidden input to store the value as JSON array
|
||||
html += `<input type="hidden" id="${fieldId}_data" name="${escapeHtml(inputName)}" value='${escapeHtml(JSON.stringify(selectedDays))}'>`;
|
||||
// Note: Using single quotes for attribute, JSON uses double quotes, so no escaping needed
|
||||
html += `<input type="hidden" id="${fieldId}_data" name="${escapeHtml(inputName)}" value='${JSON.stringify(selectedDays)}'>`;
|
||||
|
||||
// Select All toggle
|
||||
if (showSelectAll) {
|
||||
|
||||
@@ -53,6 +53,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Deterministic color class mapping to avoid Tailwind JIT purging
|
||||
const STRENGTH_COLORS = {
|
||||
gray: 'bg-gray-300',
|
||||
red: 'bg-red-500',
|
||||
orange: 'bg-orange-500',
|
||||
yellow: 'bg-yellow-500',
|
||||
lime: 'bg-lime-500',
|
||||
green: 'bg-green-500'
|
||||
};
|
||||
|
||||
function calculateStrength(password, options) {
|
||||
if (!password) return { score: 0, label: '', color: 'gray' };
|
||||
|
||||
@@ -143,20 +153,21 @@
|
||||
// Strength indicator
|
||||
if (showStrength) {
|
||||
const strength = calculateStrength(currentValue, xOptions);
|
||||
const colorClass = STRENGTH_COLORS[strength.color] || STRENGTH_COLORS.gray;
|
||||
html += `
|
||||
<div id="${fieldId}_strength" class="mt-2 ${currentValue ? '' : 'hidden'}">
|
||||
<div class="flex gap-1 mb-1">
|
||||
<div class="h-1 flex-1 rounded bg-gray-200">
|
||||
<div id="${fieldId}_bar0" class="h-full rounded ${strength.score >= 1 ? 'bg-' + strength.color + '-500' : ''}" style="width: ${strength.score >= 1 ? '100%' : '0'}"></div>
|
||||
<div id="${fieldId}_bar0" class="h-full rounded ${strength.score >= 1 ? colorClass : ''}" style="width: ${strength.score >= 1 ? '100%' : '0'}"></div>
|
||||
</div>
|
||||
<div class="h-1 flex-1 rounded bg-gray-200">
|
||||
<div id="${fieldId}_bar1" class="h-full rounded ${strength.score >= 2 ? 'bg-' + strength.color + '-500' : ''}" style="width: ${strength.score >= 2 ? '100%' : '0'}"></div>
|
||||
<div id="${fieldId}_bar1" class="h-full rounded ${strength.score >= 2 ? colorClass : ''}" style="width: ${strength.score >= 2 ? '100%' : '0'}"></div>
|
||||
</div>
|
||||
<div class="h-1 flex-1 rounded bg-gray-200">
|
||||
<div id="${fieldId}_bar2" class="h-full rounded ${strength.score >= 3 ? 'bg-' + strength.color + '-500' : ''}" style="width: ${strength.score >= 3 ? '100%' : '0'}"></div>
|
||||
<div id="${fieldId}_bar2" class="h-full rounded ${strength.score >= 3 ? colorClass : ''}" style="width: ${strength.score >= 3 ? '100%' : '0'}"></div>
|
||||
</div>
|
||||
<div class="h-1 flex-1 rounded bg-gray-200">
|
||||
<div id="${fieldId}_bar3" class="h-full rounded ${strength.score >= 4 ? 'bg-' + strength.color + '-500' : ''}" style="width: ${strength.score >= 4 ? '100%' : '0'}"></div>
|
||||
<div id="${fieldId}_bar3" class="h-full rounded ${strength.score >= 4 ? colorClass : ''}" style="width: ${strength.score >= 4 ? '100%' : '0'}"></div>
|
||||
</div>
|
||||
</div>
|
||||
<span id="${fieldId}_strength_label" class="text-xs text-gray-500">${strength.label}</span>
|
||||
@@ -256,15 +267,8 @@
|
||||
strengthEl.classList.remove('hidden');
|
||||
const strength = calculateStrength(value, { minLength });
|
||||
|
||||
// Update bars
|
||||
const colors = {
|
||||
red: 'bg-red-500',
|
||||
orange: 'bg-orange-500',
|
||||
yellow: 'bg-yellow-500',
|
||||
lime: 'bg-lime-500',
|
||||
green: 'bg-green-500'
|
||||
};
|
||||
const colorClass = colors[strength.color] || 'bg-gray-300';
|
||||
// Update bars using shared color mapping
|
||||
const colorClass = STRENGTH_COLORS[strength.color] || STRENGTH_COLORS.gray;
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const bar = document.getElementById(`${safeId}_bar${i}`);
|
||||
|
||||
@@ -92,7 +92,6 @@
|
||||
value="${escapeHtml(String(optValue))}"
|
||||
${isChecked ? 'checked' : ''}
|
||||
${disabled ? 'disabled' : ''}
|
||||
onchange="window.LEDMatrixWidgets.getHandlers('radio-group').onChange('${fieldId}', this.value)"
|
||||
class="h-4 w-4 text-blue-600 border-gray-300 focus:ring-blue-500 ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}">
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
@@ -106,6 +105,17 @@
|
||||
html += '</div>';
|
||||
|
||||
container.innerHTML = html;
|
||||
|
||||
// Attach event listeners (safer than inline handlers, prevents XSS)
|
||||
const widget = document.getElementById(`${fieldId}_widget`);
|
||||
if (widget) {
|
||||
const radios = widget.querySelectorAll('input[type="radio"]');
|
||||
radios.forEach(radio => {
|
||||
radio.addEventListener('change', () => {
|
||||
triggerChange(fieldId, radio.value);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getValue: function(fieldId) {
|
||||
|
||||
@@ -53,6 +53,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Guard against missing global registry
|
||||
if (!window.LEDMatrixWidgets || typeof window.LEDMatrixWidgets.register !== 'function') {
|
||||
console.error('[SelectDropdownWidget] LEDMatrixWidgets registry not available');
|
||||
return;
|
||||
}
|
||||
|
||||
window.LEDMatrixWidgets.register('select-dropdown', {
|
||||
name: 'Select Dropdown Widget',
|
||||
version: '1.0.0',
|
||||
|
||||
@@ -35,6 +35,11 @@
|
||||
return div.innerHTML.replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// Escape for use in HTML attributes (also escapes quotes)
|
||||
function escapeAttr(text) {
|
||||
return escapeHtml(text).replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function sanitizeId(id) {
|
||||
if (base) return base.sanitizeId(id);
|
||||
return String(id).replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
@@ -81,7 +86,7 @@
|
||||
const currentValue = value !== null && value !== undefined ? value : min;
|
||||
const colorClass = COLOR_CLASSES[color] || COLOR_CLASSES.blue;
|
||||
|
||||
let html = `<div id="${fieldId}_widget" class="slider-widget" data-field-id="${fieldId}" data-prefix="${escapeHtml(prefix)}" data-suffix="${escapeHtml(suffix)}">`;
|
||||
let html = `<div id="${fieldId}_widget" class="slider-widget" data-field-id="${fieldId}" data-prefix="${escapeAttr(prefix)}" data-suffix="${escapeAttr(suffix)}">`;
|
||||
|
||||
// Value display above slider
|
||||
if (showValue) {
|
||||
|
||||
Reference in New Issue
Block a user