Skip to content

Conversation

@ChuckBuilds
Copy link
Owner

@ChuckBuilds ChuckBuilds commented Jan 15, 2026

Summary by CodeRabbit

  • New Features

    • Global widget framework and base utilities for plugin configuration forms.
    • New interactive widgets: file upload (drag-and-drop, per-item scheduling), checkbox group, custom feeds manager, and color picker.
    • Runtime loading of plugin-provided widgets.
  • Documentation

    • Added a comprehensive Widget Development Guide with API, examples, best practices, and troubleshooting.
  • Chores

    • Added an editor workspace file and a script to update/manage local plugin repositories.

✏️ Tip: You can customize this high-level summary in your review settings.

Chuck added 2 commits January 14, 2026 20:16
- Create core widget registry system (registry.js, base-widget.js)
- Extract existing widgets to separate modules:
  - file-upload.js: Image upload with drag-and-drop, preview, delete, scheduling
  - checkbox-group.js: Multi-select checkboxes for array fields
  - custom-feeds.js: Table-based RSS feed editor with logo uploads
- Implement plugin widget loading system (plugin-loader.js)
- Add comprehensive documentation (widget-guide.md, README.md)
- Include example custom widget (example-color-picker.js)
- Maintain backwards compatibility with existing plugins
- All widget handlers available globally for existing functionality

This enables:
- Reusable UI components for plugin configuration forms
- Third-party plugins to create custom widgets without modifying LEDMatrix
- Modular widget architecture for future enhancements

Existing plugins (odds-ticker, static-image, news) continue to work without changes.
@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

📝 Walkthrough

Walkthrough

Adds a client-side LEDMatrix widget system: a global widget registry and loader, BaseWidget utilities, multiple widgets (file-upload, custom-feeds, checkbox-group, color-picker example), documentation, template script imports, a VSCode workspace file, and a script to update plugin repositories.

Changes

Cohort / File(s) Summary
Registry & Loader
web_interface/static/v3/js/widgets/registry.js, web_interface/static/v3/js/widgets/plugin-loader.js
Introduces window.LEDMatrixWidgets registry (register/get/has/list/render/getValue/setValue/unregister/clear/debug) and plugin-loading APIs (loadPluginWidget, ensureWidget, loadPluginWidgetsFromManifest) with validation, dynamic import attempts, and error handling.
Base utilities
web_interface/static/v3/js/widgets/base-widget.js
Adds BaseWidget class with config validation scaffolding, sanitization helpers, DOM ID utilities, error display, HTML escaping, event notification helpers, and exposes window.BaseWidget.
File upload & scheduling
web_interface/static/v3/js/widgets/file-upload.js
Implements comprehensive file-upload widget registration plus many global helpers for drag/drop/select, upload via fetch, deletion endpoints, hidden-data serialization, dynamic image list DOM, and per-image scheduling UI/state management.
Custom feeds editor
web_interface/static/v3/js/widgets/custom-feeds.js
Adds Custom Feeds widget and globals: getValue/setValue, addCustomFeedRow, removeCustomFeedRow, and handleCustomFeedLogoUpload (uploads to /api/v3/plugins/assets/upload), plus row reindexing and asset preview handling.
Checkbox group widget
web_interface/static/v3/js/widgets/checkbox-group.js
Adds Checkbox Group widget with getValue/setValue and window.updateCheckboxGroupData(fieldId) to aggregate checked items into a hidden JSON input and emit widget-change.
Example widget
web_interface/static/v3/js/widgets/example-color-picker.js
Adds color-picker example widget (render/getValue/setValue) syncing color input, hex input, preview, and emitting widget-change.
Template integration
web_interface/templates/v3/base.html
Injects script tags to load the new widget scripts (registry, base-widget, file-upload, checkbox-group, custom-feeds, plugin-loader) into the base template.
Documentation
docs/widget-guide.md, web_interface/static/v3/js/widgets/README.md
Adds comprehensive Widget Development Guide and README covering registry, widget API, examples (color-picker, slider), best practices, troubleshooting, and migration notes.
Workspace & tooling
LEDMatrix.code-workspace, scripts/update_plugin_repos.py
Adds VSCode workspace file and a Python script to discover and pull updates for plugin git repositories listed in the workspace.
Submodule pointer
plugins/basketball-scoreboard
Updates submodule commit reference only (no functional changes).

Sequence Diagram(s)

sequenceDiagram
    participant Form as Config Form
    participant Registry as LEDMatrixWidgets
    participant PluginLoader as Plugin Loader
    participant Widget as Widget Script
    participant DOM as Browser DOM

    Form->>Registry: has(widgetName)
    Registry-->>Form: true/false

    alt widget not registered
        Form->>PluginLoader: loadPluginWidget(pluginId, widgetName)
        PluginLoader->>DOM: fetch/append widget script (multiple path fallbacks)
        DOM-->>Widget: execute script
        Widget->>Registry: register(widgetName, definition)
        Registry-->>Form: registered
    end

    Form->>Registry: render(widgetName, container, config, value, options)
    Registry->>Widget: render(container, config, value, options)
    Widget->>DOM: create/update DOM
    DOM-->>Form: rendered UI

    User->>DOM: interacts
    DOM->>Widget: event handlers
    Widget->>Registry: getValue(widgetName, fieldId)
    Registry-->>Widget: value returned
    Widget-->>Form: updated value
