From c35769cefb9b1dd5c6bf6c4d4b0d600f0d820384 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Mon, 12 Jan 2026 18:44:06 -0500 Subject: [PATCH] Fix/checkbox save and dynamic duration (#182) * fix: Use plugin.modes instead of manifest.json for available modes - Display controller now checks plugin_instance.modes first before falling back to manifest - This allows plugins to dynamically provide modes based on enabled leagues - Fixes issue where disabled leagues (WNBA, NCAAW) appeared in available modes - Plugins can now control their available modes at runtime based on config * fix: Handle permission errors when removing plugin directories - Added _safe_remove_directory() method to handle permission errors gracefully - Fixes permissions on __pycache__ directories before removal - Updates uninstall_plugin() and install methods to use safe removal - Resolves [Errno 13] Permission denied errors during plugin install/uninstall * refactor: Improve error handling in _safe_remove_directory - Rename unused 'dirs' variable to '_dirs' to indicate intentional non-use - Use logger.exception() instead of logger.error() to preserve stack traces - Add comment explaining 0o777 permissions are acceptable (temporary before deletion) * fix(install): Fix one-shot-install script reliability issues - Install git and curl before attempting repository clone - Add HOME variable validation to prevent path errors - Improve git branch detection (try current branch, main, then master) - Add validation for all directory change operations - Improve hostname command handling in success message - Fix edge cases for better installation success rate * fix(install): Fix IP address display in installation completion message - Replace unreliable pipe-to-while-read loop with direct for loop - Filter out loopback addresses (127.0.0.1, ::1) from display - Add proper message when no non-loopback IPs are found - Fixes blank IP address display issue at end of installation * fix(install): Prevent unintended merges in one-shot-install git pull logic - Use git pull --ff-only for current branch to avoid unintended merges - Use git fetch (not pull) for other branches to check existence without merging - Only update current branch if fast-forward is possible - Provide better warnings when branch updates fail but other branches exist - Prevents risk of merging remote main/master into unrelated working branches * fix(install): Improve IPv6 address handling in installation scripts - Filter out IPv6 link-local addresses (fe80:) in addition to loopback - Properly format IPv6 addresses with brackets in URLs (http://[::1]:5000) - Filter loopback and link-local addresses when selecting IP for display - Prevents invalid IPv6 URLs and excludes non-useful addresses - Fixes: first_time_install.sh and one-shot-install.sh IP display logic * fix: Fix checkbox-group saving and improve dynamic duration calculation - Fix checkbox-group widget saving by setting values directly in plugin_config - Fix element_gap calculation bug in ScrollHelper (was over-calculating width) - Use actual image width instead of calculated width for scroll calculations - Add comprehensive INFO-level logging for dynamic duration troubleshooting - Enhanced scroll completion logging with position and percentage details This fixes issues where checkbox-group values weren't saving correctly and improves dynamic duration calculation accuracy for scrolling content. --------- Co-authored-by: Chuck --- src/common/scroll_helper.py | 37 ++++++++++++++++++++++++++---- web_interface/blueprints/api_v3.py | 14 ++++++----- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/common/scroll_helper.py b/src/common/scroll_helper.py index 8c36b33a..88c63ec5 100644 --- a/src/common/scroll_helper.py +++ b/src/common/scroll_helper.py @@ -136,9 +136,12 @@ class ScrollHelper: return self.cached_image # Calculate total width needed + # Sum of all item widths total_width = sum(img.width for img in content_items) + # Add item gaps between items (not after last item) total_width += item_gap * (len(content_items) - 1) - total_width += element_gap * (len(content_items) * 2 - 1) + # Add element_gap after each item (matches positioning logic) + total_width += element_gap * len(content_items) # Add initial gap before first item total_width += self.display_width @@ -162,7 +165,20 @@ class ScrollHelper: self.cached_image = full_image # Convert to numpy array for fast operations self.cached_array = np.array(full_image) - self.total_scroll_width = total_width + + # Use actual image width instead of calculated width to ensure accuracy + # This fixes cases where width calculation doesn't match actual positioning + actual_image_width = full_image.width + self.total_scroll_width = actual_image_width + + # Log if there's a mismatch (indicating a bug in width calculation) + if actual_image_width != total_width: + self.logger.warning( + "Width calculation mismatch: calculated=%dpx, actual=%dpx (diff=%dpx). " + "Using actual width for scroll calculations.", + total_width, actual_image_width, abs(actual_image_width - total_width) + ) + self.scroll_position = 0.0 self.total_distance_scrolled = 0.0 self.scroll_complete = False @@ -184,7 +200,11 @@ class ScrollHelper: self.duration_buffer, ) - self.logger.debug(f"Created scrolling image: {total_width}x{self.display_height}") + self.logger.info( + "Created scrolling image: %dx%dpx (total_scroll_width=%dpx, %d items, item_gap=%d, element_gap=%d)", + actual_image_width, self.display_height, self.total_scroll_width, + len(content_items), item_gap, element_gap + ) return full_image def update_scroll_position(self) -> None: @@ -248,11 +268,17 @@ class ScrollHelper: if is_complete: # Only log completion once to avoid spam if not self.scroll_complete: - elapsed = current_time - self.scroll_start_time + elapsed = current_time - (self.scroll_start_time or current_time) + scroll_percent = (self.total_distance_scrolled / required_total_distance * 100) if required_total_distance > 0 else 0.0 + position_percent = (self.scroll_position / self.total_scroll_width * 100) if self.total_scroll_width > 0 else 0.0 self.logger.info( - "Scroll cycle COMPLETE: scrolled %.0f/%d px (elapsed %.2fs, target %.2fs)", + "Scroll cycle COMPLETE: scrolled %.0f/%d px (%.1f%%, position=%.0f/%.0f px, %.1f%%) - elapsed %.2fs, target %.2fs", self.total_distance_scrolled, required_total_distance, + scroll_percent, + self.scroll_position, + self.total_scroll_width, + position_percent, elapsed, self.calculated_duration, ) @@ -261,6 +287,7 @@ class ScrollHelper: # Clamp position to prevent wrap when complete if self.scroll_position >= self.total_scroll_width: self.scroll_position = self.total_scroll_width - 1 + self.logger.debug("Clamped scroll position to %d (max=%d)", self.scroll_position, self.total_scroll_width - 1) else: self.scroll_complete = False diff --git a/web_interface/blueprints/api_v3.py b/web_interface/blueprints/api_v3.py index ee98320a..545dcb6a 100644 --- a/web_interface/blueprints/api_v3.py +++ b/web_interface/blueprints/api_v3.py @@ -3353,7 +3353,7 @@ def save_plugin_config(): if key in form_data: del form_data[key] - # Process bracket notation fields and add to form_data as JSON strings + # Process bracket notation fields and set directly in plugin_config # Use JSON encoding instead of comma-join to handle values containing commas import json for base_path, values in bracket_array_fields.items(): @@ -3362,11 +3362,13 @@ def save_plugin_config(): if base_prop and base_prop.get('type') == 'array': # Filter out empty values and sentinel empty strings filtered_values = [v for v in values if v and v.strip()] - # Encode as JSON array string (handles values with commas correctly) - # Empty array (all unchecked) is represented as "[]" - combined_value = json.dumps(filtered_values) - form_data[base_path] = combined_value - logger.debug(f"Processed bracket notation array field {base_path}: {values} -> {combined_value}") + # Set directly in plugin_config (values are already strings, no need to parse) + # Empty array (all unchecked) is represented as [] + _set_nested_value(plugin_config, base_path, filtered_values) + logger.debug(f"Processed bracket notation array field {base_path}: {values} -> {filtered_values}") + # Remove from form_data to avoid double processing + if base_path in form_data: + del form_data[base_path] # Second pass: detect and combine array index fields (e.g., "text_color.0", "text_color.1" -> "text_color" as array) # This handles cases where forms send array fields as indexed inputs