Loading
sequenceDiagram
    participant User as User
    participant FileWidget as File Upload Widget
    participant Server as Upload API
    participant DataInput as Hidden Data Input
    participant DOM as Browser DOM

    User->>FileWidget: drop/select files
    FileWidget->>FileWidget: validate (count, size, type)
    alt validation fails
        FileWidget->>User: show error/notification
    else
        FileWidget->>FileWidget: show progress UI
        loop each file
            FileWidget->>Server: POST FormData (file + metadata)
            Server-->>FileWidget: response {success, path, id}
            FileWidget->>DataInput: append serialized image entry
            FileWidget->>DOM: build/update image list item + schedule UI
        end
        FileWidget->>User: hide progress, update UI
    end

    User->>FileWidget: edit schedule or delete image
    FileWidget->>DataInput: update serialized state
    FileWidget->>DOM: refresh list/preserve open schedule panels
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hopped in with scripts and a keen little grin,
Registry in paw, new widgets begin.
Uploads and colors and feeds in a row,
Plugins will load and the UI will glow.
Hooray for components — let's make them grow! 🎉

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature/widget registry system' is directly related to the main change, which introduces a comprehensive widget registry system with multiple widget implementations and supporting infrastructure.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.



📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9d8debf and 2bef8ed.

📒 Files selected for processing (1)
  • web_interface/static/v3/js/widgets/README.md
✅ Files skipped from review due to trivial changes (1)
  • web_interface/static/v3/js/widgets/README.md

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🤖 Fix all issues with AI agents
In `@web_interface/static/v3/js/widgets/base-widget.js`:
- Around line 114-121: escapeHtml currently returns String(text) for non-strings
without escaping, which allows unescaped content to reach places like showError
where it's inserted via innerHTML; change escapeHtml to always coerce the input
to a string (e.g., String(text)) and then escape it by assigning that string to
a DOM node's textContent and returning node.innerHTML so every input type is
properly escaped; update the escapeHtml implementation (referenced by escapeHtml
and callers like showError) to perform the coercion first and always use
textContent -> innerHTML for the returned escaped string.

In `@web_interface/static/v3/js/widgets/checkbox-group.js`:
- Around line 60-75: In setValue, values.includes(optionValue) can fail when
types differ (e.g., optionValue is "1" but values contains 1); normalize types
before comparison by converting the incoming values to strings or numbers (or
convert optionValue) so comparisons are consistent—e.g., create a
normalizedValues collection (e.g., values.map(String) or values.map(Number)) and
then use normalizedValues.includes(String(optionValue)) (update references in
setValue and leave updateCheckboxGroupData unchanged).

In `@web_interface/static/v3/js/widgets/custom-feeds.js`:
- Around line 77-95: The current setValue function clears the DOM (tbody for
fieldId) but does not recreate rows, which can erase user data; update setValue
(in CustomFeedsWidget) to be defensive: either throw a clear NotImplementedError
(e.g., throw new Error('CustomFeedsWidget.setValue not implemented')) at the top
when Array.isArray(feeds) is true to avoid mutating the DOM, or implement the
full behavior by using options.fullKey and options.pluginId to build each row
for each feed, creating row elements and appending them to the tbody (use
fieldId to locate `${fieldId}_tbody`), and only clear tbody.innerHTML
immediately before appending the new rows; ensure you reference setValue,
fieldId, feeds, tbody, options.fullKey and options.pluginId in the change.

In `@web_interface/static/v3/js/widgets/example-color-picker.js`:
- Around line 49-76: The inline template in container.innerHTML uses
escapeHtml(currentValue) directly inside the style attribute (background-color:
${escapeHtml(currentValue)}) which allows CSS injection; update the code in
example-color-picker.js where container.innerHTML is built to first validate
currentValue against a strict hex color regex (e.g., /^#[0-9A-Fa-f]{6}$/) and
only insert it into the style and input values if it passes, otherwise
substitute a safe default like "#000000"; keep using escapeHtml for HTML
contexts but ensure the value used in the style attribute is the
validated/sanitized hex and apply the same check when setting the value for the
text input (IDs: ${fieldId}_color, ${fieldId}_hex, and preview element
${fieldId}_preview).

In `@web_interface/static/v3/js/widgets/file-upload.js`:
- Around line 323-387: The list re-render in updateImageList clears any open
schedule editor because callers like updateImageSchedule* fully replace
innerHTML; change updateImageList to support a "preserve open" path (or a
data-only patch) so schedule editors are not closed: detect/open state by
reading the open schedule id from the DOM (the schedule_<id> element or a new
data attribute on img_<id>), then when updating use targeted DOM updates for
changed fields (update hidden input fieldId_images_data and only replace
elements inside img_<id> that changed: thumbnail, text nodes, size/date,
schedule summary) instead of resetting imageList.innerHTML, or restore the saved
open schedule content into schedule_<id> after rendering; update callers
(updateImageSchedule* and similar) to pass the preserve flag or rely on the
DOM-detected open id so editing remains open.
- Around line 335-376: The current use of imageList.innerHTML building strings
(including img.path, img.filename, fieldId, pluginId) and inline onclick
handlers (window.openImageSchedule, window.deleteUploadedImage) creates a stored
XSS risk; replace the HTML string construction in the imageList.innerHTML block
with programmatic DOM creation: create container elements (matching the same
classes/structure and id pattern "img_<...>"), set textContent/attributes (use
the existing escapeHtml or prefer textContent) for filenames and summaries, set
img.src via DOM property (not via innerHTML), store identifiers in data-*
attributes on the element, and attach event listeners with addEventListener that
read those data attributes and call
window.openImageSchedule/window.deleteUploadedImage; ensure any user-controlled
values used in ids are sanitized to safe token (as currently done with replace)
and never injected into inline JS strings.
- Around line 233-239: When filtering out the deleted file in file-upload.js,
normalize types so string fileId matches numeric file.id; modify the filter
inside the code that builds newFiles (which currently references
currentFiles.filter and compares (file.id || file.category_name) !== fileId) to
coerce both sides to the same type (e.g., String(file.id) or
String(file.category_name) compared to String(fileId)) or use a loose equality
on normalized values, then call window.updateImageList(fieldId, newFiles) as
before; update the comparison near currentFiles, fileId, file.id,
file.category_name, getCurrentImages and updateImageList to ensure proper
removal of the deleted item.
- Around line 489-495: openImageSchedule uses the raw imageId when querying the
DOM ("schedule_${imageId || imageIdx}") which can fail if IDs contain special
chars; align it with updateImageList's sanitization by computing the same
sanitizedId (e.g. const sanitized = (imageId ||
imageIdx).toString().replace(/[^a-z0-9-_]/gi, '') or call the existing sanitizer
if one exists) and use document.getElementById(`schedule_${sanitized}`) so the
lookup matches updateImageList's DOM IDs.
- Around line 96-139: The max-files check in window.handleFiles runs before
filtering and doesn't honor uploadConfig.allowed_types; move the maxFiles
enforcement so it occurs after you build validFiles, and derive allowedTypes
from uploadConfig.allowed_types (falling back to the existing default list) when
validating each file. Specifically: in handleFiles, use
uploadConfig.allowed_types ||
['image/png','image/jpeg','image/jpg','image/bmp','image/gif'] to validate
file.type (and still handle 'json' case), collect validFiles first, then check
if currentFiles.length + validFiles.length > maxFiles and notify or trim the
validFiles array accordingly before proceeding with uploads. Ensure you
reference maxFiles, allowedTypes (or uploadConfig.allowed_types), currentFiles
and validFiles in the updated logic.

In `@web_interface/static/v3/js/widgets/README.md`:
- Around line 293-300: Extend the Security section to advise sanitizing
identifiers used as element IDs and in CSS selectors (e.g., fieldId) in addition
to escaping HTML; instruct authors to validate and normalize IDs (allow only a
safe subset like [A-Za-z0-9_-], trim/replace invalid chars, or prefix IDs) and
to use DOM-safe APIs (textContent, escapeHtml) and CSS.escape or a sanitizeId
helper before using values in querySelector, getElementById, or setting id
attributes to prevent selector injection/XSS; reference existing functions/terms
like escapeHtml, textContent, and the identifier fieldId so reviewers can update
code to call a sanitizeId routine where IDs are generated or used.
- Around line 120-203: The examples use fieldId directly in the template and
querySelector causing XSS/selector-injection; add a sanitization helper (e.g.,
sanitizeId or escapeCssIdentifier) and use it in render, getValue, and setValue
when building the id attribute and when querying the DOM (replace occurrences of
`${fieldId}_input` and `#${fieldId}_input`), ensuring the helper strips/encodes
unsafe characters or produces a safe DOM id and a safe CSS selector; keep
escapeHtml for values but never interpolate raw fieldId into HTML or selectors
without running it through the new sanitizer.
♻️ Duplicate comments (2)
web_interface/static/v3/js/widgets/README.md (1)

321-451: Apply fieldId sanitization to all widget examples.

The color picker and slider examples have the same XSS and selector injection vulnerabilities as the basic widget example:

Color Picker Widget:

  • Lines 333, 337: Unsanitized fieldId in element IDs
  • Lines 344-345, 361-366: Unsafe querySelector usage

Slider Widget:

  • Lines 402, 410: Unsanitized fieldId in element IDs
  • Lines 416-417, 426, 431-437: Unsafe querySelector usage

Apply the same sanitization approach from the previous comment to these examples.

docs/widget-guide.md (1)

117-200: Same fieldId sanitization issue as in the web_interface README.

This example has the same XSS and selector injection vulnerabilities:

  • Line 155: id="${fieldId}_input" - unsanitized fieldId in HTML
  • Lines 172, 180: querySelector('#${fieldId}_input') - unsafe selector usage

While this example includes an inline escapeHtml function (lines 146-150), it only sanitizes the value, not the fieldId.

Apply the same sanitization fix as recommended for the other README file.

🧹 Nitpick comments (6)
web_interface/static/v3/js/widgets/README.md (1)

476-485: Clarify the relationship between server-side and client-side rendering.

The migration section states "server-side rendering remains the primary method" but the entire guide focuses on client-side widget creation with LEDMatrixWidgets.register(). This could confuse developers about when and how to use each approach.

Consider adding a section that explicitly explains:

  1. When widgets are server-side rendered (existing core widgets)
  2. When client-side rendering is used (custom plugin widgets)
  3. The current capabilities vs. future enhancements
  4. How the two approaches coexist
docs/widget-guide.md (1)

1-347: Consider consolidating or clearly differentiating the two widget guides.

There are now two widget documentation files with significant overlap:

  1. web_interface/static/v3/js/widgets/README.md (491 lines)
  2. docs/widget-guide.md (347 lines)

While they have some differences (this file includes "Used by" references and implementation status, while the README has more detailed examples), the duplication could lead to:

  • Maintenance burden (updating both files)
  • Inconsistencies over time
  • Confusion about which to reference

Consider one of these approaches:

  1. Single source of truth: Keep only one comprehensive guide and remove the other
  2. Clear differentiation: Make one a "Developer Reference" (comprehensive API docs) and the other a "Quick Start Guide" (getting started tutorial)
  3. Cross-reference: Have one file be the main guide and the other reference it with "See [other file] for complete documentation"
web_interface/static/v3/js/widgets/base-widget.js (1)

51-56: Clarify sanitizeValue’s security guarantees.

This only strips <script> tags; it won’t neutralize event handlers or unsafe URLs if values are later injected into HTML. Consider documenting the limitation to avoid a false sense of XSS protection.

💡 Suggested clarification
-            // Basic XSS prevention
+            // NOTE: This only strips <script> tags. If values are injected into HTML,
+            // callers must escape or sanitize appropriately.
             return value.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
web_interface/static/v3/js/widgets/plugin-loader.js (1)

85-92: Misleading comment: no core widget loading logic follows.

The comment at line 87 states "Continue to check if it's a core widget" but the code simply returns false. Consider removing or updating the comment to reflect actual behavior.

📝 Suggested fix
             } catch (error) {
                 console.warn(`[PluginWidgetLoader] Could not load widget ${widgetName} from plugin ${pluginId}:`, error);
-                // Continue to check if it's a core widget
             }
         }

-        // Widget not found
+        // Widget not found in registry and could not be loaded
         return false;
web_interface/static/v3/js/widgets/example-color-picker.js (1)

167-181: Inconsistent event dispatch target.

This widget dispatches widget-change on document (line 174), while checkbox-group.js dispatches on the hiddenInput element. Consider using a consistent pattern across widgets for easier event handling.

Additionally, line 177's selector input[name*="${fieldId}"] uses a substring match which could match unintended inputs if fieldId is a common substring.

web_interface/static/v3/js/widgets/file-upload.js (1)

157-183: Handle non-OK and non-JSON upload responses.

response.json() will throw on HTML/empty error bodies and hides HTTP status. A small guard improves resilience and user-facing error quality.

♻️ Suggested hardening
-            const data = await response.json();
+            if (!response.ok) {
+                throw new Error(`Upload failed (${response.status})`);
+            }
+            let data;
+            try {
+                data = await response.json();
+            } catch {
+                throw new Error('Upload failed (invalid JSON response)');
+            }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3b8910a and bf2978d.

📒 Files selected for processing (11)
  • docs/widget-guide.md
  • plugins/basketball-scoreboard
  • web_interface/static/v3/js/widgets/README.md
  • web_interface/static/v3/js/widgets/base-widget.js
  • web_interface/static/v3/js/widgets/checkbox-group.js
  • web_interface/static/v3/js/widgets/custom-feeds.js
  • web_interface/static/v3/js/widgets/example-color-picker.js
  • web_interface/static/v3/js/widgets/file-upload.js
  • web_interface/static/v3/js/widgets/plugin-loader.js
  • web_interface/static/v3/js/widgets/registry.js
  • web_interface/templates/v3/base.html
🧰 Additional context used
🧬 Code graph analysis (6)
web_interface/static/v3/js/widgets/base-widget.js (3)
web_interface/static/v3/js/widgets/custom-feeds.js (1)
  • container (310-310)
web_interface/static/v3/js/widgets/example-color-picker.js (3)
  • div (44-44)
  • fieldId (39-39)
  • event (169-173)
web_interface/static/v3/js/widgets/checkbox-group.js (1)
  • event (108-112)
web_interface/static/v3/js/widgets/plugin-loader.js (1)
web_interface/static/v3/js/widgets/file-upload.js (2)
  • pluginId (98-98)
  • pluginId (333-333)
web_interface/static/v3/js/widgets/checkbox-group.js (3)
web_interface/static/v3/js/widgets/custom-feeds.js (1)
  • container (310-310)
web_interface/static/v3/js/widgets/example-color-picker.js (3)
  • fieldId (39-39)
  • hiddenInput (177-177)
  • event (169-173)
web_interface/static/v3/js/widgets/file-upload.js (2)
  • hiddenInput (307-307)
  • hiddenInput (324-324)
web_interface/static/v3/js/widgets/custom-feeds.js (1)
web_interface/static/v3/js/widgets/file-upload.js (12)
  • pluginId (98-98)
  • pluginId (333-333)
  • notifyFn (107-107)
  • notifyFn (116-116)
  • notifyFn (124-124)
  • notifyFn (132-132)
  • notifyFn (173-173)
  • notifyFn (176-176)
  • notifyFn (181-181)
  • notifyFn (241-241)
  • notifyFn (244-244)
  • notifyFn (249-249)
web_interface/static/v3/js/widgets/file-upload.js (1)
web_interface/static/v3/js/widgets/checkbox-group.js (2)
  • hiddenInput (43-43)
  • hiddenInput (89-89)
web_interface/static/v3/js/widgets/example-color-picker.js (2)
web_interface/static/v3/js/widgets/file-upload.js (3)
  • escapeHtml (341-345)
  • escapeHtml (510-514)
  • div (511-511)
web_interface/static/v3/js/widgets/checkbox-group.js (1)
  • event (108-112)
🔇 Additional comments (25)
plugins/basketball-scoreboard (1)

1-1: No action needed. The basketball-scoreboard submodule has not been updated in this PR—it remains at commit d626e538c0fbfcb92cbc3f1c351ffe6d2c6c00bb in both HEAD and the current state. No changes to the submodule were found.

Likely an incorrect or invalid review comment.

web_interface/static/v3/js/widgets/README.md (1)

1-92: LGTM! Clear and comprehensive documentation.

The overview and core widgets documentation is well-structured with clear schema examples and feature descriptions. This provides excellent guidance for developers.

docs/widget-guide.md (2)

1-116: LGTM! Clear documentation with helpful plugin references.

The core widgets documentation includes "Used by:" fields that provide valuable context about which plugins already use each widget. This helps developers understand real-world usage patterns.


324-342: Excellent implementation status section!

This "Current Implementation Status" section clearly explains:

  • What's been completed (Phase 1)
  • Current behavior (server-side rendering)
  • Future enhancements (client-side rendering)
  • Backwards compatibility guarantees

This clarity is valuable and helps set appropriate expectations for developers.

web_interface/templates/v3/base.html (1)

4820-4826: LGTM: widget scripts are loaded in dependency order.

Registry and base classes load before widget implementations, and the defer usage keeps the page non-blocking.

web_interface/static/v3/js/widgets/plugin-loader.js (3)

1-17: LGTM!

Good defensive initialization pattern - checking for the registry existence before attempting to extend it.


25-65: LGTM!

The multiple path fallback pattern with proper error aggregation is well implemented. The warning when a script loads but doesn't register the widget is helpful for debugging plugin development issues.


101-126: LGTM!

Good error handling - failures for individual widgets are logged but don't block loading of other widgets from the manifest.

web_interface/static/v3/js/widgets/checkbox-group.js (3)

1-17: LGTM!

Consistent initialization pattern matching other widget files.


42-53: LGTM!

Proper error handling for JSON parsing with a safe fallback to an empty array.


87-114: LGTM!

Clean implementation that aggregates checked values and dispatches a properly structured widget-change event.

web_interface/static/v3/js/widgets/example-color-picker.js (2)

1-22: LGTM!

Good documentation explaining how to use this as an example widget for plugin developers.


129-156: LGTM!

Good validation in setValue to reject invalid hex values, and appropriate null handling in getValue.

web_interface/static/v3/js/widgets/registry.js (5)

1-17: LGTM!

Clean module structure with Map-based storage for widgets and handlers.


30-55: LGTM!

Solid validation for required properties. Note that re-registering a widget with the same name silently overwrites the previous definition, which is reasonable for hot-reload scenarios.


101-131: LGTM!

Robust container resolution with proper validation and error handling.


139-175: LGTM!

Proper delegation to widget-specific getValue/setValue with appropriate fallback behavior and error handling.


177-217: LGTM!

Useful cleanup methods and debug introspection for development.

web_interface/static/v3/js/widgets/custom-feeds.js (5)

1-17: LGTM!

Consistent initialization pattern.


42-70: LGTM!

Clean extraction of feed data from DOM with proper null handling for logo fields.


109-218: LGTM!

Good use of DOM API for element creation, avoiding innerHTML with user data. Proper max items validation and required field attributes.


224-260: LGTM!

Proper row removal with reindexing logic. The regex pattern /\.\d+\./ correctly handles the expected name format.


291-377: LGTM!

Good DOM-based construction avoiding innerHTML with server response data. Proper error handling with user notification fallback.

web_interface/static/v3/js/widgets/file-upload.js (2)

13-61: Registry guard + widget registration look solid.

Nice defensive check and clean registry integration.


306-327: No action required—hidden input ID suffix is already aligned.

Templates consistently use ${fieldId}_images_data for file upload widgets (matching file-upload.js), while other widgets use ${fieldId}_data for different field types. There is no suffix mismatch; uploads will persist correctly.

Likely an incorrect or invalid review comment.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

- base-widget.js: Fix escapeHtml to always escape (coerce to string first)
- base-widget.js: Add sanitizeId helper for safe DOM ID usage
- base-widget.js: Use DOM APIs in showError instead of innerHTML
- checkbox-group.js: Normalize types in setValue for consistent comparison
- custom-feeds.js: Implement setValue with full row creation logic
- example-color-picker.js: Validate hex colors before using in style attributes
- file-upload.js: Replace innerHTML with DOM creation to prevent XSS
- file-upload.js: Preserve open schedule editors when updating image list
- file-upload.js: Normalize types when filtering deleted files
- file-upload.js: Sanitize imageId in openImageSchedule and all schedule handlers
- file-upload.js: Fix max-files check order and use allowed_types from config
- README.md: Add security guidance for ID sanitization in examples
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@scripts/update_plugin_repos.py`:
- Around line 19-40: Update load_workspace_plugins to open WORKSPACE_FILE with
explicit encoding (e.g., open(WORKSPACE_FILE, 'r', encoding='utf-8')) and wrap
the file read and json.load call in a try/except that catches FileNotFoundError,
PermissionError, and json.JSONDecodeError; for each catch, emit a clear
user-friendly message (via logging or print) that includes WORKSPACE_FILE and
the exception details, and return an empty plugins list or re-raise a wrapped
exception as appropriate. Ensure references to WORKSPACE_FILE, GITHUB_DIR, and
the plugins list remain intact so the subsequent loop over
workspace.get('folders', []) and creation of plugin_path using GITHUB_DIR /
plugin_name continues unchanged.
- Around line 49-75: The git fetch result isn't checked and the broad except
hides specific subprocess issues; change the first subprocess.run to store in a
distinct variable (e.g., fetch_result), verify fetch_result.returncode == 0
before continuing (on non-zero print fetch_result.stderr and return False),
avoid reusing the name "result" for both fetch and pull (use pull_result for the
git pull call and check pull_result.returncode and output), and replace the bare
"except Exception as e" with a specific catch like "except
(subprocess.SubprocessError, OSError) as e" so only relevant errors from
subprocess/os are handled; reference the existing git fetch subprocess.run, git
pull subprocess.run, branch_result/current_branch, and the try/except block in
scripts/update_plugin_repos.py.

In `@web_interface/static/v3/js/widgets/base-widget.js`:
- Around line 173-180: The notify method can throw when called without a type
because it calls type.toUpperCase(); update notify (and its fallback logging) to
guard against undefined/null type by normalizing type (e.g., default to 'info'
or cast to string) before calling toUpperCase(); locate the notify method and
its call to getNotificationFunction() in base-widget.js and ensure the fallback
console.log uses the normalizedType (or a safe conditional) instead of calling
toUpperCase() directly on the possibly undefined type.

In `@web_interface/static/v3/js/widgets/file-upload.js`:
- Around line 630-660: The template injects unsanitized fieldId into inline
onchange handlers (e.g., in scheduleContainer.innerHTML for elements with IDs
schedule_enabled_${sanitizedId} and schedule_mode_${sanitizedId}), allowing JS
injection; remove inline handlers and instead set safe data-* attributes (e.g.,
data-field-id, data-image-idx) on those elements when building innerHTML, then
after assigning scheduleContainer.innerHTML query for those IDs
(schedule_enabled_${sanitizedId}, schedule_mode_${sanitizedId},
schedule_options_${sanitizedId}) and attach event listeners with
addEventListener that call the existing functions
window.toggleImageScheduleEnabled and window.updateImageScheduleMode using the
values from the data attributes (fieldId, sanitizedId, imageIdx); ensure
sanitizedId is used in DOM IDs and do not interpolate raw fieldId into HTML
strings.
- Around line 520-535: The UI always shows a hardcoded "(PNG, JPG, GIF, BMP)"
even though hideUploadProgress reads allowedTypes from uploadConfig; update
hideUploadProgress to derive a user-friendly extension list from allowedTypes
(e.g., map MIME types like "image/png" -> "PNG" and handle common file
extensions) and inject that generated string into the dropZone.innerHTML instead
of the hardcoded text; keep the existing fallback to the default allowedTypes
array and ensure dropZone and fieldId are used as before.

In `@web_interface/static/v3/js/widgets/README.md`:
- Around line 349-411: The Color Picker example registered under
register('color-picker') uses fieldId directly in render, getValue, setValue and
handlers.onChange; replace those usages with a sanitized identifier (e.g., call
the documented sanitizeId(fieldId) once in render and store as sanitizedFieldId)
and use sanitizedFieldId when building element IDs and querySelector strings,
and also use the same sanitizedFieldId in getValue, setValue, and
handlers.onChange to match the documentation's sanitization pattern.
🧹 Nitpick comments (5)
scripts/update_plugin_repos.py (1)

8-12: Unused import os.

The os module is imported but never used in this script. Consider removing it.

 import json
-import os
 import subprocess
 import sys
 from pathlib import Path
web_interface/static/v3/js/widgets/base-widget.js (1)

51-58: Regex-based XSS prevention is easily bypassed.

The script-tag regex is fragile and can be circumvented with variations like <script >, <SCRIPT>, event handlers (onerror), or other injection vectors. Consider using a proper sanitization library (e.g., DOMPurify) or DOM-based sanitization if actual HTML sanitization is needed, or document that this is only a minimal safeguard.

💡 Alternative approach using DOM-based sanitization
         sanitizeValue(value) {
-            // Base implementation - widgets should override for specific needs
-            if (typeof value === 'string') {
-                // Basic XSS prevention
-                return value.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
-            }
-            return value;
+            // Base implementation - widgets should override for specific needs
+            if (typeof value === 'string') {
+                // Strip all HTML tags for basic XSS prevention
+                const div = document.createElement('div');
+                div.textContent = value;
+                return div.textContent;
+            }
+            return value;
         }
web_interface/static/v3/js/widgets/checkbox-group.js (1)

111-118: Minor inconsistency in event dispatch target.

This widget dispatches widget-change on the hiddenInput element, while BaseWidget.triggerChange dispatches on document. Both work due to event bubbling, but consider aligning the pattern for consistency across widgets.

web_interface/static/v3/js/widgets/file-upload.js (1)

488-492: Preserved schedule content restoration uses innerHTML.

Line 490 restores preservedScheduleContent via innerHTML. While this preserves UX by keeping the schedule editor open, it could re-inject XSS if the schedule content was previously compromised. Consider rebuilding the schedule UI from data instead of preserving raw HTML.

web_interface/static/v3/js/widgets/custom-feeds.js (1)

370-375: Regex for reindexing may match unintended positions.

The pattern /\.\d+\./ on line 374 replaces any numeric segment in the name, which could incorrectly modify names like feeds.v2.0.name (replacing 2 instead of 0). Consider a more precise approach.

🔧 Proposed fix: Match index at end of second-to-last segment
                 r.querySelectorAll('input, button').forEach(input => {
                     const name = input.getAttribute('name');
                     if (name) {
-                        // Replace pattern like "feeds.custom_feeds.0.name" with "feeds.custom_feeds.1.name"
-                        input.setAttribute('name', name.replace(/\.\d+\./, `.${index}.`));
+                        // Replace index before final segment: "feeds.custom_feeds.0.name" -> "feeds.custom_feeds.1.name"
+                        // Match only the last numeric segment followed by a dot and property name
+                        input.setAttribute('name', name.replace(/\.(\d+)\.([^.]+)$/, `.${index}.$2`));
                     }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bf2978d and cbe747b.

📒 Files selected for processing (8)
  • LEDMatrix.code-workspace
  • scripts/update_plugin_repos.py
  • web_interface/static/v3/js/widgets/README.md
  • web_interface/static/v3/js/widgets/base-widget.js
  • web_interface/static/v3/js/widgets/checkbox-group.js
  • web_interface/static/v3/js/widgets/custom-feeds.js
  • web_interface/static/v3/js/widgets/example-color-picker.js
  • web_interface/static/v3/js/widgets/file-upload.js
✅ Files skipped from review due to trivial changes (1)
  • LEDMatrix.code-workspace
🚧 Files skipped from review as they are similar to previous changes (1)
  • web_interface/static/v3/js/widgets/example-color-picker.js
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (.cursor/rules/coding-standards.mdc)

**/*.py: Prefer clear, readable code over clever optimizations (Simplicity First principle)
Make intentions clear through naming and structure (Explicit over Implicit principle)
Validate inputs and handle errors early (Fail Fast principle)
Use docstrings for classes and complex functions
Use PascalCase for class names (e.g., NHLRecentManager)
Use snake_case for function and variable names (e.g., fetch_game_data)
Use UPPER_SNAKE_CASE for constants (e.g., ESPN_NHL_SCOREBOARD_URL)
Use leading underscore for private methods (e.g., _fetch_data)
Use structured logging with context (e.g., [NHL Recent]) for logging messages
Catch specific exceptions, not bare except: statements
Provide user-friendly error messages that explain what went wrong and potential solutions
Implement graceful degradation to continue operation when non-critical features fail
Use type hints for function parameters and return values
Validate required configuration fields on initialization
Provide sensible default values in code rather than in configuration files
Handle different deployment contexts with environment awareness in code

**/*.py: Code must run on Raspberry Pi, not Windows development machine
Optimize code for Raspberry Pi's limited RAM and CPU capabilities
Clean up resources regularly to manage memory effectively
Implement comprehensive logging for remote debugging on Raspberry Pi
Provide clear error messages for troubleshooting

Files:

  • scripts/update_plugin_repos.py
🧠 Learnings (1)
📚 Learning: 2025-12-27T19:16:24.801Z
Learnt from: CR
Repo: ChuckBuilds/LEDMatrix PR: 0
File: .cursorrules:0-0
Timestamp: 2025-12-27T19:16:24.801Z
Learning: Install pre-push git hook from LEDMatrix repo to handle automatic version bumping: copy from `scripts/git-hooks/pre-push-plugin-version` to plugin repo `.git/hooks/pre-push`

Applied to files:

  • scripts/update_plugin_repos.py
🧬 Code graph analysis (1)
web_interface/static/v3/js/widgets/base-widget.js (2)
web_interface/static/v3/js/widgets/file-upload.js (10)
  • notifyFn (114-114)
  • notifyFn (122-122)
  • notifyFn (129-129)
  • notifyFn (140-140)
  • notifyFn (177-177)
  • notifyFn (180-180)
  • notifyFn (185-185)
  • notifyFn (249-249)
  • notifyFn (252-252)
  • notifyFn (257-257)
web_interface/static/v3/js/widgets/checkbox-group.js (1)
  • event (112-116)
🪛 Ruff (0.14.11)
scripts/update_plugin_repos.py

51-51: Starting a process with a partial executable path

(S607)


55-55: Starting a process with a partial executable path

(S607)


60-60: subprocess call: check for execution of untrusted input

(S603)


60-60: Starting a process with a partial executable path

(S607)


73-73: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (8)
scripts/update_plugin_repos.py (1)

78-105: LGTM!

The main function has clear flow: discover plugins, update each, and report results with appropriate exit codes. The summary output is helpful for users.

web_interface/static/v3/js/widgets/base-widget.js (1)

122-128: LGTM - escapeHtml now properly handles all input types.

The function correctly coerces input to string before escaping, addressing the previous review concern.

web_interface/static/v3/js/widgets/checkbox-group.js (1)

60-79: LGTM - Type coercion issue addressed.

The setValue function now normalizes values to strings (line 67) and uses string comparison (line 74), resolving the previous type mismatch concern.

web_interface/static/v3/js/widgets/file-upload.js (2)

96-143: LGTM - Max-file validation now runs after filtering.

The validation logic correctly builds validFiles first (lines 111-136), then checks the max files limit (lines 139-143), addressing the previous review concern.


237-247: LGTM - ID type normalization implemented.

The comparison now uses String() coercion on both sides (lines 240-243), resolving the previous type mismatch concern.

web_interface/static/v3/js/widgets/custom-feeds.js (2)

78-93: LGTM - Defensive approach implemented for setValue.

The function now throws an explicit error when required options are missing (lines 85-87), preventing silent data loss. The full implementation properly rebuilds rows from data.


436-494: LGTM - Safe DOM construction for logo cell.

The logo upload handler uses programmatic DOM creation (document.createElement, textContent, appendChild) instead of innerHTML, preventing XSS vulnerabilities.

web_interface/static/v3/js/widgets/README.md (1)

314-328: LGTM - Security section now includes comprehensive identifier sanitization guidance.

The section addresses the previous review concern by documenting the need to sanitize identifiers used in selectors and IDs, with specific examples and allowed character sets.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

- scripts/update_plugin_repos.py: Add explicit UTF-8 encoding and proper error handling for file operations
- scripts/update_plugin_repos.py: Fix git fetch/pull error handling with returncode checks and specific exception types
- base-widget.js: Guard notify method against undefined/null type parameter
- file-upload.js: Remove inline handlers from schedule template, use addEventListener with data attributes
- file-upload.js: Update hideUploadProgress to show dynamic file types from config instead of hardcoded list
- README.md: Update Color Picker example to use sanitized fieldId throughout
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@web_interface/static/v3/js/widgets/README.md`:
- Around line 426-492: The Slider example uses fieldId directly in DOM IDs and
selectors (in render, getValue, setValue, and handlers.onChange) which breaks
the documented sanitization pattern; fix it by computing a sanitized identifier
(e.g., const safeId = sanitizeFieldId(fieldId) or similar helper used in the
Color Picker example) at the top of render/getValue/setValue and use safeId for
all generated IDs (#${safeId}_slider, #${safeId}_value) and query selectors and
event payloads so that every reference to the DOM uses the sanitized identifier
consistently.
🧹 Nitpick comments (4)
scripts/update_plugin_repos.py (3)

8-12: Remove unused os import.

The os module is imported but never used in the script.

Proposed fix
 import json
-import os
 import subprocess
 import sys
 from pathlib import Path

19-50: Well-structured error handling.

The function properly handles FileNotFoundError, PermissionError, and json.JSONDecodeError with clear user-friendly messages, and uses explicit UTF-8 encoding. The plugin filtering logic is correct.

Consider adding type hints per coding guidelines:

def load_workspace_plugins() -> list[dict]:

53-89: Solid git operation handling with proper error checks.

The function correctly:

  • Validates the .git directory exists before operations
  • Checks fetch_result.returncode before proceeding to pull
  • Catches specific exceptions (subprocess.SubprocessError, OSError) per guidelines
  • Provides clear status messages for all outcomes

The Ruff S607 warnings about partial executable paths (git) are acceptable for developer utility scripts where git is expected to be in PATH.

Consider adding type hints:

def update_repo(repo_path: Path) -> bool:
web_interface/static/v3/js/widgets/file-upload.js (1)

557-563: Consider handling edge case for invalid byte values.

The formatFileSize function handles zero correctly, but negative or NaN byte values would cause Math.log to return NaN, resulting in an undefined size unit. This is unlikely in practice but could occur with corrupted data.

🔧 Optional defensive check
 window.formatFileSize = function(bytes) {
-    if (bytes === 0) return '0 B';
+    if (!bytes || bytes <= 0) return '0 B';
     const k = 1024;
     const sizes = ['B', 'KB', 'MB'];
     const i = Math.floor(Math.log(bytes) / Math.log(k));
-    return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
+    return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + (sizes[i] || 'B');
 };
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cbe747b and 9d8debf.

📒 Files selected for processing (4)
  • scripts/update_plugin_repos.py
  • web_interface/static/v3/js/widgets/README.md
  • web_interface/static/v3/js/widgets/base-widget.js
  • web_interface/static/v3/js/widgets/file-upload.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • web_interface/static/v3/js/widgets/base-widget.js
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (.cursor/rules/coding-standards.mdc)

**/*.py: Prefer clear, readable code over clever optimizations (Simplicity First principle)
Make intentions clear through naming and structure (Explicit over Implicit principle)
Validate inputs and handle errors early (Fail Fast principle)
Use docstrings for classes and complex functions
Use PascalCase for class names (e.g., NHLRecentManager)
Use snake_case for function and variable names (e.g., fetch_game_data)
Use UPPER_SNAKE_CASE for constants (e.g., ESPN_NHL_SCOREBOARD_URL)
Use leading underscore for private methods (e.g., _fetch_data)
Use structured logging with context (e.g., [NHL Recent]) for logging messages
Catch specific exceptions, not bare except: statements
Provide user-friendly error messages that explain what went wrong and potential solutions
Implement graceful degradation to continue operation when non-critical features fail
Use type hints for function parameters and return values
Validate required configuration fields on initialization
Provide sensible default values in code rather than in configuration files
Handle different deployment contexts with environment awareness in code

**/*.py: Code must run on Raspberry Pi, not Windows development machine
Optimize code for Raspberry Pi's limited RAM and CPU capabilities
Clean up resources regularly to manage memory effectively
Implement comprehensive logging for remote debugging on Raspberry Pi
Provide clear error messages for troubleshooting

Files:

  • scripts/update_plugin_repos.py
🧠 Learnings (1)
📚 Learning: 2025-12-27T19:16:24.801Z
Learnt from: CR
Repo: ChuckBuilds/LEDMatrix PR: 0
File: .cursorrules:0-0
Timestamp: 2025-12-27T19:16:24.801Z
Learning: Install pre-push git hook from LEDMatrix repo to handle automatic version bumping: copy from `scripts/git-hooks/pre-push-plugin-version` to plugin repo `.git/hooks/pre-push`

Applied to files:

  • scripts/update_plugin_repos.py
🪛 Ruff (0.14.11)
scripts/update_plugin_repos.py

61-61: Starting a process with a partial executable path

(S607)


69-69: Starting a process with a partial executable path

(S607)


74-74: subprocess call: check for execution of untrusted input

(S603)


74-74: Starting a process with a partial executable path

(S607)

🔇 Additional comments (13)
scripts/update_plugin_repos.py (1)

92-119: LGTM!

The main function correctly orchestrates the workflow with proper exit codes (0 for success, 1 for failures or no plugins found) and provides clear progress feedback to the user.

web_interface/static/v3/js/widgets/README.md (4)

1-113: Documentation structure looks good.

The overview, available core widgets documentation, and usage instructions are well-organized and provide clear guidance for plugin developers.


139-220: Custom widget example demonstrates proper sanitization.

The example correctly shows sanitization of fieldId in render, getValue, and setValue methods, and includes a reusable sanitizeId helper. This addresses the security concerns from previous reviews.


312-328: Security best practices section is comprehensive.

The security guidance now properly covers HTML escaping, input validation, value sanitization, and importantly, identifier sanitization with detailed examples for CSS selectors and HTML attributes.


349-424: Color Picker example correctly applies sanitization.

The example now properly sanitizes fieldId in all methods (render, getValue, setValue) before using it in DOM IDs and selectors, aligning with the documented security best practices.

web_interface/static/v3/js/widgets/file-upload.js (8)

10-61: Widget registration structure is well-organized.

The IIFE pattern with registry check, placeholder render for server-side rendered widgets, and delegation to window-scoped functions maintains backwards compatibility while integrating with the new registry system.


96-197: File validation and upload logic is correct.

The implementation properly validates file types against allowedTypes from config, checks max files after filtering valid files, and handles upload errors appropriately. The shadowed currentFiles variable on line 171 is harmless since the re-fetched value is intentionally fresher.


237-247: ID type normalization implemented correctly.

The filter now properly normalizes both fileId and file.id/file.category_name to strings before comparison, ensuring deletion works regardless of whether IDs arrive as numbers or strings.


267-307: Config retrieval logic handles common cases.

The schema traversal logic works for the documented widget patterns. The underscore-to-dot conversion (line 274) assumes field IDs use underscores to represent dot-separated paths, which aligns with the server-side rendering conventions.


332-498: DOM creation approach properly mitigates XSS risks.

The implementation uses programmatic DOM element creation for user-controlled data (filenames, paths, IDs), sanitizes IDs for element attributes, and attaches event listeners via addEventListener instead of inline handlers. The schedule preservation correctly restores self-generated HTML.


520-550: File type display now reflects configuration.

The implementation dynamically generates the extension list from allowedTypes with a sensible mapping and deduplication, addressing the previous concern about hardcoded file type text.


615-754: Schedule editor properly implements security mitigations.

The implementation sanitizes imageId for DOM lookups (line 621), uses escapeHtml for data attributes containing fieldId, and attaches event listeners programmatically instead of using inline handlers. This addresses the previous concerns about XSS and selector injection.


837-963: Schedule handlers consistently apply ID sanitization.

All schedule update handlers properly sanitize imageId before DOM queries, maintaining consistency with updateImageList's ID generation. The schedule preservation logic in updateImageList (lines 345-365, 488-492) ensures the UI doesn't collapse during edits.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

- Add sanitizeId helper to Slider example render, getValue, and setValue methods
- Use sanitizedFieldId for all DOM IDs and query selectors
- Maintain consistency with Color Picker example pattern
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants