Commit Graph

1757 Commits

Author SHA1 Message Date
Chuck
3975940cff Add light/dark mode toggle and fix log readability (#243)
* feat(web): add light/dark mode toggle and fix log readability

Add a theme toggle button (moon/sun icon) to the header that switches
between light and dark mode. Theme preference persists in localStorage
and falls back to the OS prefers-color-scheme setting.

The implementation uses a data-theme attribute on <html> with CSS
overrides, so all 13 partial templates and 20+ widget JS files get
dark mode support without any modifications — only 3 files changed.

Also fixes log timestamp readability: text-gray-500 had ~3.5:1 contrast
ratio against the dark log background, now uses text-gray-400 (~5.3:1)
which passes WCAG AA in both light and dark mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(web): address dark mode review — accessibility, robustness, and code quality

- WCAG touch target: enforce 44×44px minimum on theme toggle button
  with display:inline-flex centering
- Accessibility: add type="button", aria-pressed (dynamically updated),
  aria-hidden on decorative icons, and contextual aria-label/title that
  reflects current state ("Switch to light/dark mode")
- Robustness: wrap all localStorage and matchMedia calls in try/catch
  with fallbacks for private browsing and restricted contexts; use
  addListener fallback for older browsers lacking addEventListener
- Stylelint: convert all rgba() to modern rgb(…/…%) notation across
  both light and dark theme shadows and gradients
- DRY: replace hardcoded hex values in dark mode utility overrides and
  component overrides with CSS variable references (--color-surface,
  --color-background, --color-border, --color-text-primary, etc.)
- Remove redundant [data-theme="dark"] body rule (body already uses
  CSS variables that are redefined under the dark theme selector)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 21:12:37 -05:00
Chuck
158e07c82b fix(plugins): prevent root-owned files from blocking plugin updates (#242)
* fix(web): unify operation history tracking for monorepo plugin operations

The operation history UI was reading from the wrong data source
(operation_queue instead of operation_history), install/update records
lacked version details, toggle operations used a type name that didn't
match UI filters, and the Clear History button was non-functional.

- Switch GET /plugins/operation/history to read from OperationHistory
  audit log with return type hint and targeted exception handling
- Add DELETE /plugins/operation/history endpoint; wire up Clear button
- Add _get_plugin_version helper with specific exception handling
  (FileNotFoundError, PermissionError, json.JSONDecodeError) and
  structured logging with plugin_id/path context
- Record plugin version, branch, and commit details on install/update
- Record install failures in the direct (non-queue) code path
- Replace "toggle" operation type with "enable"/"disable"
- Add normalizeStatus() in JS to map completed→success, error→failed
  so status filter works regardless of server-side convention
- Truncate commit SHAs to 7 chars in details display
- Fix HTML filter options, operation type colors, duplicate JS init

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(plugins): prevent root-owned files from blocking plugin updates

The root ledmatrix service creates __pycache__ and data cache files
owned by root inside plugin directories. The web service (non-root)
cannot delete these when updating or uninstalling plugins, causing
operations to fail with "Permission denied".

Defense in depth with three layers:
- Prevent: PYTHONDONTWRITEBYTECODE=1 in systemd service + run.py
- Fallback: sudoers rules for rm on plugin directories
- Code: _safe_remove_directory() now uses sudo as last resort,
  and all bare shutil.rmtree() calls routed through it

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): harden sudo removal with path-validated helper script

Address code review findings:

- Replace raw rm/find sudoers wildcards with a vetted helper script
  (safe_plugin_rm.sh) that resolves symlinks and validates the target
  is a strict child of plugin-repos/ or plugins/ before deletion
- Add allow-list validation in sudo_remove_directory() that checks
  resolved paths against allowed bases before invoking sudo
- Check _safe_remove_directory() return value before shutil.move()
  in the manifest ID rename path
- Move stat import to module level in store_manager.py
- Use stat.S_IRWXU instead of 0o777 in chmod fallback stage
- Add ignore_errors=True to temp dir cleanup in finally block
- Use command -v instead of which in configure_web_sudo.sh

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): address code review round 2 — harden paths and error handling

- safe_plugin_rm.sh: use realpath --canonicalize-missing for ALLOWED_BASES
  so the script doesn't fail under set -e when dirs don't exist yet
- safe_plugin_rm.sh: add -- before path in rm -rf to prevent flag injection
- permission_utils.py: use shutil.which('bash') instead of hardcoded /bin/bash
  to match whatever path the sudoers BASH_PATH resolves to
- store_manager.py: check _safe_remove_directory() return before shutil.move()
  in _install_from_monorepo_zip to prevent moving into a non-removed target
- store_manager.py: catch OSError instead of PermissionError in Stage 1 removal
  to handle both EACCES and EPERM error codes
- store_manager.py: hoist sudo_remove_directory import to module level
- configure_web_sudo.sh: harden safe_plugin_rm.sh to root-owned 755 so
  the web user cannot modify the vetted helper script

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): validate command paths in sudoers config and use resolved paths

- configure_web_sudo.sh: validate that required commands (systemctl, bash,
  python3) resolve to non-empty paths before generating sudoers entries;
  abort with clear error if any are missing; skip optional commands
  (reboot, poweroff, journalctl) with a warning instead of emitting
  malformed NOPASSWD lines; validate helper script exists on disk
- permission_utils.py: pass the already-resolved path to the subprocess
  call and use it for the post-removal exists() check, eliminating a
  TOCTOU window between Python-side validation and shell-side execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 19:28:05 -05:00
Chuck
9a72adbde1 fix(web): unify operation history tracking for monorepo plugin operations (#240)
The operation history UI was reading from the wrong data source
(operation_queue instead of operation_history), install/update records
lacked version details, toggle operations used a type name that didn't
match UI filters, and the Clear History button was non-functional.

- Switch GET /plugins/operation/history to read from OperationHistory
  audit log with return type hint and targeted exception handling
- Add DELETE /plugins/operation/history endpoint; wire up Clear button
- Add _get_plugin_version helper with specific exception handling
  (FileNotFoundError, PermissionError, json.JSONDecodeError) and
  structured logging with plugin_id/path context
- Record plugin version, branch, and commit details on install/update
- Record install failures in the direct (non-queue) code path
- Replace "toggle" operation type with "enable"/"disable"
- Add normalizeStatus() in JS to map completed→success, error→failed
  so status filter works regardless of server-side convention
- Truncate commit SHAs to 7 chars in details display
- Fix HTML filter options, operation type colors, duplicate JS init

Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 12:11:12 -05:00
Chuck
9d3bc55c18 fix: post-merge monorepo hardening and cleanup (#239)
* fix: address PR review nitpicks for monorepo hardening

- Add docstring note about regex limitation in parse_json_with_trailing_commas
- Abort on zip-slip in ZIP installer instead of skipping (consistent with API installer)
- Use _safe_remove_directory for non-git plugin reinstall path
- Use segment-wise encodeURIComponent for View button URL encoding

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: check _safe_remove_directory result before reinstalling plugin

Avoid calling install_plugin into a partially-removed directory by
checking the boolean return of _safe_remove_directory, mirroring the
guard already used in the git-remote migration path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: normalize subpath prefix and add zip-slip guard to download installer

- Strip trailing slashes from plugin_subpath before building the tree
  filter prefix, preventing double-slash ("subpath//") that would cause
  file_entries to silently miss all matches.
- Add zip-slip protection to _install_via_download (extractall path),
  matching the guard already present in _install_from_monorepo_zip.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 11:59:23 -05:00
Chuck
df3cf9bb56 Feat/monorepo migration (#238)
* feat: adapt LEDMatrix for monorepo plugin architecture

Update store_manager to fetch manifests from subdirectories within the
monorepo (plugin_path/manifest.json) instead of repo root. Remove 21
plugin submodule entries from .gitmodules, simplify workspace file to
reference the monorepo, and clean up scripts for the new layout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: auto-reinstall plugins when registry repo URL changes

When a user clicks "Update" on a git-cloned plugin, detect if the
local git remote URL no longer matches the registry's repo URL (e.g.
after monorepo migration). Instead of pulling from the stale archived
repo, automatically remove and reinstall from the new registry source.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: plugin store "View" button links to correct monorepo subdirectory

When a plugin has a plugin_path (monorepo plugin), construct the GitHub
URL as repo/tree/main/plugin_path so users land on the specific plugin
directory. Pass plugin_path through the store API response to the
frontend.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: monorepo manifest fetch in search + version-based update detection

Fix search_plugins() to pass plugin_path when fetching manifests from
GitHub, matching the fix already in get_plugin_info(). Without this,
monorepo plugin descriptions 404 in search results.

Add version comparison for non-git plugins (monorepo installs) so
"Update All" skips plugins already at latest_version instead of blindly
reinstalling every time.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: show plugin version instead of misleading monorepo commit info

Replace commit hash, date, and stars on plugin cards with the plugin's
version number. In a monorepo all plugins share the same commit history
and star count, making those fields identical and misleading. Version
is the meaningful per-plugin signal users care about.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add CLAUDE.md with project structure and plugin store docs

Documents plugin store architecture, monorepo install flow, version-
based update detection, and the critical version bump workflow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* perf: extract only target plugin from monorepo ZIP instead of all files

Previously _install_from_monorepo() called extractall() on the entire
monorepo ZIP (~13MB, 600+ files) just to grab one plugin subdirectory.
Now filter zip members by the plugin prefix and extract only matching
files, reducing disk I/O by ~96% per install/update.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* perf: download only target plugin files via GitHub Trees API

Replace full monorepo ZIP download (~5MB) with targeted file downloads
(~200KB per plugin) using the GitHub Git Trees API for directory listing
and raw.githubusercontent.com for individual file content.

One API call fetches the repo tree, client filters for the target
plugin's files, then downloads each file individually. Falls back to
ZIP if the API is unavailable (rate limited, no network, etc.).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: clean up partial files between API and ZIP install fallbacks

Ensure target_path is fully removed before the ZIP fallback runs, and
before shutil.move() in the ZIP method. Prevents directory nesting if
the API method creates target_path then fails mid-download.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: harden scripts and fix monorepo URL handling

- setup_plugin_repos.py: add type hints, remove unnecessary f-string,
  wrap manifest parsing in try/except to skip malformed manifests
- update_plugin_repos.py: add 120s timeout to git pull with
  TimeoutExpired handling
- store_manager.py: fix rstrip('.zip') stripping valid branch chars,
  use removesuffix('.zip'); remove redundant import json
- plugins_manager.js: View button uses dynamic branch, disables when
  repo is missing, encodes plugin_path in URL
- CLAUDE.md: document plugin repo naming convention

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: harden monorepo install security and cleanup

- store_manager: fix temp dir leak in _install_from_monorepo_zip by
  moving cleanup to finally block
- store_manager: add zip-slip guard validating extracted paths stay
  inside temp directory
- store_manager: add 500-file sanity cap to API-based install
- store_manager: extract _normalize_repo_url as @staticmethod
- setup_plugin_repos: propagate create_symlinks() failure via sys.exit,
  narrow except to OSError

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add path traversal guard to API-based monorepo installer

Validate that each file's resolved destination stays inside
target_path before creating directories or writing bytes, mirroring
the zip-slip guard in _install_from_monorepo_zip.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use _safe_remove_directory for monorepo migration cleanup

Replace shutil.rmtree(ignore_errors=True) with _safe_remove_directory
which handles permission errors gracefully and returns status, preventing
install_plugin from running against a partially-removed directory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 18:57:30 -05:00
Chuck
448a15c1e6 feat(fonts): add dynamic font selection and font manager improvements (#232)
* feat(fonts): add dynamic font selection and font manager improvements

- Add font-selector widget for dynamic font selection in plugin configs
- Enhance /api/v3/fonts/catalog with filename, display_name, and type
- Add /api/v3/fonts/preview endpoint for server-side font rendering
- Add /api/v3/fonts/<family> DELETE endpoint with system font protection
- Fix /api/v3/fonts/upload to actually save uploaded font files
- Update font manager tab with dynamic dropdowns, server-side preview, and font deletion
- Add new BDF fonts: 6x10, 6x12, 6x13, 7x13, 7x14, 8x13, 9x15, 9x18, 10x20 (with bold/oblique variants)
- Add tom-thumb, helvR12, clR6x12, texgyre-27 fonts

Plugin authors can use x-widget: "font-selector" in schemas to enable
dynamic font selection that automatically shows all available fonts.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(fonts): security fixes and code quality improvements

- Fix README.md typos and add language tags to code fences
- Remove duplicate delete_font function causing Flask endpoint collision
- Add safe integer parsing for size parameter in preview endpoint
- Fix path traversal vulnerability in /fonts/preview endpoint
- Fix path traversal vulnerability in /fonts/<family> DELETE endpoint
- Fix XSS vulnerability in fonts.html by using DOM APIs instead of innerHTML
- Move baseUrl to shared scope to fix ReferenceError in multiple functions

Security improvements:
- Validate font filenames reject path separators and '..'
- Validate paths are within fonts_dir before file operations
- Use textContent and data attributes instead of inline onclick handlers
- Restrict file extensions to known font types

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(fonts): address code issues and XSS vulnerabilities

- Move `import re` to module level, remove inline imports
- Remove duplicate font_file assignment in upload_font()
- Remove redundant validation with inconsistent allowed extensions
- Remove redundant PathLib import, use already-imported Path
- Fix XSS vulnerabilities in fonts.html by using DOM APIs instead of
  innerHTML with template literals for user-controlled data

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(fonts): add size limits to font preview endpoint

Add input validation to prevent DoS via large image generation:
- MAX_TEXT_CHARS (100): Limit text input length
- MAX_TEXT_LINES (3): Limit number of newlines
- MAX_DIM (1024): Limit max width/height
- MAX_PIXELS (500000): Limit total pixel count

Validates text early before processing and checks computed
dimensions after bbox calculation but before image allocation.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(fonts): improve error handling, catalog keys, and BDF preview

- Add structured logging for cache invalidation failures instead of
  silent pass (FontUpload, FontDelete, FontCatalog contexts)
- Use filename as unique catalog key to prevent collisions when
  multiple font files share the same family_name from metadata
- Return explicit error for BDF font preview instead of showing
  misleading preview with default font

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(fonts): address nitpick issues in font management

Frontend (fonts.html):
- Remove unused escapeHtml function (dead code)
- Add max-attempts guard (50 retries) to initialization loop
- Add response.ok checks before JSON parsing in deleteFont,
  addFontOverride, deleteFontOverride, uploadSelectedFonts
- Use is_system flag from API instead of hardcoded client-side list

Backend (api_v3.py):
- Move SYSTEM_FONTS to module-level frozenset for single source of truth
- Add is_system flag to font catalog entries
- Simplify delete_font system font check using frozenset lookup

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(fonts): align frontend upload validation with backend

- Add .otf to accepted file extensions (HTML accept attribute, JS filter)
- Update validation regex to allow hyphens (matching backend)
- Preserve hyphens in auto-generated font family names
- Update UI text to reflect all supported formats

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(fonts): fix lint errors and missing variable

- Remove unused exception binding in set_cached except block
- Define font_family_lower before case-insensitive fallback loop
- Add response.ok check to font preview fetch (consistent with other handlers)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(fonts): address nitpick code quality issues

- Add return type hints to get_font_preview and delete_font endpoints
- Catch specific PIL exceptions (IOError/OSError) when loading fonts
- Replace innerHTML with DOM APIs for trash icon (consistency)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(fonts): remove unused exception bindings in cache-clearing blocks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 18:21:27 -05:00
Chuck
b99be88cec fix(plugins): namespace-isolate modules for safe parallel loading (#237)
* fix(plugins): prevent KeyError race condition in module cleanup

When multiple plugins have modules with the same name (e.g.,
background_data_service.py), the _clear_conflicting_modules function
could raise a KeyError if a module was removed between iteration
and deletion. This race condition caused plugin loading failures
with errors like: "Unexpected error loading plugin: 'background_data_service'"

Changes:
- Use sys.modules.pop(mod_name, None) instead of del sys.modules[mod_name]
  to safely handle already-removed modules
- Apply same fix to plugin unload in plugin_manager.py for consistency
- Fix typo in sports.py: rankself._team_rankings_cacheings ->
  self._team_rankings_cache

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(plugins): namespace-isolate plugin modules to prevent parallel loading collisions

Multiple sport plugins share identically-named Python files (scroll_display.py,
game_renderer.py, sports.py, etc.). When loaded in parallel via ThreadPoolExecutor,
bare module names collide in sys.modules causing KeyError crashes.

Replace _clear_conflicting_modules with _namespace_plugin_modules: after exec_module
loads a plugin, its bare-name sub-modules are moved to namespaced keys
(e.g. _plg_basketball_scoreboard_scroll_display) so they cannot collide.
A threading lock serializes the exec_module window where bare names temporarily exist.

Also updates unload_plugin to clean up namespaced sub-modules from sys.modules.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(plugins): address review feedback on namespace isolation

- Fix main module accidentally renamed: move before_keys snapshot to
  after sys.modules[module_name] insertion so the main entry is excluded
  from namespace renaming and error cleanup
- Use Path.is_relative_to() instead of substring matching for plugin
  directory containment checks to avoid false-matches on overlapping
  directory names
- Add try/except around exec_module to clean up partially-initialized
  modules on failure, preventing leaked bare-name entries
- Add public unregister_plugin_modules() method on PluginLoader so
  PluginManager doesn't reach into private attributes during unload
- Update stale comment referencing removed _clear_conflicting_modules

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(plugins): remove unused plugin_dir_str variable

Leftover from the old substring containment check, now replaced by
Path.is_relative_to().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(plugins): extract shared helper for bare-module filtering

Hoist plugin_dir.resolve() out of loops and deduplicate the bare-module
filtering logic between _namespace_plugin_modules and the error cleanup
block into _iter_plugin_bare_modules().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(plugins): keep bare-name alias to prevent lazy import duplication

Stop removing bare module names from sys.modules after namespacing.
Removing them caused lazy intra-plugin imports (deferred imports inside
methods) to re-import from disk, creating a second inconsistent module
copy. Keeping both the bare and namespaced entries pointing to the same
object avoids this. The next plugin's exec_module naturally overwrites
the bare entry with its own version.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 22:47:24 -05:00
Chuck
4a9fc2df3a feat(web): add shutdown button to Quick Actions (#234)
Add a "Shutdown System" button to the Overview page that gracefully
powers off the Raspberry Pi. Uses sudo poweroff, consistent with the
existing reboot_system action, letting sudo's secure_path handle
binary resolution.

Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 12:35:37 -05:00
Chuck
d207e7c6dd feat(config): add led_rgb_sequence option to config template (#231)
Add the led_rgb_sequence configuration option to the matrix config template,
allowing users to specify the RGB sequence for their LED panels.

Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 13:12:47 -05:00
Chuck
7e98fa9bd8 fix(web): handle string booleans and mode variants in schedule-picker widget (#228)
* fix(web): handle string boolean values in schedule-picker widget

The normalizeSchedule function used strict equality (===) to check the
enabled field, which would fail if the config value was a string "true"
instead of boolean true. This could cause the checkbox to always appear
unchecked even when the setting was enabled.

Added coerceToBoolean helper that properly handles:
- Boolean true/false (returns as-is)
- String "true", "1", "on" (case-insensitive) → true
- String "false" or other values → false

Applied to both main schedule enabled and per-day enabled fields.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: trim whitespace in coerceToBoolean string handling

* fix: normalize mode value to handle per_day and per-day variants

---------

Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 16:24:16 -05:00
Chuck
0d5510d8f7 Fix/plugin module namespace collision (#229)
* fix(web): handle string boolean values in schedule-picker widget

The normalizeSchedule function used strict equality (===) to check the
enabled field, which would fail if the config value was a string "true"
instead of boolean true. This could cause the checkbox to always appear
unchecked even when the setting was enabled.

Added coerceToBoolean helper that properly handles:
- Boolean true/false (returns as-is)
- String "true", "1", "on" (case-insensitive) → true
- String "false" or other values → false

Applied to both main schedule enabled and per-day enabled fields.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: trim whitespace in coerceToBoolean string handling

* fix: normalize mode value to handle per_day and per-day variants

* fix(plugins): resolve module namespace collisions between plugins

When multiple plugins have modules with the same name (e.g., data_fetcher.py),
Python's sys.modules cache would return the wrong module. This caused plugins
like ledmatrix-stocks to fail loading because it imported data_fetcher from
ledmatrix-leaderboard instead of its own.

Added _clear_conflicting_modules() to remove cached plugin modules from
sys.modules before loading each plugin, ensuring correct module resolution.

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>
2026-01-30 16:24:06 -05:00
Chuck
18fecd3cda fix(web): handle string boolean values in schedule-picker widget (#227)
* fix(web): handle string boolean values in schedule-picker widget

The normalizeSchedule function used strict equality (===) to check the
enabled field, which would fail if the config value was a string "true"
instead of boolean true. This could cause the checkbox to always appear
unchecked even when the setting was enabled.

Added coerceToBoolean helper that properly handles:
- Boolean true/false (returns as-is)
- String "true", "1", "on" (case-insensitive) → true
- String "false" or other values → false

Applied to both main schedule enabled and per-day enabled fields.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: trim whitespace in coerceToBoolean string handling

---------

Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 14:57:57 -05:00
Chuck
1c3269c0f3 Fix/led panel permissions 224 (#226)
* fix(install): exclude rpi-rgb-led-matrix from permission normalization

The permission normalization step in first_time_install.sh was running
chmod 644 on all files, which stripped executable bits from compiled
library files (librgbmatrix.so.1) after make build-python created them.

This caused LED panels to not work after fresh installation until users
manually ran chmod on the rpi-rgb-led-matrix-master directory.

Fixes #224

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(install): resolve install script issues and speed up web UI startup

Issues addressed:
- Remove redundant python3-pillow from apt (Debian maps it to python3-pil)
- Only upgrade pip, not setuptools/wheel (they conflict with apt versions)
- Remove separate apt numpy install (pip handles it from requirements.txt)
- Install web interface deps during first-time setup, not on every startup
- Add marker file (.web_deps_installed) to skip redundant pip installs
- Add user-friendly message about wait time after installation

The web UI was taking 30-60+ seconds to start because it ran pip install
on every startup. Now it only installs dependencies on first run.

Fixes #208

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(install): prevent duplicate web dependency installation

Step 7 was installing web dependencies again even though they were
already installed in Step 5. Now Step 7 checks for the .web_deps_installed
marker file and skips the installation if it already exists.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(install): only create web deps marker on successful install

The .web_deps_installed marker file should only be created when pip
install actually succeeds. Previously it was created regardless of
the pip exit status, which could cause subsequent runs to skip
installing missing dependencies.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(install): create config files before starting services

The services were being started before config files existed, causing
the web service to fail with "config.json not found". Reordered steps
so config files are created (Step 4) before services are installed
and started (Step 4.1).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(install): remove pip upgrade step (apt version is sufficient)

The apt-installed pip cannot be upgraded because it doesn't have a
RECORD file. Since the apt version (25.1.1) is already recent enough,
we can skip the upgrade step entirely.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(install): reorder script to install services after dependencies

Moved main LED Matrix service installation (Step 7.5) to after all
Python dependencies are installed (Steps 5-7). Previously services
were being started before pip packages and rgbmatrix were ready,
causing startup failures.

New order:
- Step 5: Python pip dependencies
- Step 6: rpi-rgb-led-matrix build
- Step 7: Web interface dependencies
- Step 7.5: Main LED Matrix service (moved here)
- Step 8: Web interface service

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(install): update step list and fix setcap symlink handling

- Updated step list header to match actual step order after reordering
  (Step 4 is now "Ensure configuration files exist", added Step 7.5
  for main service, added Step 8.1 for systemd permissions)

- Fixed Python capabilities configuration:
  - Check if setcap command exists before attempting to use it
  - Resolve symlinks with readlink -f to get the real binary path
  - Only print success message when setcap actually succeeds
  - Print clear warning with helpful info when setcap fails

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>
2026-01-30 12:49:07 -05:00
Chuck
ea61331d46 fix(install): resolve install issues and speed up web UI startup (#225)
* fix(install): exclude rpi-rgb-led-matrix from permission normalization

The permission normalization step in first_time_install.sh was running
chmod 644 on all files, which stripped executable bits from compiled
library files (librgbmatrix.so.1) after make build-python created them.

This caused LED panels to not work after fresh installation until users
manually ran chmod on the rpi-rgb-led-matrix-master directory.

Fixes #224

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(install): resolve install script issues and speed up web UI startup

Issues addressed:
- Remove redundant python3-pillow from apt (Debian maps it to python3-pil)
- Only upgrade pip, not setuptools/wheel (they conflict with apt versions)
- Remove separate apt numpy install (pip handles it from requirements.txt)
- Install web interface deps during first-time setup, not on every startup
- Add marker file (.web_deps_installed) to skip redundant pip installs
- Add user-friendly message about wait time after installation

The web UI was taking 30-60+ seconds to start because it ran pip install
on every startup. Now it only installs dependencies on first run.

Fixes #208

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(install): prevent duplicate web dependency installation

Step 7 was installing web dependencies again even though they were
already installed in Step 5. Now Step 7 checks for the .web_deps_installed
marker file and skips the installation if it already exists.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(install): only create web deps marker on successful install

The .web_deps_installed marker file should only be created when pip
install actually succeeds. Previously it was created regardless of
the pip exit status, which could cause subsequent runs to skip
installing missing dependencies.

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>
2026-01-30 12:08:57 -05:00
Chuck
8fb2800495 feat: add error detection, monitoring, and code quality improvements (#223)
* feat: add error detection, monitoring, and code quality improvements

This comprehensive update addresses automatic error detection, code
quality, and plugin development experience:

## Error Detection & Monitoring
- Add ErrorAggregator service for centralized error tracking
- Add pattern detection for recurring errors (5+ in 60 min)
- Add error dashboard API endpoints (/api/v3/errors/*)
- Integrate error recording into plugin executor

## Code Quality
- Remove 10 silent `except: pass` blocks in sports.py and football.py
- Remove hardcoded debug log paths
- Add pre-commit hooks to prevent future bare except clauses

## Validation & Type Safety
- Add warnings when plugins lack config_schema.json
- Add config key collision detection for plugins
- Improve type coercion logging in BasePlugin

## Testing
- Add test_config_validation_edge_cases.py
- Add test_plugin_loading_failures.py
- Add test_error_aggregator.py

## Documentation
- Add PLUGIN_ERROR_HANDLING.md guide
- Add CONFIG_DEBUGGING.md guide

Note: GitHub Actions CI workflow is available in the plan but requires
workflow scope to push. Add .github/workflows/ci.yml manually.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address code review issues

- Fix GitHub issues URL in CONFIG_DEBUGGING.md
- Use RLock in error_aggregator.py to prevent deadlock in export_to_file
- Distinguish missing vs invalid schema files in plugin_manager.py
- Add assertions to test_null_value_for_required_field test
- Remove unused initial_count variable in test_plugin_load_error_recorded
- Add validation for max_age_hours in clear_old_errors API endpoint

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>
2026-01-30 10:05:09 -05:00
Chuck
8912501604 fix(web): ensure unchecked checkboxes save as false in main config forms (#222)
* fix: remove plugin-specific calendar duration from config template

Plugin display durations should be added dynamically when plugins are
installed, not hardcoded in the template.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(web): ensure unchecked checkboxes save as false in main config forms

HTML checkboxes omit their key entirely when unchecked, so the backend
never received updates to set boolean values to false. This affected:

- vegas_scroll_enabled: Now uses _coerce_to_bool helper
- use_short_date_format: Now uses _coerce_to_bool helper
- Plugin system checkboxes (auto_discover, auto_load_enabled, development_mode):
  Now uses _coerce_to_bool helper
- Hardware checkboxes (disable_hardware_pulsing, inverse_colors, show_refresh_rate):
  Now uses _coerce_to_bool helper
- web_display_autostart: Now uses _coerce_to_bool helper

Added _coerce_to_bool() helper function that properly converts form string
values ("true", "on", "1", "yes") to actual Python booleans, ensuring
consistent JSON types in config and correct downstream boolean checks.

Also added value="true" to all main config checkboxes for consistent boolean
parsing (sends "true" instead of "on" when checked).

This is the same issue fixed in commit 10d70d91 for plugin configs, but
for the main configuration forms (display settings, general settings).

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>
2026-01-29 21:49:29 -05:00
Chuck
68c4259370 fix: reduce scroll catch-up steps to limit jitter (#219)
Reduce max_steps from 0.1s to 0.04s of catch-up time (from 5 to 2 steps
at 50 FPS). When the system lags, the previous catch-up logic allowed
jumping up to 5 pixels at once, causing visible jitter. Limiting to 2
steps provides smoother scrolling while still allowing for minor timing
corrections.

Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 20:03:17 -05:00
Chuck
7f5c7399fb fix: remove plugin-specific calendar duration from config template (#221)
Plugin display durations should be added dynamically when plugins are
installed, not hardcoded in the template.

Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 19:58:42 -05:00
Chuck
14c50f316e feat: add timezone support for schedules and dim schedule feature (#218)
* feat: add timezone support for schedules and dim schedule feature

- Fix timezone handling in _check_schedule() to use configured timezone
  instead of system time (addresses schedule offset issues)
- Add dim schedule feature for automatic brightness dimming:
  - New dim_schedule config section with brightness level and time windows
  - Smart interaction: dim schedule won't turn display on if it's off
  - Supports both global and per-day modes like on/off schedule
- Add set_brightness() and get_brightness() methods to DisplayManager
  for runtime brightness control
- Add REST API endpoints: GET/POST /api/v3/config/dim-schedule
- Add web UI for dim schedule configuration in schedule settings page

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: normalize per-day mode and validate dim_brightness input

- Normalize mode string in _check_dim_schedule to handle both "per-day"
  and "per_day" variants
- Add try/except around dim_brightness int conversion to handle invalid
  input gracefully

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: improve error handling in brightness and dim schedule endpoints

- display_manager.py: Add fail-fast input validation, catch specific
  exceptions (AttributeError, TypeError, ValueError), add [BRIGHTNESS]
  context tags, include stack traces in error logs
- api_v3.py: Catch specific config exceptions (FileNotFoundError,
  JSONDecodeError, IOError), add [DIM SCHEDULE] context tags for
  Pi debugging, include stack traces

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>
2026-01-29 18:12:45 -05:00
Chuck
ddd300a117 Docs/consolidate documentation (#217)
* docs: rename FONT_MANAGER_USAGE.md to FONT_MANAGER.md

Renamed for clearer naming convention.
Part of documentation consolidation effort.

* docs: consolidate Plugin Store guides (2→1)

Merged:
- PLUGIN_STORE_USER_GUIDE.md
- PLUGIN_STORE_QUICK_REFERENCE.md

Into: PLUGIN_STORE_GUIDE.md

- Unified writing style to professional technical
- Added Quick Reference section at top for easy access
- Removed duplicate content
- Added cross-references to related documentation
- Updated formatting to match style guidelines

* docs: create user-focused Web Interface Guide

Created WEB_INTERFACE_GUIDE.md consolidating:
- V3_INTERFACE_README.md (technical details)
- User-facing interface documentation

- Focused on end-user tasks and navigation
- Removed technical implementation details
- Added common tasks section
- Included troubleshooting
- Professional technical writing style

* docs: consolidate WiFi setup guides (4→1)

Merged:
- WIFI_SETUP.md
- OPTIMAL_WIFI_AP_FAILOVER_SETUP.md
- AP_MODE_MANUAL_ENABLE.md
- WIFI_ETHERNET_AP_MODE_FIX.md (behavior documentation)

Into: WIFI_NETWORK_SETUP.md

- Comprehensive coverage of WiFi setup and configuration
- Clear explanation of AP mode failover and grace period
- Configuration scenarios and best practices
- Troubleshooting section combining all sources
- Professional technical writing style
- Added quick reference table for behavior

* docs: consolidate troubleshooting guides (4→1)

Merged:
- TROUBLESHOOTING_QUICK_START.md
- WEB_INTERFACE_TROUBLESHOOTING.md
- CAPTIVE_PORTAL_TROUBLESHOOTING.md
- WEATHER_TROUBLESHOOTING.md

Into: TROUBLESHOOTING.md

- Organized by issue category (web, WiFi, plugins)
- Comprehensive diagnostic commands reference
- Quick diagnosis steps at the top
- Service file template preserved
- Complete diagnostic script included
- Professional technical writing style

* docs: create consolidated Advanced Features guide

Merged:
- VEGAS_SCROLL_MODE.md
- ON_DEMAND_DISPLAY_QUICK_START.md
- ON_DEMAND_DISPLAY_API.md
- ON_DEMAND_CACHE_MANAGEMENT.md
- BACKGROUND_SERVICE_README.md
- PERMISSION_MANAGEMENT_GUIDE.md

Into: ADVANCED_FEATURES.md

- Comprehensive guide covering all advanced features
- Vegas scroll mode with integration examples
- On-demand display with API reference
- Cache management troubleshooting
- Background service documentation
- Permission management patterns
- Professional technical writing style

* docs: create Getting Started guide for first-time users

Created GETTING_STARTED.md:
- Quick start guide (5 minutes)
- Initial configuration walkthrough
- Common first-time issues and solutions
- Next steps and quick reference
- User-friendly tone for beginners
- Links to detailed documentation

* docs: archive consolidated source files and ephemeral docs

Archived files that have been consolidated:
- Plugin Store guides (2 files → PLUGIN_STORE_GUIDE.md)
- Web Interface guide (V3_INTERFACE_README.md → WEB_INTERFACE_GUIDE.md)
- WiFi Setup guides (4 files → WIFI_NETWORK_SETUP.md)
- Troubleshooting guides (4 files → TROUBLESHOOTING.md)
- Advanced Features (6 files → ADVANCED_FEATURES.md)

Archived ephemeral/debug documentation:
- DEBUG_WEB_ISSUE.md
- BROWSER_ERRORS_EXPLANATION.md
- FORM_VALIDATION_FIXES.md
- WEB_UI_RELIABILITY_IMPROVEMENTS.md
- CAPTIVE_PORTAL_TESTING.md
- NEXT_STEPS_COMMANDS.md
- STATIC_IMAGE_MULTI_UPLOAD_PLAN.md
- RECONNECT_AFTER_CAPTIVE_PORTAL_TESTING.md

Archived implementation summaries:
- PLUGIN_CONFIG_TABS_SUMMARY.md
- PLUGIN_CONFIG_SYSTEM_VERIFICATION.md
- PLUGIN_SCHEMA_AUDIT_SUMMARY.md
- STARTUP_OPTIMIZATION_SUMMARY.md
- PLUGIN_DISPATCH_IMPLEMENTATION.md
- NESTED_SCHEMA_IMPLEMENTATION.md
- AP_MODE_MANUAL_ENABLE_CHANGES.md
- PLUGIN_CONFIG_SYSTEM_EXPLANATION.md

Total archived: 27 files
Preserves git history while cleaning up main docs directory

* docs: rename API_REFERENCE.md to REST_API_REFERENCE.md

Renamed for clarity - this is specifically the REST API reference
for the web interface, not a general API reference.

* docs: update README.md to reflect consolidated documentation structure

Updated documentation index:
- Reflects new consolidated guides (51 → 16-17 files)
- Updated Quick Start sections with new file names
- Added consolidation history (January 2026)
- Updated file references (API_REFERENCE → REST_API_REFERENCE)
- Documented archival of 33 files
- Added benefits of consolidation
- Updated statistics and highlights
- Removed outdated references
- Professional writing style maintained throughout

* docs: add Vegas scroll mode system architecture documentation

Added comprehensive internal architecture section for Vegas mode:
- Component overview with diagram
- VegasModeCoordinator responsibilities and main loop
- StreamManager buffering strategy and content flow
- PluginAdapter integration and fallback behavior
- RenderPipeline 125 FPS rendering process
- Component interaction flows
- Thread safety patterns
- Performance characteristics

Covers:
- How the four components work together
- Initialization and render loop flows
- Config update handling
- Frame rate management and optimization
- Memory usage and CPU characteristics

---------

Co-authored-by: Chuck <chuck@example.com>
2026-01-29 10:32:00 -05:00
Chuck
7524747e44 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>
2026-01-29 10:23:56 -05:00
Chuck
10d70d911a Fix unchecked boolean checkboxes not saving as false (#216)
* fix(web): ensure unchecked boolean checkboxes save as false

HTML checkboxes don't submit values when unchecked. The plugin config
save endpoint starts from existing config (for partial updates), so an
unchecked checkbox's old `true` value persists. Additionally,
merge_with_defaults fills in schema defaults for missing fields, causing
booleans with `"default": true` to always re-enable.

This affected the odds-ticker plugin where NFL/NBA leagues (default:
true) could not be disabled via the checkbox UI, while NHL (default:
false) appeared to work by coincidence.

Changes:
- Add _set_missing_booleans_to_false() that walks the schema after form
  processing and sets any boolean field absent from form data to false
- Add value="true" to boolean checkboxes so checked state sends "true"
  instead of "on" (proper boolean parsing)
- Handle "on"/"off" strings in _parse_form_value_with_schema for
  backwards compatibility with checkboxes lacking value="true"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(web): guard on/off coercion to boolean schema types, handle arrays

- Only coerce "on"/"off" strings to booleans when the schema type is
  boolean; "true"/"false" remain unconditional
- Extend _set_missing_booleans_to_false to recurse into arrays of
  objects (e.g. custom_feeds.0.enabled) by discovering item indices
  from submitted form keys and recursing per-index

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(web): preserve array structures when setting missing booleans

_set_nested_value uses dict-style access for all path segments, which
corrupts lists when paths contain numeric array indices (e.g.
"feeds.custom_feeds.0.enabled").

Refactored _set_missing_booleans_to_false to:
- Accept an optional config_node parameter for direct array item access
- When inside an array item, set booleans directly on the item dict
- Navigate to array lists manually, preserving their list type
- Ensure array items exist as dicts before recursing

This prevents array-of-object configs (like custom_feeds) from being
converted to nested dicts with numeric string keys.

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>
2026-01-28 09:15:38 -05:00
Chuck
a8c85dd015 feat(widgets): add modular widget system for schedule and common inputs (#213)
* feat(widgets): add modular widget system for schedule and common inputs

Add 15 new reusable widgets following the widget registry pattern:
- schedule-picker: composite widget for enable/mode/time configuration
- day-selector: checkbox group for days of the week
- time-range: paired start/end time inputs with validation
- text-input, number-input, textarea: enhanced text inputs
- toggle-switch, radio-group, select-dropdown: selection widgets
- slider, color-picker, date-picker: specialized inputs
- email-input, url-input, password-input: validated string inputs

Refactor schedule.html to use the new schedule-picker widget instead
of inline JavaScript. Add x-widget support in plugin_config.html for
all new widgets so plugins can use them via schema configuration.

Fix form submission for checkboxes by using hidden input pattern to
ensure unchecked state is properly sent via JSON-encoded forms.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(widgets): improve security, validation, and form binding across widgets

- Fix XSS vulnerability: escapeHtml now escapes quotes in all widget fallbacks
- color-picker: validate presets with isValidHex(), use data attributes
- date-picker: add placeholder attribute support
- day-selector: use options.name for hidden input form binding
- password-input: implement requireUppercase/Number/Special validation
- radio-group: fix value injection using this.value instead of interpolation
- schedule-picker: preserve day values when disabling (don't clear times)
- select-dropdown: remove undocumented searchable/icons options
- text-input: apply patternMessage via setCustomValidity
- time-range: use options.name for hidden inputs
- toggle-switch: preserve configured color from data attribute
- url-input: combine browser and custom protocol validation
- plugin_config: add widget support for boolean/number types, pass name to day-selector
- schedule: handle null config gracefully, preserve explicit mode setting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(widgets): validate day-selector input, consistent minLength default, escape JSON quotes

- day-selector: filter incoming selectedDays to only valid entries in DAYS array
  (prevents invalid persisted values from corrupting UI/state)
- password-input: use default minLength of 8 when not explicitly set
  (fixes inconsistency between render() and onInput() strength meter baseline)
- plugin_config.html: escape single quotes in JSON hidden input values
  (prevents broken attributes when JSON contains single quotes)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(widgets): add global notification widget, consolidate duplicated code

- Create notification.js widget with toast-style notifications
- Support for success, error, warning, info types
- Auto-dismiss with configurable duration
- Stacking support with max notifications limit
- Accessible with aria-live and role="alert"
- Update base.html to load notification widget early
- Replace duplicate showNotification in raw_json.html
- Simplify fonts.html fallback notification
- Net reduction of ~66 lines of duplicated code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(widgets): escape options.name in all widgets, validate day-selector format

Security fixes:
- Escape options.name attribute in all 13 widgets to prevent injection
- Affected: color-picker, date-picker, email-input, number-input,
  password-input, radio-group, select-dropdown, slider, text-input,
  textarea, toggle-switch, url-input

Defensive coding:
- day-selector: validate format option exists in DAY_LABELS before use
- Falls back to 'long' format for unsupported/invalid format values

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(plugins): add type="button" to control buttons, add debug logging

- Add type="button" attribute to refresh, update-all, and restart buttons
  to prevent potential form submission behavior
- Add console logging to diagnose button click issues:
  - Log when event listeners are attached (and whether buttons found)
  - Log when handler functions are called

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(widgets): improve security and validation across widget inputs

- color-picker.js: Add sanitizeHex() to validate hex values before HTML
  interpolation, ensuring only safe #rrggbb strings are used
- day-selector.js: Escape inputName in hidden input name attribute
- number-input.js: Sanitize and escape currentValue in input element
- password-input.js: Validate minLength as non-negative integer, clamp
  invalid values to default of 8
- slider.js: Add null check for input element before accessing value
- text-input.js: Clear custom validity before checkValidity() to avoid
  stale errors, re-check after setting pattern message
- url-input.js: Normalize allowedProtocols to array, filter to valid
  protocol strings, and escape before HTML interpolation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(widgets): add defensive fallback for DAY_LABELS lookup in day-selector

Extract labelMap with fallback before loop to ensure safe access even if
format validation somehow fails.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(widgets): add timezone-selector widget with IANA timezone dropdown

- Create timezone-selector.js widget with comprehensive IANA timezone list
- Group timezones by region (US & Canada, Europe, Asia, etc.)
- Show current UTC offset for each timezone
- Display live time preview for selected timezone
- Update general.html to use timezone-selector instead of text input
- Add script tag to base.html for widget loading

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(ui): suppress on-demand status notification on page load

Change loadOnDemandStatus(true) to loadOnDemandStatus(false) during
initPluginsPage() to prevent the "on-demand status refreshed"
notification from appearing every time a tab is opened or the page
is navigated. The notification should only appear on explicit user
refresh.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* style(ui): soften notification close button appearance

Replace blocky FontAwesome X icon with a cleaner SVG that has rounded
stroke caps. Make the button circular, slightly transparent by default,
and add smooth hover transitions for a more polished look.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(widgets): multiple security and validation improvements

- color-picker.js: Ensure presets is always an array before map/filter
- number-input.js: Guard against undefined options parameter
- number-input.js: Sanitize and escape min/max/step HTML attributes
- text-input.js: Clear custom validity in onInput to unblock form submit
- timezone-selector.js: Replace legacy Europe/Belfast with Europe/London
- url-input.js: Use RFC 3986 scheme pattern for protocol validation
- general.html: Use |tojson filter to escape timezone value safely

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(url-input): centralize RFC 3986 protocol validation

Extract protocol normalization into reusable normalizeProtocols()
helper function that validates against RFC 3986 scheme pattern.
Apply consistently in render, validate, and onInput to ensure
protocols like "git+ssh", "android-app" are properly handled
everywhere. Also lowercase protocol comparison in isValidUrl().

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(timezone-selector): use hidden input for form submission

Replace direct select name attribute with a hidden input pattern to
ensure timezone value is always properly serialized in form submissions.
The hidden input is synced on change and setValue calls. This matches
the pattern used by other widgets and ensures HTMX json-enc properly
captures the value.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(general): preserve timezone dropdown value after save

Add inline script to sync the timezone select with the hidden input
value after form submission. This prevents the dropdown from visually
resetting to the old value while the save has actually succeeded.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(widgets): preserve timezone selection across form submission

Use before-request handler to capture the selected timezone value
before HTMX processes the form, then restore it in after-request.
This is more robust than reading from the hidden input which may
also be affected by form state changes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(widgets): add HTMX protection to timezone selector

Add global HTMX event listeners in the timezone-selector widget
that preserve the selected value across any form submissions.
This is more robust than form-specific handlers as it protects
the widget regardless of how/where forms are submitted.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* debug(widgets): add logging and prevent timezone widget re-init

Add debug logging and guards to prevent the timezone widget from
being re-initialized after it's already rendered. This should help
diagnose why the dropdown is reverting after save.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* debug: add console logging to timezone HTMX protection

* debug: add onChange logging to trace timezone selection

* fix(widgets): use selectedIndex to force visual update in timezone dropdown

The browser's select.value setter sometimes doesn't trigger a visual
update when optgroup elements are present. Using selectedIndex instead
forces the browser to correctly update the visible selection.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(widgets): force browser repaint on timezone dropdown restore

Adding display:none/reflow/display:'' pattern to force browser to
visually update the select element after changing selectedIndex.
Increased timeout to 50ms for reliability.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore(widgets): remove debug logging from timezone selector

Clean up console.log statements that were used for debugging the
timezone dropdown visual update issue.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(ui): improve HTMX after-request handler in general settings

- Parse xhr.responseText with JSON.parse in try/catch instead of
  using nonstandard responseJSON property
- Check xhr.status for 2xx success range
- Show error notification for non-2xx responses
- Default to safe fallback values if JSON parsing fails

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(widgets): add input sanitization and timezone validation

- Sanitize minLength/maxLength in text-input.js to prevent attribute
  injection (coerce to integers, validate range)
- Update Europe/Kiev to Europe/Kyiv (canonical IANA identifier)
- Validate timezone currentValue against TIMEZONE_GROUPS before rendering

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(ui): correct error message fallback in HTMX after-request handler

Initialize message to empty string so error responses can use the
fallback 'Failed to save settings' when no server message is provided.
Previously, the truthy default 'Settings saved' would always be used.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(widgets): add constraint normalization and improve value validation

- text-input: normalize minLength/maxLength so maxLength >= minLength
- timezone-selector: validate setValue input against TIMEZONE_GROUPS
- timezone-selector: sync hidden input to actual selected value
- timezone-selector: preserve empty selections across HTMX requests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(widgets): simplify HTMX restore using select.value and dispatch change event

Replace selectedIndex manipulation with direct value assignment for cleaner
placeholder handling, and dispatch change event to refresh timezone preview.

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>
2026-01-27 19:56:16 -05:00
Chuck
0203c5c1b5 Update Discord link in README.md (#211)
Signed-off-by: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
2026-01-26 15:48:28 -05:00
Chuck
384ed096ff fix: prevent /tmp permission corruption breaking system updates (#209)
Issue: LEDMatrix was changing /tmp permissions from 1777 (drwxrwxrwt)
to 2775 (drwxrwsr-x), breaking apt update and other system tools.

Root cause: display_manager.py's _write_snapshot_if_due() called
ensure_directory_permissions() on /tmp when writing snapshots to
/tmp/led_matrix_preview.png. This removed the sticky bit and
world-writable permissions that /tmp requires.

Fix:
- Added PROTECTED_SYSTEM_DIRECTORIES safelist to permission_utils.py
  to prevent modifying permissions on /tmp and other system directories
- Added explicit check in display_manager.py to skip /tmp
- Defense-in-depth approach prevents similar issues in other code paths

The sticky bit (1xxx) is critical for /tmp - it prevents users from
deleting files they don't own. Without world-writable permissions,
regular users cannot create temp files.

Fixes #202

Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-26 10:56:30 -05:00
Chuck
f9de9fa29e Add installation video link to README (#205)
Added installation video link for the LEDMatrix project.

Signed-off-by: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
2026-01-22 17:34:00 -05:00
Chuck
d0ad2031c8 fix(ui): wrap plugin tabs to new lines instead of scrolling (#201)
* fix(ui): wrap plugin tabs to new lines instead of scrolling

Change plugin tabs row from overflow-x-auto to flex-wrap so that
when many plugins are installed, tabs break to new lines instead
of becoming smaller or requiring horizontal scrolling.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(ui): use gap-x instead of space-x for proper wrapped row alignment

Switch from space-x-* to gap-x-* utilities so wrapped rows align
correctly without indentation on subsequent lines.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(ui): add missing flex-wrap and gap utilities to CSS

The project uses hand-written Tailwind-like CSS, not actual Tailwind.
Added missing utility classes needed for plugin tabs wrapping:
- flex-wrap
- gap-x-4, gap-x-6, gap-x-8, gap-y-2
- lg:gap-x-6, xl:gap-x-8 responsive variants

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(ui): apply flex-wrap to system tabs row

Apply the same wrapping behavior to the system tabs row (Overview,
General, WiFi, etc.) so they also wrap to new lines on smaller
viewports instead of scrolling.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(ui): constrain tab container width to enable flex-wrap

Add max-w-full and overflow-hidden to tab row containers to properly
constrain their width, allowing flex-wrap to work correctly.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(ui): remove overflow-hidden that was hiding tabs

Revert the max-w-full overflow-hidden approach as it was hiding
content. Keep both rows using flex-wrap with gap utilities.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: Add custom-leagues widget support for soccer plugin

- Add server-side template rendering for x-widget="custom-leagues"
- Renders table with Name, League Code, Priority, Enabled columns
- Includes inline JavaScript for add/remove row functionality
- Uses indexed field naming for proper array serialization
- Shows common ESPN league codes as hint

This enables the soccer scoreboard plugin's custom leagues feature
to work properly in the web UI.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(ui): reduce tab gap spacing for tighter layout

Reduce horizontal gap between tabs from gap-x-4/6/8 to gap-x-2/3/4
for a more compact appearance.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(widget): Replace custom-leagues with generic array-table widget

- Add generic array-table widget that reads columns from schema
- Support x-columns to specify which columns to display
- Auto-detect columns from items.properties if x-columns not specified
- Remove hardcoded custom-leagues implementation
- Any plugin can now use x-widget: "array-table" for array-of-objects

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(ui): use data attributes for array table button to avoid JSON escaping issues

Move JSON blobs (item_properties and display_columns) from inline onclick
to data-* attributes with proper HTML entity escaping via Jinja's |e filter.
Update addArrayTableRow() to read and parse these data attributes.

This fixes HTML attribute breakage caused by tojson emitting double quotes
inside the onclick attribute value.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(ui): update Add button state when array table rows change

Add updateAddButtonState() helper that toggles the Add button's disabled
attribute and opacity based on current row count vs maxItems.

Called after addArrayTableRow() and removeArrayTableRow(), and also on
page load to ensure correct initial state.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(ui): add try/catch for JSON parsing in addArrayTableRow

Wrap JSON.parse calls for data-item-properties and data-display-columns
in try/catch blocks with fallback to {} and [] respectively. Logs error
with raw attribute values to help debug malformed JSON.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(array-table): Fix getValue input name validation and setValue Add button state sync

- Fix getValue to use early-continue guard preventing errors on inputs without names
- Add updateAddButtonState call in setValue to refresh Add button state after repopulating rows

* fix(ui): make Configure button larger than Uninstall in plugin manager

Swapped button sizes in installed plugins section - Configure button is now
the largest (flex-2), Update is medium (flex-1), and Uninstall is smallest
(no flex class). This prioritizes the Configure action over Uninstall.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix(ui): correct forEach continue and plugin button flex sizing

- Replace invalid continue with return in array-table forEach callback
- Remove redundant hidden input type check in array-table getValue
- Fix plugin button sizing using inline flex styles instead of invalid flex-2 class
- Configure button now properly sized at flex: 2, Update and Uninstall at flex: 1

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* refactor(ui): reorganize plugin buttons into two-row layout

Configure button now takes full width on first row, while Update and
Uninstall buttons share the second row evenly. This makes Configure
more prominent and separates destructive actions to a second row.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix(ui): override inline-flex on Configure button to enable full width

The .btn class uses display: inline-flex which prevents w-full from working.
Added inline style to override with display: flex and width: 100% so the
Configure button properly takes the full width of its row.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix(ui): use inline styles for plugin action buttons layout

Replace Tailwind classes with explicit inline styles to ensure proper
two-row layout for plugin action buttons. Configure button on first row
at full width, Update and Uninstall sharing second row evenly.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
v3.0.0
2026-01-22 10:40:13 -05:00
Chuck
1833e30c1d Feature/wifi setup improvements (#187)
* 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

* feat(wifi): Add grace period for AP mode and improve setup documentation

- Add 90-second grace period (3 checks at 30s intervals) before enabling AP mode
- Change AP to open network (no password) for easier initial setup
- Add verification script for WiFi setup
- Update documentation with grace period details and open network info
- Improve WiFi monitor daemon logging and error handling

* feat(wifi): Add Trixie compatibility and dynamic interface discovery

- Add dynamic WiFi interface discovery instead of hardcoded wlan0
  - Supports traditional (wlan0), predictable (wlp2s0), and USB naming
  - Falls back gracefully if detection fails

- Add Raspberry Pi OS Trixie (Debian 13) detection and compatibility
  - Detect Netplan configuration and connection file locations
  - Disable PMF (Protected Management Frames) on Trixie for better
    client compatibility with certain WiFi adapters

- Improve nmcli hotspot setup for Trixie
  - Add explicit IP configuration (192.168.4.1/24)
  - Add channel configuration to hotspot creation
  - Handle Trixie's default 10.42.0.1 IP override

- Add dnsmasq conflict detection
  - Warn if Pi-hole or other DNS services are using dnsmasq
  - Create backup before overwriting config

- Improve error handling
  - Replace bare except clauses with specific exceptions
  - All subprocess calls now have explicit timeouts

- Document sudoers requirements in module docstring
  - List all required NOPASSWD entries for ledpi user

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(wifi): Use NM_CONNECTIONS_PATHS constant in _detect_trixie

Replace hardcoded Path instances with references to the
NM_CONNECTIONS_PATHS constant for consistency.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(verify): Use ETH_CONNECTED and AP_ACTIVE in summary output

Add connectivity summary section that displays Ethernet and AP mode
status using the previously unused ETH_CONNECTED and AP_ACTIVE flags.

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>
2026-01-19 16:43:02 -05:00
Chuck
2381ead03f feat(cache): Add intelligent disk cache cleanup with retention policies (#199)
* feat(cache): Add intelligent disk cache cleanup with retention policies

- Add cleanup_expired_files() method to DiskCache class
- Implement retention policies based on cache data types:
  * Odds data: 2 days (lines move frequently)
  * Live/recent/leaderboard: 7 days (weekly updates)
  * News/stocks: 14 days
  * Upcoming/schedules/team_info/logos: 60 days (stable data)
- Add cleanup_disk_cache() orchestration in CacheManager
- Start background cleanup thread running every 24 hours
- Run cleanup on application startup
- Add disk cleanup metrics tracking
- Comprehensive logging with cleanup statistics

This prevents disk cache from accumulating indefinitely while preserving
important season data longer than volatile live game data.

* refactor(cache): improve disk cache cleanup implementation

- Implement force parameter throttle mechanism in cleanup_disk_cache
- Fix TOCTOU race condition in disk cache cleanup (getsize/remove)
- Reduce lock contention by processing files outside lock where possible
- Add CacheStrategyProtocol for better type safety (replaces Any)
- Move time import to module level in cache_metrics
- Defer initial cleanup to background thread for non-blocking startup
- Add graceful shutdown mechanism with threading.Event for cleanup thread
- Add stop_cleanup_thread() method for controlled thread termination

* fix(cache): improve disk cache cleanup initialization and error handling

- Only start cleanup thread when disk caching is enabled (cache_dir is set)
- Remove unused retention policy keys (leaderboard, live_scores, logos)
- Handle FileNotFoundError as benign race condition in cleanup
- Preserve existing OSError handling for actual file system errors

---------

Co-authored-by: Chuck <chuck@example.com>
2026-01-19 15:57:19 -05:00
Chuck
bc23b7c75c fix(logos): Add ncaam/ncaaw sport key aliases for basketball plugin (#200)
The basketball scoreboard plugin uses sport_key="ncaam" and "ncaaw",
but LogoDownloader only had "ncaam_basketball" mapped. This caused
get_logo_directory() to fall back to "assets/sports/ncaam_logos"
(non-existent) instead of "assets/sports/ncaa_logos".

Added aliases to both LOGO_DIRECTORIES and API_ENDPOINTS:
- ncaam -> assets/sports/ncaa_logos
- ncaaw -> assets/sports/ncaa_logos
- ncaaw_basketball -> assets/sports/ncaa_logos

Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 09:48:52 -05:00
Chuck
bff16d3e00 refactor: migrate from submodules to multi-root workspace for plugins (#198)
* refactor: migrate from submodules to multi-root workspace for plugins

- Updated LEDMatrix.code-workspace to include all plugin repos as root folders
- Removed symlinks from plugin-repos/ and plugins/ directories
- Updated .gitignore to reflect new plugin management approach
- Added setup_plugin_repos.py script for managing plugin symlinks (if needed)
- Added MULTI_ROOT_WORKSPACE_SETUP.md documentation

Plugins are now managed as independent repositories via multi-root workspace,
allowing for easier development and independent updates without modifying
the LEDMatrix project structure.

* Fix MULTI_ROOT_WORKSPACE_SETUP.md and add JSON error handling

- Remove deprecated clone_plugin_repos.py command reference
- Add language tag to directory tree code fence (fixes MD040)
- Add JSONDecodeError handling in setup_plugin_repos.py with user-friendly error messages

---------

Co-authored-by: Chuck <chuck@example.com>
2026-01-18 13:06:57 -05:00
Chuck
23ada60544 Fix/plugins manager syntax error (#192)
* chore: Update basketball-scoreboard submodule for odds font fix

* fix(plugins): Add missing closing brace in file-upload widget if block

Fixed syntax error where the if statement starting at line 2949 was missing
its closing brace before the else if statement. This caused 'Unexpected token
else' error at line 3257 when parsing the loadCustomHtmlWidget function.

The fix adds the missing closing brace at line 3048 to properly close the
if block before the else if chain continues.

* fix(plugins): Resolve unmatched else if syntax error in plugins_manager.js

- Fixed indentation of else if chain for custom-feeds widget (line 3203)
- Fixed indentation of final else block (line 3240)
- Added missing closing brace to properly close array handling block (line 3257)
- Resolves 'Unexpected token else' syntax error at line 3257
- Allows plugin store to load correctly

* fix: Update plugins_manager.js cache-busting version to force reload of syntax fix

---------

Co-authored-by: Chuck <chuck@example.com>
2026-01-16 15:43:18 -05:00
Chuck
fadcf0f407 Fix/plugins manager syntax error (#191)
* chore: Update basketball-scoreboard submodule for odds font fix

* fix(plugins): Add missing closing brace in file-upload widget if block

Fixed syntax error where the if statement starting at line 2949 was missing
its closing brace before the else if statement. This caused 'Unexpected token
else' error at line 3257 when parsing the loadCustomHtmlWidget function.

The fix adds the missing closing brace at line 3048 to properly close the
if block before the else if chain continues.

---------

Co-authored-by: Chuck <chuck@example.com>
2026-01-16 15:16:46 -05:00
Chuck
71584d4361 Feature/widget registry system (#190)
* chore: Update basketball-scoreboard submodule for odds font fix

* feat(widgets): Add widget registry system for plugin configuration forms

- 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.

* fix(widgets): Security and correctness fixes for widget system

- 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

* fix(widgets): Additional security and error handling improvements

- 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

* fix(widgets): Update Slider example to use sanitized fieldId

- 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

* fix(plugins_manager): Move configurePlugin and togglePlugin to top of file

- Move configurePlugin and togglePlugin definitions to top level (after uninstallPlugin)
- Ensures these critical functions are available immediately when script loads
- Fixes 'Critical functions not available after 20 attempts' error
- Functions are now defined before any HTML rendering checks

* fix(plugins_manager): Fix checkbox state saving using querySelector

- Add escapeCssSelector helper function for safe CSS selector usage
- Replace form.elements[actualKey] with form.querySelector for boolean fields
- Properly handle checkbox checked state using element.checked property
- Fix both schema-based and schema-less boolean field processing
- Ensures checkboxes with dot notation names (nested fields) work correctly

Fixes issue where checkbox states were not properly saved when field names
use dot notation (e.g., 'display.scroll_enabled'). The form.elements
collection doesn't reliably handle dot notation in bracket notation access.

* fix(base.html): Fix form element lookup for dot notation field names

- Add escapeCssSelector helper function (both as method and standalone)
- Replace form.elements[key] with form.querySelector for element type detection
- Fixes element lookup failures when field names use dot notation
- Ensures checkbox and multi-select skipping logic works correctly
- Applies fix to both Alpine.js method and standalone function

This complements the fix in plugins_manager.js to ensure all form
element lookups handle nested field names (e.g., 'display.scroll_enabled')
reliably across the entire web interface.

* fix(plugins_manager): Add race condition protection to togglePlugin

- Initialize window._pluginToggleRequests map for per-plugin request tokens
- Generate unique token for each toggle request to track in-flight requests
- Disable checkbox and wrapper UI during request to prevent overlapping toggles
- Add visual feedback with opacity and pointer-events-none classes
- Verify token matches before applying response updates (both success and error)
- Ignore out-of-order responses to preserve latest user intent
- Clear token and re-enable UI after request completes

Prevents race conditions when users rapidly toggle plugins, ensuring
only the latest toggle request's response affects the UI state.

* refactor(escapeCssSelector): Use CSS.escape() for better selector safety

- Prefer CSS.escape() when available for proper CSS selector escaping
- Handles edge cases: unicode characters, leading digits, and spec compliance
- Keep regex-based fallback for older browsers without CSS.escape support
- Update all three instances: plugins_manager.js and both in base.html

CSS.escape() is the standard API for escaping CSS selectors and provides
more robust handling than custom regex, especially for unicode and edge cases.

* fix(plugins_manager): Fix syntax error - missing closing brace for file-upload if block

- Add missing closing brace before else-if for checkbox-group widget
- Fixes 'Unexpected token else' error at line 3138
- The if block for file-upload widget (line 3034) was missing its closing brace
- Now properly structured: if (file-upload) { ... } else if (checkbox-group) { ... }

* fix(plugins_manager): Fix indentation in file-upload widget if block

- Properly indent all code inside the file-upload if block
- Fix template string closing brace indentation
- Ensures proper structure: if (file-upload) { ... } else if (checkbox-group) { ... }
- Resolves syntax error at line 3138

* fix(plugins_manager): Skip checkbox-group [] inputs to prevent config leakage

- Add skip logic for keys ending with '[]' in handlePluginConfigSubmit
- Prevents checkbox-group bracket notation inputs from leaking into config
- Checkbox-group widgets emit name="...[]" checkboxes plus a _data JSON field
- The _data field is already processed correctly, so [] inputs are redundant
- Prevents schema validation failures and extra config keys

The checkbox-group widget creates:
1. Individual checkboxes with name="fullKey[]" (now skipped)
2. Hidden input with name="fullKey_data" containing JSON array (processed)
3. Sentinel hidden input with name="fullKey[]" and empty value (now skipped)

* fix(plugins_manager): Normalize string booleans when checkbox input is missing

- Fix boolean field processing to properly normalize string booleans in fallback path
- Prevents "false"/"0" from being coerced to true when checkbox element is missing
- Handles common string boolean representations: 'true', 'false', '1', '0', 'on', 'off'
- Applies to both schema-based (lines 2386-2400) and schema-less (lines 2423-2433) paths

When a checkbox element cannot be found, the fallback logic now:
1. Checks if value is a string and normalizes known boolean representations
2. Treats undefined/null as false
3. Coerces other types to boolean using Boolean()

This ensures string values like "false" or "0" are correctly converted to false
instead of being treated as truthy non-empty strings.

* fix(base.html): Improve escapeCssSelector fallback to match CSS.escape behavior

- Handle leading digits by converting to hex escape (e.g., '1' -> '\0031 ')
- Handle leading whitespace by converting to hex escape (e.g., ' ' -> '\0020 ')
- Escape internal spaces as '\ ' (preserving space in hex escapes)
- Ensures trailing space after hex escapes per CSS spec
- Applies to both Alpine.js method and standalone function

The fallback now better matches CSS.escape() behavior for older browsers:
1. Escapes leading digits (0-9) as hex escapes with trailing space
2. Escapes leading whitespace as hex escapes with trailing space
3. Escapes all special characters as before
4. Escapes internal spaces while preserving hex escape format

This prevents selector injection issues with field names starting with digits
or whitespace, matching the standard CSS.escape() API behavior.

---------

Co-authored-by: Chuck <chuck@example.com>
2026-01-16 14:09:38 -05:00
Chuck
3b8910ac09 Fix/duplicate display settings (#173)
* fix(plugins): Remove compatible_versions requirement from single plugin install

Remove compatible_versions from required fields in install_from_url method
to match install_plugin behavior. This allows installing plugins from URLs
without manifest version requirements, consistent with store plugin installation.

* fix(7-segment-clock): Update submodule with separator and spacing fixes

* fix(plugins): Add onchange handlers to existing custom feed inputs

- Add onchange handlers to key and value inputs for existing patternProperties fields
- Fixes bug where editing existing custom RSS feeds didn't save changes
- Ensures hidden JSON input field is updated when users edit feed entries
- Affects all plugins using patternProperties (custom_feeds, feed_logo_map, etc.)

* Add array-of-objects widget support to web UI

- Add support for rendering arrays of objects in web UI (for custom_feeds)
- Implement add/remove/update functions for array-of-objects widgets
- Support file-upload widgets within array items
- Update form data handling to support array JSON data fields

* Update plugins_manager.js cache-busting version

Update version parameter to force browser to load new JavaScript with array-of-objects widget support.

* Fix: Move array-of-objects detection before file-upload/checkbox checks

Move the array-of-objects widget detection to the top of the array handler so it's checked before file-upload and checkbox-group widgets. This ensures custom_feeds is properly detected as an array of objects.

* Update cache-busting version for array-of-objects fix

* Remove duplicate array-of-objects check

* Update cache version again

* Add array-of-objects widget support to server-side template

Add detection and rendering for array-of-objects in the Jinja2 template (plugin_config.html).
This enables the custom_feeds widget to display properly with name, URL, enabled checkbox, and logo upload fields.

The widget is detected by checking if prop.items.type == 'object' && prop.items.properties,
and is rendered before the file-upload widget check.

* Use window. prefix for array-of-objects JavaScript functions

Explicitly use window.addArrayObjectItem, window.removeArrayObjectItem, etc.
in the template to ensure the functions are accessible from inline event handlers.
Also add safety checks to prevent errors if functions aren't loaded yet.

* Fix duplicate display settings in config

Prevent display settings from being saved at both nested (display.hardware/runtime) and root level. The save_main_config function was processing display fields twice - once correctly in the nested structure, and again in the catch-all section creating root-level duplicates.

Added display_fields to the skip list in the catch-all section to prevent root-level duplicates. All code expects the nested format, so this ensures consistency.

* fix: Recreate one-shot install script with APT permission and non-interactive fixes

Recreate one-shot install script that was deleted, with fixes for:
1. APT permission denied errors on /tmp
2. Non-interactive mode support

Fixes:
1. Fix /tmp permissions before running first_time_install.sh:
   - chmod 1777 /tmp to ensure APT can write temp files
   - Set TMPDIR=/tmp explicitly
   - Preserve TMPDIR when using sudo -E

2. Enable non-interactive mode:
   - Pass -y flag or LEDMATRIX_ASSUME_YES=1 to first_time_install.sh
   - Prevents read prompt failure at line 242 when run via curl | bash

3. Better error handling:
   - Temporarily disable errexit to capture exit code
   - Re-enable errexit after capturing
   - Added fix_tmp_permissions() function

This resolves the 'Permission denied' errors for APT temp files and the
interactive prompt failure when running via pipe.

* fix(plugins): Restore version and display_modes to required_fields and fix array object data persistence

- Restore 'version' and 'display_modes' to required_fields in store_manager.py manifest validation (both occurrences at lines 839 and 977)
- Fix updateArrayObjectData to merge input fields with existing item data to preserve non-editable properties like logo objects
- Implement handleArrayObjectFileUpload to properly upload files and store metadata in data-file-data attribute
- Implement removeArrayObjectFile to properly remove file metadata and update data structure
- Update renderArrayObjectItem to preserve file data in data-file-data attribute when rendering existing items

* fix(plugins): Remove version from required_fields, keep display_modes required

- Remove 'version' from required_fields in store_manager.py (both occurrences)
  - Some existing plugins have version: null or no version field (basketball-scoreboard, odds-ticker)
  - All code uses safe accessors (manifest.get('version')), so optional is safe
- Keep 'display_modes' as required - all plugins have it and tests expect it

* fix: Preserve exit codes in retry() and fix null handling in JSON data detection

- Fix retry() function to preserve original command exit code by capturing status immediately after command execution
- Fix JSON data detection to prevent null from overwriting config by checking jsonValue !== null before treating as object
- Both fixes prevent edge cases that could cause incorrect behavior or data corruption

* fix: Resolve merge conflict, fix array-of-objects file upload, and improve retry function

- Remove unresolved merge conflict marker in array rendering (checkbox input attributes)
- Fix array-of-objects file upload selector mismatch by adding id to wrapper element
- Fix index-based preserve corruption by using data-item-data attributes instead of array indices
- Add showNotification guards to prevent errors when notifications aren't available
- Fix retry() function to work with set -Eeuo pipefail by disabling errexit for command execution

* fix: Remove duplicate implementations, fix upload config, and add type coercion

- Remove/guard duplicate updateArrayObjectData, handleArrayObjectFileUpload, and removeArrayObjectFile stub implementations that were overwriting real implementations
- Fix hard-coded plugin ID fallback in renderArrayObjectItem - use null instead of 'ledmatrix-news'
- Fix upload config to use uploadConfig.allowed_types and uploadConfig.max_size_mb from schema instead of hard-coded values
- Store uploadConfig in data-upload-config attribute and read it in handleArrayObjectFileUpload for validation
- Add type coercion to updateArrayObjectData: coerce number inputs to Number, array inputs via JSON.parse with comma-split fallback

* fix: Use event-based element lookup in handleArrayObjectFileUpload

- Change from constructing ID to using event.target.closest('.array-object-item') to find item element
- Query fileUploadContainer from itemEl instead of using constructed ID lookup
- Remove reliance on `${fieldId}_item_${itemIndex}` which breaks after reindexing
- Add response.ok check before calling response.json() to avoid JSON parsing errors on HTTP errors
- Handle non-OK responses with proper error messages (JSON parse with fallback)

* fix: Improve HTML escaping and add pluginId validation for file uploads

- Replace manual single-quote escaping with escapeAttribute() for proper HTML escaping in array-of-objects hidden input
- Update default allowed_types to include 'image/jpg' in handleArrayObjectFileUpload
- Add explicit pluginId validation before upload to fail fast with clear error message
- Prevents XSS vulnerabilities and backend rejections from invalid uploads

* fix: Use propKey-scoped selector and harden pluginId validation

- Narrow file widget lookup to use propKey-specific selector (.file-upload-widget-inline[data-prop-key]) to target correct widget when item has multiple file widgets
- Harden pluginId validation by checking typeof pluginId === 'string' before calling trim() to prevent errors on non-string values

---------

Co-authored-by: Chuck <chuck@example.com>
2026-01-14 10:51:55 -05:00
Chuck
94d5a38358 Update README.md (#188)
Signed-off-by: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
2026-01-13 18:35:11 -05:00
Chuck
4a63ff87cb Feature/soccer scroll support (#186)
* 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

* debug(display): Change FPS check logging from debug to info level

- Change FPS check log from DEBUG to INFO to help diagnose scrolling FPS issues
- Add active_mode to log message for clarity
- Helps identify if plugins are being detected for high-FPS mode

* debug(display): Add logging for display_interval in both FPS loops

- Log display_interval when entering high-FPS and normal loops
- Shows expected FPS for high-FPS mode
- Helps diagnose why news ticker shows 50 FPS despite high-FPS detection

* feat: Update soccer-scoreboard submodule with scroll display support

- Submodule now includes full feature parity with football-scoreboard
- Granular display modes for 8 leagues (24 total modes)
- Scroll display mode with game_renderer.py and scroll_display.py
- League registry system with enabled state filtering
- Modernized config_schema.json with per-league scroll settings
- League-aware logo caching to prevent collisions
- Pillow 8.x compatibility for image resampling

Submodule branch: feature/football-feature-parity
Commit: e22a16d

* style(web): Update plugin button colors and reorganize documentation

- Change update button color to yellow-600 in installed plugins section to match plugin config page
- Change refresh plugins button color to blue-600 to match restart display button
- Move DEVELOPMENT.md and MIGRATION_GUIDE.md from root to docs/ directory
- Remove IMPACT_EXPLANATION.md and MERGE_CONFLICT_RESOLUTION_PLAN.md

---------

Co-authored-by: Chuck <chuck@example.com>
2026-01-13 13:33:53 -05:00
Chuck
fdf09fabd2 Debug/fps logging (#183)
* 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

* debug(display): Change FPS check logging from debug to info level

- Change FPS check log from DEBUG to INFO to help diagnose scrolling FPS issues
- Add active_mode to log message for clarity
- Helps identify if plugins are being detected for high-FPS mode

* debug(display): Add logging for display_interval in both FPS loops

- Log display_interval when entering high-FPS and normal loops
- Shows expected FPS for high-FPS mode
- Helps diagnose why news ticker shows 50 FPS despite high-FPS detection

---------

Co-authored-by: Chuck <chuck@example.com>
2026-01-13 10:55:54 -05:00
Chuck
75a8219a29 Update README.md (#185)
Signed-off-by: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
2026-01-13 10:55:18 -05:00
Chuck
a9798e1a7f Enhance README with project details and instructions (#184)
Added project overview, setup instructions, and plugin architecture details.

Signed-off-by: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
2026-01-13 10:45:28 -05:00
Chuck
c35769cefb 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 <chuck@example.com>
2026-01-12 18:44:06 -05:00
Chuck
f1f33989b2 Fix/plugin permission errors (#181)
* 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

---------

Co-authored-by: Chuck <chuck@example.com>
2026-01-12 18:11:54 -05:00
Chuck
f9e21c6033 Fix/plugin permission errors (#180)
* 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)

---------

Co-authored-by: Chuck <chuck@example.com>
2026-01-12 16:15:12 -05:00
Chuck
0f4dbb6c1a Feature/one shot installer (#178)
* fix(plugins): Remove compatible_versions requirement from single plugin install

Remove compatible_versions from required fields in install_from_url method
to match install_plugin behavior. This allows installing plugins from URLs
without manifest version requirements, consistent with store plugin installation.

* fix(7-segment-clock): Update submodule with separator and spacing fixes

* fix(plugins): Add onchange handlers to existing custom feed inputs

- Add onchange handlers to key and value inputs for existing patternProperties fields
- Fixes bug where editing existing custom RSS feeds didn't save changes
- Ensures hidden JSON input field is updated when users edit feed entries
- Affects all plugins using patternProperties (custom_feeds, feed_logo_map, etc.)

* Add array-of-objects widget support to web UI

- Add support for rendering arrays of objects in web UI (for custom_feeds)
- Implement add/remove/update functions for array-of-objects widgets
- Support file-upload widgets within array items
- Update form data handling to support array JSON data fields

* Update plugins_manager.js cache-busting version

Update version parameter to force browser to load new JavaScript with array-of-objects widget support.

* Fix: Move array-of-objects detection before file-upload/checkbox checks

Move the array-of-objects widget detection to the top of the array handler so it's checked before file-upload and checkbox-group widgets. This ensures custom_feeds is properly detected as an array of objects.

* Update cache-busting version for array-of-objects fix

* Remove duplicate array-of-objects check

* Update cache version again

* Add array-of-objects widget support to server-side template

Add detection and rendering for array-of-objects in the Jinja2 template (plugin_config.html).
This enables the custom_feeds widget to display properly with name, URL, enabled checkbox, and logo upload fields.

The widget is detected by checking if prop.items.type == 'object' && prop.items.properties,
and is rendered before the file-upload widget check.

* Use window. prefix for array-of-objects JavaScript functions

Explicitly use window.addArrayObjectItem, window.removeArrayObjectItem, etc.
in the template to ensure the functions are accessible from inline event handlers.
Also add safety checks to prevent errors if functions aren't loaded yet.

* Fix syntax error: Missing indentation for html += in array else block

The html += statement was outside the else block, causing a syntax error.
Fixed by properly indenting it inside the else block.

* Update cache version for syntax fix

* Add debug logging to diagnose addArrayObjectItem availability

* Fix: Wrap array-of-objects functions in window check and move outside IIFE

Ensure functions are available globally by wrapping them in a window check
and ensuring they're defined outside any IIFE scope. Also fix internal
function calls to use window.updateArrayObjectData for consistency.

* Update cache version for array-of-objects fix

* Move array-of-objects functions outside IIFE to make them globally available

The functions were inside the IIFE scope, making them inaccessible from
inline event handlers. Moving them outside the IIFE ensures they're
available on window when the script loads.

* Update cache version for IIFE fix

* Fix: Add array-of-objects functions after IIFE ends

The functions were removed from inside the IIFE but never added after it.
Also removed orphaned code that was causing syntax errors.

* Update cache version for array-of-objects fix

* Fix: Remove all orphaned code and properly add array-of-objects functions after IIFE

* Add array-of-objects functions after IIFE ends

These functions must be outside the IIFE to be accessible from inline
event handlers in the server-rendered template.

* Update cache version for syntax fix

* Fix syntax error: Add missing closing brace for else block

* Update cache version for syntax fix

* Replace complex array-of-objects widget with simple table interface

- Replace nested array-of-objects widget with clean table interface
- Table shows: Name, URL, Logo (with upload), Enabled checkbox, Delete button
- Fix file-upload widget detection order to prevent breaking static-image plugin
- Add simple JavaScript functions for add/remove rows and logo upload
- Much more intuitive and easier to use

* Add simple table interface for custom feeds

- Replace complex array-of-objects widget with clean table
- Table columns: Name, URL, Logo (upload), Enabled checkbox, Delete
- Use dot notation for form field names (feeds.custom_feeds.0.name)
- Add JavaScript functions for add/remove rows and logo upload
- Fix file-upload detection order to prevent breaking static-image plugin

* Fix custom feeds table issues

- Fix JavaScript error in removeCustomFeedRow (get tbody before removing row)
- Improve array conversion logic to handle nested paths like feeds.custom_feeds
- Add better error handling and debug logging for array conversion
- Ensure dicts with numeric keys are properly converted to arrays before validation

* Add fallback fix for feeds.custom_feeds dict-to-array conversion

- Add explicit fallback conversion for feeds.custom_feeds if fix_array_structures misses it
- This ensures the dict with numeric keys is converted to an array before validation
- Logo field is already optional in schema (not in required array)

* feat(web): Add checkbox-group widget support for plugin config arrays

Add server-side rendering support for checkbox-group widget in plugin
configuration forms. This allows plugins to use checkboxes for multi-select
array fields instead of comma-separated text inputs.

The implementation:
- Checks for x-widget: 'checkbox-group' in schema
- Renders checkboxes for each enum item in items.enum
- Supports custom labels via x-options.labels
- Works with any plugin that follows the pattern

Already used by:
- ledmatrix-news plugin (enabled_feeds)
- odds-ticker plugin (enabled_leagues)

* feat(install): Add one-shot installation script

- Create comprehensive one-shot installer with robust error handling
- Includes network checks, disk space validation, and retry logic
- Handles existing installations gracefully (idempotent)
- Updates README with quick install command prominently featured
- Manual installation instructions moved to collapsible section

The script provides explicit error messages and never fails silently.
All prerequisites are validated before starting installation.

* fix: Remove accidental plugins/7-segment-clock submodule entry

Remove uninitialized submodule 'plugins/7-segment-clock' that was
accidentally included. This submodule is not related to the one-shot
installer feature and should not be part of this PR.

- Remove submodule entry from .gitmodules
- Remove submodule from git index
- Clean up submodule configuration

* fix(array-objects): Fix schema lookup, reindexing, and disable file upload

Address PR review feedback for array-of-objects helpers:

1. Schema resolution: Use getSchemaProperty() instead of manual traversal
   - Fixes nested array-of-objects schema lookup (e.g., news.custom_feeds)
   - Now properly descends through .properties for nested objects

2. Reindexing: Replace brittle regex with targeted patterns
   - Only replace index in bracket notation [0], [1], etc. for names
   - Only replace _item_<digits> pattern for IDs (not arbitrary digits)
   - Use specific function parameter patterns for onclick handlers
   - Prevents corruption of fieldId, pluginId, or other numeric values

3. File upload: Disable widget until properly implemented
   - Hide/disable upload button with clear message
   - Show existing logos if present but disable upload functionality
   - Prevents silent failures when users attempt to upload files
   - Added TODO comments for future implementation

Also fixes exit code handling in one-shot-install.sh to properly capture
first_time_install.sh exit status before error trap fires.

* fix(security): Fix XSS vulnerability in handleCustomFeedLogoUpload

Replace innerHTML usage with safe DOM manipulation using createElement
and setAttribute to prevent XSS when injecting uploadedFile.path and
uploadedFile.id values.

- Clear logoCell using textContent instead of innerHTML
- Create all DOM elements using document.createElement
- Set uploadedFile.path and uploadedFile.id via setAttribute (automatically escaped)
- Properly structure DOM tree by appending elements in order
- Prevents malicious HTML/script injection through file path or ID values

* fix: Update upload button onclick when reindexing custom feed rows

Fix removeCustomFeedRow to update button onclick handlers that reference
file input IDs with _logo_<index> when rows are reindexed after deletion.

Previously, after deleting a row, the upload button's onclick still referenced
the old file input ID, causing the upload functionality to fail.

Now properly updates:
- getElementById('..._logo_<num>') patterns in onclick handlers
- Other _logo_<num> patterns in button onclick strings
- Function parameter indices in onclick handlers

This ensures upload buttons continue to work correctly after row deletion.

* fix: Make custom feeds table widget-specific instead of generic fallback

Replace generic array-of-objects check with widget-specific check for
'custom-feeds' widget to prevent hardcoded schema from breaking other
plugins with different array-of-objects structures.

Changes:
- Check for x-widget == 'custom-feeds' before rendering custom feeds table
- Add schema validation to ensure required fields (name, url) exist
- Show warning message if schema doesn't match expected structure
- Fall back to generic array input for other array-of-objects schemas
- Add comments for future generic array-of-objects support

This ensures the hardcoded custom feeds table (name, url, logo, enabled)
only renders when explicitly requested via widget type, preventing
breakage for other plugins with different array-of-objects schemas.

* fix: Add image/gif to custom feed logo upload accept attribute

Update file input accept attributes for custom feed logo uploads to include
image/gif, making it consistent with the file-upload widget which also
allows GIF images.

Updated in three places:
- Template file input (plugin_config.html)
- JavaScript addCustomFeedRow function (base.html)
- Dynamic file input creation in handleCustomFeedLogoUpload (base.html)

All custom feed logo upload inputs now accept: image/png, image/jpeg,
image/bmp, image/gif

* fix: Add hidden input for enabled checkbox to ensure false is submitted

Add hidden input with value='false' before enabled checkbox in custom feeds
table to ensure an explicit false value is sent when checkbox is unchecked.

Pattern implemented:
- Hidden input: name='enabled', value='false' (always submitted)
- Checkbox: name='enabled', value='true' (only submitted when checked)
- When unchecked: only hidden input submits (false)
- When checked: both submit, checkbox value (true) overwrites hidden

Updated in two places:
- Template checkbox in plugin_config.html (existing rows)
- JavaScript addCustomFeedRow function in base.html (new rows)

Backend verification:
- Backend (api_v3.py) handles string boolean values and converts properly
- JavaScript form processing explicitly checks element.checked, independent of this pattern
- Standard form submission uses last value when multiple values share same name

* fix: Expose renderArrayObjectItem to window for addArrayObjectItem

Fix scope issue where renderArrayObjectItem is defined inside IIFE but
window.addArrayObjectItem is defined outside, causing the function check
to always fail and fallback to degraded HTML rendering.

Problem:
- renderArrayObjectItem (line 2469) is inside IIFE (lines 796-6417)
- window.addArrayObjectItem (line 6422) is outside IIFE
- Check 'typeof renderArrayObjectItem === function' at line 6454 always fails
- Fallback code lacks file upload widgets, URL input types, descriptions, styling

Solution:
- Expose renderArrayObjectItem to window object before IIFE closes
- Function maintains closure access to escapeHtml and other IIFE-scoped functions
- Newly added items now have full functionality matching initially rendered items

* fix: Reorder array type checks to match template order

Fix inconsistent rendering where JavaScript and Jinja template had opposite
ordering for array type checks, causing schemas with both x-widget: file-upload
AND items.type: object (like static-image) to render differently.

Problem:
- Template checks file-upload FIRST (to avoid breaking static-image plugin)
- JavaScript checked array-of-objects FIRST
- Server-rendered forms showed file-upload widget correctly
- JS-rendered forms incorrectly displayed array-of-objects table widget

Solution:
- Reorder JavaScript checks to match template order:
  1. Check file-upload widget FIRST
  2. Check checkbox-group widget
  3. Check custom-feeds widget
  4. Check array-of-objects as fallback
  5. Regular array input (comma-separated)

This ensures consistent rendering between server-rendered and JS-rendered forms
for schemas that have both x-widget: file-upload AND items.type: object.

* fix: Handle None value for feeds config to prevent TypeError

Fix crash when plugin_config['feeds'] exists but is None, causing
TypeError when checking 'custom_feeds' in feeds_config.

Problem:
- When plugin_config['feeds'] exists but is None, dict.get('feeds', {})
  returns None (not the default {}) because dict.get() only uses default
  when key doesn't exist, not when value is None
- Line 3642's 'custom_feeds' in feeds_config raises TypeError because
  None is not iterable
- This can crash the API endpoint if a plugin config has feeds: null

Solution:
- Change plugin_config.get('feeds', {}) to plugin_config.get('feeds') or {}
  to ensure feeds_config is always a dict (never None)
- Add feeds_config check before 'in' operator for extra safety

This ensures the code gracefully handles feeds: null in plugin configuration.

* fix: Add default value for AVAILABLE_SPACE to prevent TypeError

Fix crash when df produces unexpected output that results in empty
AVAILABLE_SPACE variable, causing 'integer expression expected' error.

Problem:
- df may produce unexpected output format (different locale, unusual
  filesystem name spanning lines, or non-standard df implementation)
- While '|| echo "0"' handles pipeline failures, it doesn't trigger if
  awk succeeds but produces no output (empty string)
- When AVAILABLE_SPACE is empty, comparison [ "$AVAILABLE_SPACE" -lt 500 ]
  fails with 'integer expression expected' error
- With set -e, this causes script to exit unexpectedly

Solution:
- Add AVAILABLE_SPACE=${AVAILABLE_SPACE:-0} before comparison to ensure
  variable always has a numeric value (defaults to 0 if empty)
- This gracefully handles edge cases where df/awk produces unexpected output

* fix: Wrap debug console.log in debug flag check

Fix unconditional debug logging that outputs internal implementation
details to browser console for all users.

Problem:
- console.log('[ARRAY-OBJECTS] Functions defined on window:', ...)
  executes unconditionally when page loads
- Outputs debug information about function availability to all users
- Appears to be development/debugging code inadvertently included
- Noisy console output in production

Solution:
- Wrap console.log statement in _PLUGIN_DEBUG_EARLY check to only
  output when pluginDebug localStorage flag is enabled
- Matches pattern used elsewhere in the file for debug logging
- Debug info now only visible when explicitly enabled via
  localStorage.setItem('pluginDebug', 'true')

* fix: Expose getSchemaProperty, disable upload widget, handle bracket notation arrays

Multiple fixes for array-of-objects and form processing:

1. Expose getSchemaProperty to window (plugins_manager.js):
   - getSchemaProperty was defined inside IIFE but needed by global functions
   - Added window.getSchemaProperty = getSchemaProperty before IIFE closes
   - Updated window.addArrayObjectItem to use window.getSchemaProperty
   - Fixes ReferenceError when dynamically adding array items

2. Disable upload widget for custom feeds (plugin_config.html):
   - File input and Upload button were still active but should be disabled
   - Removed onchange/onclick handlers, added disabled and aria-disabled
   - Added visible disabled styling and tooltip
   - Existing logos continue to display but uploads are prevented
   - Matches PR objectives to disable upload until fully implemented

3. Handle bracket notation array fields (api_v3.py):
   - checkbox-group uses name="field_name[]" which sends multiple values
   - request.form.to_dict() collapses duplicate keys (only keeps last value)
   - Added handling to detect fields ending with "[]" before to_dict()
   - Use request.form.getlist() to get all values, combine as comma-separated
   - Processed before existing array index field handling
   - Fixes checkbox-group losing all but last selected value

* fix: Remove duplicate submit handler to prevent double POSTs

Remove document-level submit listener that conflicts with handlePluginConfigSubmit,
causing duplicate form submissions with divergent payloads.

Problem:
- handlePluginConfigSubmit correctly parses JSON from _data fields and maps to
  flatConfig[baseKey] for patternProperties and array-of-objects
- Document-level listener (line 5368) builds its own config without understanding
  _data convention and posts independently via savePluginConfiguration
- Every submit now sends two POSTs with divergent payloads:
  - First POST: Correct structure with parsed _data fields
  - Second POST: Incorrect structure with raw _data fields, missing structure
- Arrays-of-objects and patternProperties saved incorrectly in second request

Solution:
- Remove document-level submit listener for #plugin-config-form
- Rely solely on handlePluginConfigSubmit which is already attached to the form
- handlePluginConfigSubmit properly handles all form-to-config conversion including:
  - _data field parsing (JSON from hidden fields)
  - Type-aware conversion using schema
  - Dot notation to nested object conversion
  - PatternProperties and array-of-objects support

Note: savePluginConfiguration function remains for use by JSON editor saves

* fix: Use indexed names for checkbox-group to work with existing parser

Change checkbox-group widget to use indexed field names instead of bracket
notation, so the existing indexed field parser correctly handles multiple
selected values.

Problem:
- checkbox-group uses name="{{ full_key }}[]" which requires bracket
  notation handling in backend
- While bracket notation handler exists, using indexed names is more robust
  and leverages existing well-tested indexed field parser
- Indexed field parser already handles fields like "field_name.0",
  "field_name.1" correctly

Solution:
- Template: Change name="{{ full_key }}[]" to name="{{ full_key }}.{{
  loop.index0 }}"
- JavaScript: Update checkbox-group rendering to use name="."
- Backend indexed field parser (lines 3364-3388) already handles this pattern:
  - Detects fields ending with numeric indices (e.g., ".0", ".1")
  - Groups them by base_path and sorts by index
  - Combines into array correctly

This ensures checkbox-group values are properly preserved when multiple
options are selected, working with the existing schema-based parsing system.

* fix: Set values from item data in fallback array-of-objects rendering

Fix fallback code path for rendering array-of-objects items to properly
set input values from existing item data, matching behavior of proper
renderArrayObjectItem function.

Problem:
- Fallback code at lines 3078-3091 and 6471-6486 creates input elements
  without setting values from existing item data
- Text inputs have no value attribute set
- Checkboxes have no checked attribute computed from item properties
- Users would see empty form fields instead of existing configuration data
- Proper renderArrayObjectItem function correctly sets values (line 2556)

Solution:
- Extract propValue from item data: item[propKey] with schema default fallback
- For text inputs: Set value attribute with HTML-escaped propValue
- For checkboxes: Set checked attribute based on propValue truthiness
- Add inline HTML escaping for XSS prevention (since fallback code may
  run outside IIFE scope where escapeHtml function may not be available)

This ensures fallback rendering displays existing data correctly when
window.renderArrayObjectItem is not available.

* fix: Remove extra closing brace breaking if/else chain

Remove stray closing brace at line 3127 that was breaking the if/else chain
before the 'else if (prop.enum)' branch, causing 'Unexpected token else'
syntax error.

Problem:
- Extra '}' at line 3127 closed the prop.type === 'array' block prematurely
- This broke the if/else chain, causing syntax error when parser reached
  'else if (prop.enum)' at line 3128
- Structure was: } else if (array) { ... } } } else if (enum) - extra brace

Solution:
- Removed the extra closing brace at line 3127
- Structure now correctly: } else if (array) { ... } } else if (enum)
- Verified with Node.js syntax checker - no errors

* fix: Remove local logger assignments to prevent UnboundLocalError

Remove all local logger assignments inside save_plugin_config function that
were shadowing the module-level logger, causing UnboundLocalError when nested
helpers like normalize_config_values() or debug checks reference logger before
those assignments run.

Problem:
- Module-level logger exists at line 13: logger = logging.getLogger(__name__)
- Multiple local assignments inside save_plugin_config (lines 3361, 3401, 3421,
  3540, 3660, 3977, 4093, 4118) make logger a local variable for entire function
- Python treats logger as local for entire function scope when any assignment
  exists, causing UnboundLocalError if logger is used before assignments
- Nested helpers like normalize_config_values() or debug checks that reference
  logger before local assignments would fail

Solution:
- Removed all local logger = logging.getLogger(__name__) assignments in
  save_plugin_config function
- Use module-level logger directly throughout the function
- Removed redundant import logging statements that were only used for logger
- This ensures logger is always available and references the module-level logger

All logger references now use the module-level logger without shadowing.

* fix: Fix checkbox-group serialization and array-of-objects key leakage

Multiple fixes for array-of-objects and checkbox-group widgets:

1. Fix checkbox-group serialization (JS and template):
   - Changed from indexed names (categories.0, categories.1) to _data pattern
   - Added updateCheckboxGroupData() function to sync selected values
   - Hidden input stores JSON array of selected enum values
   - Checkboxes use data-checkbox-group and data-option-value attributes
   - Fixes issue where config.categories became {0: true, 1: true} instead of ['nfl', 'nba']
   - Now correctly serializes to array using existing _data handling logic

2. Prevent array-of-objects per-item key leakage:
   - Added skip pattern in handlePluginConfigSubmit for _item_<n>_ names
   - Removed name attributes from per-item inputs in renderArrayObjectItem
   - Per-item inputs now rely solely on hidden _data field
   - Prevents feeds_item_0_name from leaking into flatConfig

3. Add type coercion to updateArrayObjectData:
   - Consults itemsSchema.properties[propKey].type for coercion
   - Handles integer and number types correctly
   - Preserves string values as-is
   - Ensures numeric fields in array items are stored as numbers

4. Ensure currentPluginConfig is always available:
   - Updated addArrayObjectItem to check window.currentPluginConfig first
   - Added error logging if schema not available
   - Prevents ReferenceError when global helpers need schema

This ensures checkbox-group arrays serialize correctly and array-of-objects
per-item fields don't leak extra keys into the configuration.

* fix: Make _data field matching more specific to prevent false positives

Fix overly broad condition that matched any field containing '_data',
causing false positives and inconsistent key transformation.

Problem:
- Condition 'key.endsWith('_data') || key.includes('_data')' matches any
  field containing '_data' anywhere (e.g., 'meta_data_field', 'custom_data_config')
- key.replace(/_data$/, '') only removes '_data' from end, making logic inconsistent
- Fields with '_data' in middle get matched but key isn't transformed
- If their value happens to be valid JSON, it gets incorrectly parsed

Solution:
- Remove 'key.includes('_data')' clause
- Only check 'key.endsWith('_data')' to match actual _data suffix pattern
- Ensures consistent matching: only fields ending with '_data' are treated
  as JSON data fields, and only those get the suffix removed
- Prevents false positives on fields like 'meta_data_field' that happen to
  contain '_data' in their name

* fix: Add HTML escaping to prevent XSS in fallback code and checkbox-group

Add proper HTML escaping for schema-derived values to prevent XSS vulnerabilities
in fallback rendering code and checkbox-group widget.

Problem:
- Fallback code in generateFieldHtml (line 3094) doesn't escape propLabel
  when building HTML strings, while main renderArrayObjectItem uses escapeHtml()
- Checkbox-group widget (lines 3012-3025) doesn't escape option or label values
- While risk is limited (values come from plugin schemas), malicious plugin
  schemas or untrusted schema sources could inject XSS
- Inconsistent with main renderArrayObjectItem which properly escapes

Solution:
- Added escapeHtml() calls for propLabel in fallback array-of-objects rendering
  (both locations: generateFieldHtml and addArrayObjectItem fallback)
- Added escapeHtml() calls for option values in checkbox-group widget:
  - checkboxId (contains option)
  - data-option-value attribute
  - value attribute
  - label text in span
- Ensures consistent XSS protection across all rendering paths

This prevents potential XSS if plugin schemas contain malicious HTML/script
content in enum values or property titles.

* fix: Recreate one-shot install script with APT permission and non-interactive fixes

Recreate one-shot install script that was deleted, with fixes for:
1. APT permission denied errors on /tmp
2. Non-interactive mode support

Fixes:
1. Fix /tmp permissions before running first_time_install.sh:
   - chmod 1777 /tmp to ensure APT can write temp files
   - Set TMPDIR=/tmp explicitly
   - Preserve TMPDIR when using sudo -E

2. Enable non-interactive mode:
   - Pass -y flag or LEDMATRIX_ASSUME_YES=1 to first_time_install.sh
   - Prevents read prompt failure at line 242 when run via curl | bash

3. Better error handling:
   - Temporarily disable errexit to capture exit code
   - Re-enable errexit after capturing
   - Added fix_tmp_permissions() function

This resolves the 'Permission denied' errors for APT temp files and the
interactive prompt failure when running via pipe.

* fix: Pass both -y flag and env var to first_time_install.sh for non-interactive mode

Ensure first_time_install.sh runs in non-interactive mode by passing both:
1. The -y command-line flag
2. The LEDMATRIX_ASSUME_YES=1 environment variable

This is necessary because first_time_install.sh re-executes itself with sudo
if not running as root (line 131), and we need to ensure the non-interactive
flag is preserved through the re-execution.

Also added debug_install.sh diagnostic script to help troubleshoot
installation failures on the Pi.

* fix: Improve /tmp permission handling and non-interactive mode detection

Improve handling of /tmp permissions and non-interactive mode:

1. /tmp permissions fix:
   - Check current permissions before attempting to fix
   - Display warning when fixing incorrect permissions (2775 -> 1777)
   - Verify /tmp has permissions 1777 (sticky bit + world writable)

2. Non-interactive mode detection:
   - Redirect stdin from /dev/null when running via sudo to prevent
     read commands from hanging when stdin is not a TTY
   - Add better error message in first_time_install.sh when non-interactive
     mode is detected but ASSUME_YES is not set
   - Check if stdin is a TTY before attempting interactive read

This fixes the issues identified in diagnostic output:
- /tmp permissions 2775 causing APT write failures
- read -p failing when stdin is not a TTY (curl | bash)

Fixes installation failures when running one-shot install via curl | bash.

* refactor: Simplify /tmp permission handling - only fix if actually wrong

Simplify /tmp permission handling:
- Only check and fix /tmp permissions if they're actually incorrect (not preemptively)
- Remove redundant fix_tmp_permissions() call from prerequisites check
- Keep the fix inline where first_time_install.sh is executed
- When running manually, /tmp usually has correct permissions (1777) so no fix needed

This makes the script less aggressive and avoids unnecessary permission changes
when running manually, while still fixing the issue in automated scenarios.

* fix: Remove user confirmation prompts in install_wifi_monitor.sh for non-interactive mode

Make install_wifi_monitor.sh respect non-interactive mode:

1. Package installation prompt (line 48):
   - Check for ASSUME_YES or LEDMATRIX_ASSUME_YES environment variable
   - If set, automatically install required packages without prompting
   - If stdin is not a TTY (non-interactive), also auto-install packages
   - Only prompt user in true interactive mode (TTY available)

2. Continue installation prompt (line 145):
   - Already checks for ASSUME_YES, but now also checks LEDMATRIX_ASSUME_YES
   - Skip prompt if stdin is not a TTY
   - Proceed automatically in non-interactive mode

This fixes installation failures at step 8.5 when running via one-shot
installer or with -y flag, as the script was hanging on user prompts.

* fix: Explicitly pass ASSUME_YES to install_wifi_monitor.sh and simplify package installation

Fix WiFi monitor installation failing at step 8.5:

1. Explicitly pass ASSUME_YES environment variable when calling
   install_wifi_monitor.sh from first_time_install.sh to ensure
   non-interactive mode is respected

2. Simplify package installation logic in install_wifi_monitor.sh:
   - Use apt directly when running as root (from first_time_install.sh)
   - Use sudo when running as regular user (direct script execution)
   - Always install packages automatically in non-interactive mode
   - Only prompt in true interactive mode (TTY available and ASSUME_YES not set)

This ensures packages are installed automatically when running via
one-shot installer or with -y flag, preventing installation failures
at step 8.5.

* refactor: Remove all prompts from install_wifi_monitor.sh - install packages automatically

Simplify WiFi monitor installation by removing all user prompts:

1. Package installation: Always install required packages automatically
   - No prompt for missing packages (hostapd, dnsmasq, network-manager)
   - Just install them if missing

2. Network connection warning: Remove prompt to continue
   - Just display informational message and proceed
   - WiFi monitor will handle AP mode automatically if no network

3. Remove ASSUME_YES environment variable passing from first_time_install.sh
   - No longer needed since script has no prompts

This makes the installation completely non-interactive and simpler,
preventing any hangs or failures at step 8.5.

* fix: Address multiple issues in debug script, array rendering, and custom feeds

1. debug_install.sh: Make log path dynamic instead of hardcoded
   - Compute project root from script location
   - Use dynamic LOG_DIR instead of hardcoded /home/ledpi/LEDMatrix/logs/
   - Works from any clone location and user

2. plugins_manager.js renderArrayObjectItem: Fix XSS and metadata issues
   - HTML-escape logoValue.path in img src attribute (XSS prevention)
   - Add data-file-data attribute to preserve file metadata for serialization
   - Add data-prop-key attribute for proper property tracking
   - Use schema-driven remove button label (x-removeLabel) with fallback to 'Remove item'

3. base.html addCustomFeedRow: Fix duplicate enabled field and hardcoded pluginId
   - Remove duplicate hidden input for enabled field (checkbox alone is sufficient)
   - Add pluginId parameter to function signature
   - Pass pluginId to handleCustomFeedLogoUpload instead of hardcoded 'ledmatrix-news'
   - Update caller in plugin_config.html to pass plugin_id

These fixes improve security (XSS prevention), functionality (metadata
preservation), and maintainability (no hardcoded values).

* fix: Make install_wifi_monitor.sh more resilient to failures

Make install_wifi_monitor.sh handle errors more gracefully:

1. Remove unnecessary sudo when running as root:
   - Check EUID before using sudo for systemctl commands
   - Use systemctl directly when running as root
   - Use sudo only when running as regular user

2. Add error handling for package installation:
   - Continue even if apt update fails (just warn)
   - Continue even if apt install fails (warn and provide manual install command)
   - Allow installation to continue even if packages fail

3. Make service operations more resilient:
   - Remove sudo when running as root
   - Allow service start to fail without exiting script
   - Print warning if service fails to start
   - Service will still be enabled and may start on reboot

Note: Script still uses 'set -e' but errors in critical paths are handled
with || operators to prevent exit. This prevents the script from exiting
with code 1 when called from first_time_install.sh, allowing the
installation to continue even if some WiFi-related operations fail.

* fix: Make WiFi monitor installation failure non-fatal in first_time_install.sh

Make the WiFi monitor service installation optional/non-fatal:

1. Capture exit code from install_wifi_monitor.sh but don't fail installation
2. Continue installation even if WiFi monitor installation fails
3. Provide clear messages about the failure but allow installation to proceed
4. Check for service file creation and provide helpful messages

WiFi monitor is optional functionality - the main LED Matrix installation
should succeed even if WiFi monitor setup fails (e.g., package installation
issues, service start failures, etc.). Users can install it later if needed.

This prevents the entire installation from failing at step 8.5 due to
WiFi monitor installation issues.

* fix: Use JSON encoding for bracket-notation arrays and add sentinel for clearing

Fix bracket-notation array handling to prevent data loss:

1. Use JSON encoding instead of comma-join (lines 3358-3359):
   - Comma-join breaks if option values contain commas
   - Switch to json.dumps() to encode array values as JSON strings
   - _parse_form_value_with_schema() already handles JSON arrays correctly
   - Preserves values with commas, special characters, etc.

2. Add sentinel hidden input for clearing arrays:
   - Add hidden input with name="field[]" value="" in checkbox-group template
   - Ensures field is always submitted, even when all checkboxes unchecked
   - Backend filters out sentinel empty strings to detect empty array
   - Allows users to clear array to [] by unchecking all options

3. Update backend to handle sentinel:
   - Filter out sentinel empty strings from bracket notation values
   - Empty array (all unchecked) is represented as "[]" JSON string
   - Properly handles both sentinel-only (empty array) and sentinel+values cases

This fixes data loss when:
- Option values contain commas (comma-join corruption)
- All checkboxes are unchecked (field omitted from form, can't clear to [])

* fix: Harden upload flow - HTTP status check, path normalization, property assignment

Fix three security and reliability issues in upload flow:

1. Check HTTP status before calling response.json():
   - Prevents JSON parsing errors on non-2xx responses
   - Properly handles error responses with status codes
   - Returns error text if available for better debugging
   - Prevents masking of HTTP errors

2. Normalize uploadedFile.path before using in img src:
   - Remove leading slashes with replace(/^\/+/, '')
   - Add single leading slash for image src
   - Prevents //host/odd paths that could cause security issues
   - Ensures consistent path format

3. Replace string-based handlers with property assignment:
   - Replace setAttribute('onchange', ...) with addEventListener('change', ...)
   - Replace setAttribute('onclick', ...) with addEventListener('click', ...)
   - Refactor addCustomFeedRow to use DOM manipulation instead of innerHTML
   - Prevents injection vulnerabilities from string interpolation
   - Uses property assignment (img.src, input.name, input.value) instead of setAttribute where appropriate

These changes improve security by eliminating XSS injection surfaces
and improve reliability by properly handling HTTP errors and path formats.

* fix: Add bracket notation to checkbox-group input names

The backend expects checkbox groups to submit with bracket notation
(request.form.getlist("<field>[]")), but the templates were rendering
checkboxes without the "[]" suffix in the name attribute.

Changes:
1. Add name="{{ full_key }}[]" to checkbox inputs in plugin_config.html
2. Add name="${fullKey}[]" to checkbox inputs in plugins_manager.js

This ensures:
- Checked checkboxes submit their values with the bracket notation
- Backend can use request.form.getlist("<field>[]") to collect all values
- Sentinel hidden input (already using bracket notation) works correctly
- Backend bracket_array_fields logic receives and processes the array values

The sentinel hidden input ensures the field is always submitted (even
when all checkboxes are unchecked), allowing the backend to detect and
set empty arrays correctly.

* fix: Swap order of enabled checkbox and hidden input in custom-feeds

The hidden input with value="false" was rendered before the checkbox,
causing request.form.to_dict() to use the hidden input's value instead
of the checkbox's "true" value when checked.

Fix by rendering the checkbox first, then the hidden fallback input.
This ensures that when the checkbox is checked, its "true" value
overwrites the hidden input's "false" value in request.form.to_dict().

The hidden input still serves as a fallback to ensure "false" is
submitted when the checkbox is unchecked (since unchecked checkboxes
don't submit a value).

* fix: Enable upload buttons for existing custom feed rows in template

The template was rendering disabled upload buttons for existing custom
feed rows with the message "Logo upload for custom feeds is not yet
implemented", while the JavaScript addCustomFeedRow function creates
working upload buttons for newly added rows. This created confusing UX
where users saw disabled buttons on existing feeds but working buttons
on newly added feeds.

Since handleCustomFeedLogoUpload is fully implemented and functional,
enable the upload buttons in the template to match the JavaScript
behavior:

1. Remove disabled and aria-disabled attributes from file input
2. Remove disabled, aria-disabled, misleading title, and update button
   styling to match working buttons (remove cursor-not-allowed and
   opacity-50, add hover:bg-gray-300)
3. Add onchange handler to file input calling handleCustomFeedLogoUpload
4. Add onclick handler to button to trigger file input click

This ensures consistent UX across existing and newly added custom feed
rows, with all upload buttons functional.

* fix: Expose escapeHtml to window object for use by global functions

The escapeHtml function is defined inside the IIFE (at line 5445) but is
called at line 6508 from within window.addArrayObjectItem, which is
defined outside the IIFE (starting at line 6465). Since escapeHtml is
not exposed to the window object (unlike renderArrayObjectItem and
getSchemaProperty which are exposed at lines 6457-6458), the fallback
code path throws a ReferenceError: escapeHtml is not defined when
window.renderArrayObjectItem is unavailable.

Fix by exposing escapeHtml to the window object alongside
renderArrayObjectItem and getSchemaProperty, ensuring the fallback code
in window.addArrayObjectItem can safely call escapeHtml when the primary
rendering function fails to load.

This prevents users from being unable to add new items to array-of-objects
fields when the primary rendering function is unavailable.

* fix: Escape single quotes in checkbox-group JSON value attribute

The hidden input for checkbox-group uses a single-quoted value attribute
with {{ array_value|tojson|safe }}, but the tojson filter doesn't escape
single quotes for HTML attributes. While JSON uses double quotes for
strings, if array_value contains strings with single quotes (like
"Tom's Choice"), the resulting HTML value='["Tom's Choice"]' could
have parsing issues in some browsers when the single quote appears inside
the JSON string content.

The JavaScript equivalent at line 3037 correctly escapes single quotes
with .replace(/'/g, "&#39;"), but the Jinja2 template lacked this
escaping.

Fix by applying the replace filter to escape single quotes:
{{ (array_value|tojson|safe)|replace("'", "&#39;") }}

This ensures consistent behavior between server-side template rendering
and client-side JavaScript rendering, and prevents potential HTML attribute
parsing issues.

* fix: Move hidden input before checkbox for enabled field in custom-feeds

The hidden input and checkbox share the same name, causing duplicate form
values. When request.form.to_dict() processes multiple fields with the same
name, it uses the LAST value.

The previous fix (a315693b) had the checkbox first and hidden input second,
which meant the hidden input's "false" value would override the checkbox's
"true" value when checked.

Fix by moving the hidden input BEFORE the checkbox, so:
- When checkbox is checked: checkbox value ("true") overrides hidden ("false")
- When checkbox is unchecked: hidden input value ("false") is used (checkbox
  doesn't submit a value)

This ensures the correct boolean value is submitted in both cases.

* fix: Use dataset-driven indices for custom feed row reindexing

After removeCustomFeedRow() reindexes data-index/id/name, the existing
file-input change handlers still used stale closure indices, causing
querySelector to fail and preventing logo uploads from working.

Fix by using dataset-driven indices instead of closure-captured values:

1. In addCustomFeedRow:
   - Store index in fileInput.dataset.index
   - Read index from e.target.dataset.index in event handler
   - Use fileInput.click() directly instead of getElementById

2. In removeCustomFeedRow:
   - Update dataset.index for all inputs during reindexing
   - Remove onclick/onchange attribute rewriting (handlers use addEventListener)
   - Simplify ID updating to handle both _logo_<n> and _logo_preview_<n>

3. In handleCustomFeedLogoUpload:
   - Store index in fileInput.dataset.index
   - Read index from e.target.dataset.index in event handler
   - Use fileInput.click() directly
   - Set pathInput.value to imageSrc (normalized path)
   - Reset event.target.value to allow re-uploading the same file

This ensures event handlers always use the current index from the DOM,
preventing stale closure issues after row removal and reindexing.

* fix: Reset file input value to allow re-uploading same file

Add event.target.value = '' after successful upload to allow re-uploading
the same file (change event won't fire otherwise if the same file is
selected again).

* fix: Add proper attribute escaping for renderArrayObjectItem

The renderArrayObjectItem function was vulnerable because escapeHtml does
not properly escape attribute contexts (quotes). This could lead to XSS
if user-provided data contains quotes or other special characters in
attribute values.

Changes:
1. Create escapeAttribute function for proper attribute escaping
   - Escapes quotes, ampersands, and other special characters
   - Handles null/undefined values safely

2. Update renderArrayObjectItem to use escapeAttribute for all attribute values:
   - id attributes (itemId, propKey)
   - data-* attributes (data-prop-key, data-file-data)
   - value attributes (input values)
   - placeholder attributes
   - title attributes
   - src attributes (img src)
   - onclick/onchange handler parameters (fieldId)

3. Safely encode JSON in data-file-data attribute:
   - Use base64 encoding (btoa) instead of manual quote escaping
   - Decode with atob when reading the attribute
   - This safely handles all characters including quotes, newlines, etc.

4. Remove hardcoded 'ledmatrix-news' pluginId fallback:
   - Change fallback from 'ledmatrix-news' to null
   - Prevents surprising defaults when uploads are enabled later
   - Requires explicit pluginId configuration

This ensures all attribute values are properly escaped and prevents
XSS vulnerabilities from unescaped quotes or special characters.

* fix: Expose escapeAttribute to window object

The escapeAttribute function was not exposed to the window object, which
could cause issues if other code needs to use it. Expose it alongside
escapeHtml for consistency.

---------

Co-authored-by: Chuck <chuck@example.com>
2026-01-11 16:38:55 -05:00
Chuck
b9f839af3d Revise README for core features and plugins (#177)
Updated core features and plugin information in README.

Signed-off-by: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
2026-01-09 21:57:24 -05:00
Chuck
f438f9dfe3 Remove breaking changes and migration details (#176)
Removed breaking changes section and related migration instructions from the README.

Signed-off-by: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
2026-01-09 16:39:25 -05:00
Chuck
7f230f625d Feature/one shot installer (#175)
* fix(plugins): Remove compatible_versions requirement from single plugin install

Remove compatible_versions from required fields in install_from_url method
to match install_plugin behavior. This allows installing plugins from URLs
without manifest version requirements, consistent with store plugin installation.

* fix(7-segment-clock): Update submodule with separator and spacing fixes

* fix(plugins): Add onchange handlers to existing custom feed inputs

- Add onchange handlers to key and value inputs for existing patternProperties fields
- Fixes bug where editing existing custom RSS feeds didn't save changes
- Ensures hidden JSON input field is updated when users edit feed entries
- Affects all plugins using patternProperties (custom_feeds, feed_logo_map, etc.)

* Add array-of-objects widget support to web UI

- Add support for rendering arrays of objects in web UI (for custom_feeds)
- Implement add/remove/update functions for array-of-objects widgets
- Support file-upload widgets within array items
- Update form data handling to support array JSON data fields

* Update plugins_manager.js cache-busting version

Update version parameter to force browser to load new JavaScript with array-of-objects widget support.

* Fix: Move array-of-objects detection before file-upload/checkbox checks

Move the array-of-objects widget detection to the top of the array handler so it's checked before file-upload and checkbox-group widgets. This ensures custom_feeds is properly detected as an array of objects.

* Update cache-busting version for array-of-objects fix

* Remove duplicate array-of-objects check

* Update cache version again

* Add array-of-objects widget support to server-side template

Add detection and rendering for array-of-objects in the Jinja2 template (plugin_config.html).
This enables the custom_feeds widget to display properly with name, URL, enabled checkbox, and logo upload fields.

The widget is detected by checking if prop.items.type == 'object' && prop.items.properties,
and is rendered before the file-upload widget check.

* Use window. prefix for array-of-objects JavaScript functions

Explicitly use window.addArrayObjectItem, window.removeArrayObjectItem, etc.
in the template to ensure the functions are accessible from inline event handlers.
Also add safety checks to prevent errors if functions aren't loaded yet.

* Fix syntax error: Missing indentation for html += in array else block

The html += statement was outside the else block, causing a syntax error.
Fixed by properly indenting it inside the else block.

* Update cache version for syntax fix

* Add debug logging to diagnose addArrayObjectItem availability

* Fix: Wrap array-of-objects functions in window check and move outside IIFE

Ensure functions are available globally by wrapping them in a window check
and ensuring they're defined outside any IIFE scope. Also fix internal
function calls to use window.updateArrayObjectData for consistency.

* Update cache version for array-of-objects fix

* Move array-of-objects functions outside IIFE to make them globally available

The functions were inside the IIFE scope, making them inaccessible from
inline event handlers. Moving them outside the IIFE ensures they're
available on window when the script loads.

* Update cache version for IIFE fix

* Fix: Add array-of-objects functions after IIFE ends

The functions were removed from inside the IIFE but never added after it.
Also removed orphaned code that was causing syntax errors.

* Update cache version for array-of-objects fix

* Fix: Remove all orphaned code and properly add array-of-objects functions after IIFE

* Add array-of-objects functions after IIFE ends

These functions must be outside the IIFE to be accessible from inline
event handlers in the server-rendered template.

* Update cache version for syntax fix

* Fix syntax error: Add missing closing brace for else block

* Update cache version for syntax fix

* Replace complex array-of-objects widget with simple table interface

- Replace nested array-of-objects widget with clean table interface
- Table shows: Name, URL, Logo (with upload), Enabled checkbox, Delete button
- Fix file-upload widget detection order to prevent breaking static-image plugin
- Add simple JavaScript functions for add/remove rows and logo upload
- Much more intuitive and easier to use

* Add simple table interface for custom feeds

- Replace complex array-of-objects widget with clean table
- Table columns: Name, URL, Logo (upload), Enabled checkbox, Delete
- Use dot notation for form field names (feeds.custom_feeds.0.name)
- Add JavaScript functions for add/remove rows and logo upload
- Fix file-upload detection order to prevent breaking static-image plugin

* Fix custom feeds table issues

- Fix JavaScript error in removeCustomFeedRow (get tbody before removing row)
- Improve array conversion logic to handle nested paths like feeds.custom_feeds
- Add better error handling and debug logging for array conversion
- Ensure dicts with numeric keys are properly converted to arrays before validation

* Add fallback fix for feeds.custom_feeds dict-to-array conversion

- Add explicit fallback conversion for feeds.custom_feeds if fix_array_structures misses it
- This ensures the dict with numeric keys is converted to an array before validation
- Logo field is already optional in schema (not in required array)

* feat(web): Add checkbox-group widget support for plugin config arrays

Add server-side rendering support for checkbox-group widget in plugin
configuration forms. This allows plugins to use checkboxes for multi-select
array fields instead of comma-separated text inputs.

The implementation:
- Checks for x-widget: 'checkbox-group' in schema
- Renders checkboxes for each enum item in items.enum
- Supports custom labels via x-options.labels
- Works with any plugin that follows the pattern

Already used by:
- ledmatrix-news plugin (enabled_feeds)
- odds-ticker plugin (enabled_leagues)

* feat(install): Add one-shot installation script

- Create comprehensive one-shot installer with robust error handling
- Includes network checks, disk space validation, and retry logic
- Handles existing installations gracefully (idempotent)
- Updates README with quick install command prominently featured
- Manual installation instructions moved to collapsible section

The script provides explicit error messages and never fails silently.
All prerequisites are validated before starting installation.

* fix: Remove accidental plugins/7-segment-clock submodule entry

Remove uninitialized submodule 'plugins/7-segment-clock' that was
accidentally included. This submodule is not related to the one-shot
installer feature and should not be part of this PR.

- Remove submodule entry from .gitmodules
- Remove submodule from git index
- Clean up submodule configuration

* fix(array-objects): Fix schema lookup, reindexing, and disable file upload

Address PR review feedback for array-of-objects helpers:

1. Schema resolution: Use getSchemaProperty() instead of manual traversal
   - Fixes nested array-of-objects schema lookup (e.g., news.custom_feeds)
   - Now properly descends through .properties for nested objects

2. Reindexing: Replace brittle regex with targeted patterns
   - Only replace index in bracket notation [0], [1], etc. for names
   - Only replace _item_<digits> pattern for IDs (not arbitrary digits)
   - Use specific function parameter patterns for onclick handlers
   - Prevents corruption of fieldId, pluginId, or other numeric values

3. File upload: Disable widget until properly implemented
   - Hide/disable upload button with clear message
   - Show existing logos if present but disable upload functionality
   - Prevents silent failures when users attempt to upload files
   - Added TODO comments for future implementation

Also fixes exit code handling in one-shot-install.sh to properly capture
first_time_install.sh exit status before error trap fires.

* fix(security): Fix XSS vulnerability in handleCustomFeedLogoUpload

Replace innerHTML usage with safe DOM manipulation using createElement
and setAttribute to prevent XSS when injecting uploadedFile.path and
uploadedFile.id values.

- Clear logoCell using textContent instead of innerHTML
- Create all DOM elements using document.createElement
- Set uploadedFile.path and uploadedFile.id via setAttribute (automatically escaped)
- Properly structure DOM tree by appending elements in order
- Prevents malicious HTML/script injection through file path or ID values

* fix: Update upload button onclick when reindexing custom feed rows

Fix removeCustomFeedRow to update button onclick handlers that reference
file input IDs with _logo_<index> when rows are reindexed after deletion.

Previously, after deleting a row, the upload button's onclick still referenced
the old file input ID, causing the upload functionality to fail.

Now properly updates:
- getElementById('..._logo_<num>') patterns in onclick handlers
- Other _logo_<num> patterns in button onclick strings
- Function parameter indices in onclick handlers

This ensures upload buttons continue to work correctly after row deletion.

* fix: Make custom feeds table widget-specific instead of generic fallback

Replace generic array-of-objects check with widget-specific check for
'custom-feeds' widget to prevent hardcoded schema from breaking other
plugins with different array-of-objects structures.

Changes:
- Check for x-widget == 'custom-feeds' before rendering custom feeds table
- Add schema validation to ensure required fields (name, url) exist
- Show warning message if schema doesn't match expected structure
- Fall back to generic array input for other array-of-objects schemas
- Add comments for future generic array-of-objects support

This ensures the hardcoded custom feeds table (name, url, logo, enabled)
only renders when explicitly requested via widget type, preventing
breakage for other plugins with different array-of-objects schemas.

* fix: Add image/gif to custom feed logo upload accept attribute

Update file input accept attributes for custom feed logo uploads to include
image/gif, making it consistent with the file-upload widget which also
allows GIF images.

Updated in three places:
- Template file input (plugin_config.html)
- JavaScript addCustomFeedRow function (base.html)
- Dynamic file input creation in handleCustomFeedLogoUpload (base.html)

All custom feed logo upload inputs now accept: image/png, image/jpeg,
image/bmp, image/gif

* fix: Add hidden input for enabled checkbox to ensure false is submitted

Add hidden input with value='false' before enabled checkbox in custom feeds
table to ensure an explicit false value is sent when checkbox is unchecked.

Pattern implemented:
- Hidden input: name='enabled', value='false' (always submitted)
- Checkbox: name='enabled', value='true' (only submitted when checked)
- When unchecked: only hidden input submits (false)
- When checked: both submit, checkbox value (true) overwrites hidden

Updated in two places:
- Template checkbox in plugin_config.html (existing rows)
- JavaScript addCustomFeedRow function in base.html (new rows)

Backend verification:
- Backend (api_v3.py) handles string boolean values and converts properly
- JavaScript form processing explicitly checks element.checked, independent of this pattern
- Standard form submission uses last value when multiple values share same name

* fix: Expose renderArrayObjectItem to window for addArrayObjectItem

Fix scope issue where renderArrayObjectItem is defined inside IIFE but
window.addArrayObjectItem is defined outside, causing the function check
to always fail and fallback to degraded HTML rendering.

Problem:
- renderArrayObjectItem (line 2469) is inside IIFE (lines 796-6417)
- window.addArrayObjectItem (line 6422) is outside IIFE
- Check 'typeof renderArrayObjectItem === function' at line 6454 always fails
- Fallback code lacks file upload widgets, URL input types, descriptions, styling

Solution:
- Expose renderArrayObjectItem to window object before IIFE closes
- Function maintains closure access to escapeHtml and other IIFE-scoped functions
- Newly added items now have full functionality matching initially rendered items

* fix: Reorder array type checks to match template order

Fix inconsistent rendering where JavaScript and Jinja template had opposite
ordering for array type checks, causing schemas with both x-widget: file-upload
AND items.type: object (like static-image) to render differently.

Problem:
- Template checks file-upload FIRST (to avoid breaking static-image plugin)
- JavaScript checked array-of-objects FIRST
- Server-rendered forms showed file-upload widget correctly
- JS-rendered forms incorrectly displayed array-of-objects table widget

Solution:
- Reorder JavaScript checks to match template order:
  1. Check file-upload widget FIRST
  2. Check checkbox-group widget
  3. Check custom-feeds widget
  4. Check array-of-objects as fallback
  5. Regular array input (comma-separated)

This ensures consistent rendering between server-rendered and JS-rendered forms
for schemas that have both x-widget: file-upload AND items.type: object.

* fix: Handle None value for feeds config to prevent TypeError

Fix crash when plugin_config['feeds'] exists but is None, causing
TypeError when checking 'custom_feeds' in feeds_config.

Problem:
- When plugin_config['feeds'] exists but is None, dict.get('feeds', {})
  returns None (not the default {}) because dict.get() only uses default
  when key doesn't exist, not when value is None
- Line 3642's 'custom_feeds' in feeds_config raises TypeError because
  None is not iterable
- This can crash the API endpoint if a plugin config has feeds: null

Solution:
- Change plugin_config.get('feeds', {}) to plugin_config.get('feeds') or {}
  to ensure feeds_config is always a dict (never None)
- Add feeds_config check before 'in' operator for extra safety

This ensures the code gracefully handles feeds: null in plugin configuration.

* fix: Add default value for AVAILABLE_SPACE to prevent TypeError

Fix crash when df produces unexpected output that results in empty
AVAILABLE_SPACE variable, causing 'integer expression expected' error.

Problem:
- df may produce unexpected output format (different locale, unusual
  filesystem name spanning lines, or non-standard df implementation)
- While '|| echo "0"' handles pipeline failures, it doesn't trigger if
  awk succeeds but produces no output (empty string)
- When AVAILABLE_SPACE is empty, comparison [ "$AVAILABLE_SPACE" -lt 500 ]
  fails with 'integer expression expected' error
- With set -e, this causes script to exit unexpectedly

Solution:
- Add AVAILABLE_SPACE=${AVAILABLE_SPACE:-0} before comparison to ensure
  variable always has a numeric value (defaults to 0 if empty)
- This gracefully handles edge cases where df/awk produces unexpected output

* fix: Wrap debug console.log in debug flag check

Fix unconditional debug logging that outputs internal implementation
details to browser console for all users.

Problem:
- console.log('[ARRAY-OBJECTS] Functions defined on window:', ...)
  executes unconditionally when page loads
- Outputs debug information about function availability to all users
- Appears to be development/debugging code inadvertently included
- Noisy console output in production

Solution:
- Wrap console.log statement in _PLUGIN_DEBUG_EARLY check to only
  output when pluginDebug localStorage flag is enabled
- Matches pattern used elsewhere in the file for debug logging
- Debug info now only visible when explicitly enabled via
  localStorage.setItem('pluginDebug', 'true')

* fix: Expose getSchemaProperty, disable upload widget, handle bracket notation arrays

Multiple fixes for array-of-objects and form processing:

1. Expose getSchemaProperty to window (plugins_manager.js):
   - getSchemaProperty was defined inside IIFE but needed by global functions
   - Added window.getSchemaProperty = getSchemaProperty before IIFE closes
   - Updated window.addArrayObjectItem to use window.getSchemaProperty
   - Fixes ReferenceError when dynamically adding array items

2. Disable upload widget for custom feeds (plugin_config.html):
   - File input and Upload button were still active but should be disabled
   - Removed onchange/onclick handlers, added disabled and aria-disabled
   - Added visible disabled styling and tooltip
   - Existing logos continue to display but uploads are prevented
   - Matches PR objectives to disable upload until fully implemented

3. Handle bracket notation array fields (api_v3.py):
   - checkbox-group uses name="field_name[]" which sends multiple values
   - request.form.to_dict() collapses duplicate keys (only keeps last value)
   - Added handling to detect fields ending with "[]" before to_dict()
   - Use request.form.getlist() to get all values, combine as comma-separated
   - Processed before existing array index field handling
   - Fixes checkbox-group losing all but last selected value

* fix: Remove duplicate submit handler to prevent double POSTs

Remove document-level submit listener that conflicts with handlePluginConfigSubmit,
causing duplicate form submissions with divergent payloads.

Problem:
- handlePluginConfigSubmit correctly parses JSON from _data fields and maps to
  flatConfig[baseKey] for patternProperties and array-of-objects
- Document-level listener (line 5368) builds its own config without understanding
  _data convention and posts independently via savePluginConfiguration
- Every submit now sends two POSTs with divergent payloads:
  - First POST: Correct structure with parsed _data fields
  - Second POST: Incorrect structure with raw _data fields, missing structure
- Arrays-of-objects and patternProperties saved incorrectly in second request

Solution:
- Remove document-level submit listener for #plugin-config-form
- Rely solely on handlePluginConfigSubmit which is already attached to the form
- handlePluginConfigSubmit properly handles all form-to-config conversion including:
  - _data field parsing (JSON from hidden fields)
  - Type-aware conversion using schema
  - Dot notation to nested object conversion
  - PatternProperties and array-of-objects support

Note: savePluginConfiguration function remains for use by JSON editor saves

* fix: Use indexed names for checkbox-group to work with existing parser

Change checkbox-group widget to use indexed field names instead of bracket
notation, so the existing indexed field parser correctly handles multiple
selected values.

Problem:
- checkbox-group uses name="{{ full_key }}[]" which requires bracket
  notation handling in backend
- While bracket notation handler exists, using indexed names is more robust
  and leverages existing well-tested indexed field parser
- Indexed field parser already handles fields like "field_name.0",
  "field_name.1" correctly

Solution:
- Template: Change name="{{ full_key }}[]" to name="{{ full_key }}.{{
  loop.index0 }}"
- JavaScript: Update checkbox-group rendering to use name="."
- Backend indexed field parser (lines 3364-3388) already handles this pattern:
  - Detects fields ending with numeric indices (e.g., ".0", ".1")
  - Groups them by base_path and sorts by index
  - Combines into array correctly

This ensures checkbox-group values are properly preserved when multiple
options are selected, working with the existing schema-based parsing system.

* fix: Set values from item data in fallback array-of-objects rendering

Fix fallback code path for rendering array-of-objects items to properly
set input values from existing item data, matching behavior of proper
renderArrayObjectItem function.

Problem:
- Fallback code at lines 3078-3091 and 6471-6486 creates input elements
  without setting values from existing item data
- Text inputs have no value attribute set
- Checkboxes have no checked attribute computed from item properties
- Users would see empty form fields instead of existing configuration data
- Proper renderArrayObjectItem function correctly sets values (line 2556)

Solution:
- Extract propValue from item data: item[propKey] with schema default fallback
- For text inputs: Set value attribute with HTML-escaped propValue
- For checkboxes: Set checked attribute based on propValue truthiness
- Add inline HTML escaping for XSS prevention (since fallback code may
  run outside IIFE scope where escapeHtml function may not be available)

This ensures fallback rendering displays existing data correctly when
window.renderArrayObjectItem is not available.

* fix: Remove extra closing brace breaking if/else chain

Remove stray closing brace at line 3127 that was breaking the if/else chain
before the 'else if (prop.enum)' branch, causing 'Unexpected token else'
syntax error.

Problem:
- Extra '}' at line 3127 closed the prop.type === 'array' block prematurely
- This broke the if/else chain, causing syntax error when parser reached
  'else if (prop.enum)' at line 3128
- Structure was: } else if (array) { ... } } } else if (enum) - extra brace

Solution:
- Removed the extra closing brace at line 3127
- Structure now correctly: } else if (array) { ... } } else if (enum)
- Verified with Node.js syntax checker - no errors

* fix: Remove local logger assignments to prevent UnboundLocalError

Remove all local logger assignments inside save_plugin_config function that
were shadowing the module-level logger, causing UnboundLocalError when nested
helpers like normalize_config_values() or debug checks reference logger before
those assignments run.

Problem:
- Module-level logger exists at line 13: logger = logging.getLogger(__name__)
- Multiple local assignments inside save_plugin_config (lines 3361, 3401, 3421,
  3540, 3660, 3977, 4093, 4118) make logger a local variable for entire function
- Python treats logger as local for entire function scope when any assignment
  exists, causing UnboundLocalError if logger is used before assignments
- Nested helpers like normalize_config_values() or debug checks that reference
  logger before local assignments would fail

Solution:
- Removed all local logger = logging.getLogger(__name__) assignments in
  save_plugin_config function
- Use module-level logger directly throughout the function
- Removed redundant import logging statements that were only used for logger
- This ensures logger is always available and references the module-level logger

All logger references now use the module-level logger without shadowing.

* fix: Fix checkbox-group serialization and array-of-objects key leakage

Multiple fixes for array-of-objects and checkbox-group widgets:

1. Fix checkbox-group serialization (JS and template):
   - Changed from indexed names (categories.0, categories.1) to _data pattern
   - Added updateCheckboxGroupData() function to sync selected values
   - Hidden input stores JSON array of selected enum values
   - Checkboxes use data-checkbox-group and data-option-value attributes
   - Fixes issue where config.categories became {0: true, 1: true} instead of ['nfl', 'nba']
   - Now correctly serializes to array using existing _data handling logic

2. Prevent array-of-objects per-item key leakage:
   - Added skip pattern in handlePluginConfigSubmit for _item_<n>_ names
   - Removed name attributes from per-item inputs in renderArrayObjectItem
   - Per-item inputs now rely solely on hidden _data field
   - Prevents feeds_item_0_name from leaking into flatConfig

3. Add type coercion to updateArrayObjectData:
   - Consults itemsSchema.properties[propKey].type for coercion
   - Handles integer and number types correctly
   - Preserves string values as-is
   - Ensures numeric fields in array items are stored as numbers

4. Ensure currentPluginConfig is always available:
   - Updated addArrayObjectItem to check window.currentPluginConfig first
   - Added error logging if schema not available
   - Prevents ReferenceError when global helpers need schema

This ensures checkbox-group arrays serialize correctly and array-of-objects
per-item fields don't leak extra keys into the configuration.

* fix: Make _data field matching more specific to prevent false positives

Fix overly broad condition that matched any field containing '_data',
causing false positives and inconsistent key transformation.

Problem:
- Condition 'key.endsWith('_data') || key.includes('_data')' matches any
  field containing '_data' anywhere (e.g., 'meta_data_field', 'custom_data_config')
- key.replace(/_data$/, '') only removes '_data' from end, making logic inconsistent
- Fields with '_data' in middle get matched but key isn't transformed
- If their value happens to be valid JSON, it gets incorrectly parsed

Solution:
- Remove 'key.includes('_data')' clause
- Only check 'key.endsWith('_data')' to match actual _data suffix pattern
- Ensures consistent matching: only fields ending with '_data' are treated
  as JSON data fields, and only those get the suffix removed
- Prevents false positives on fields like 'meta_data_field' that happen to
  contain '_data' in their name

* fix: Add HTML escaping to prevent XSS in fallback code and checkbox-group

Add proper HTML escaping for schema-derived values to prevent XSS vulnerabilities
in fallback rendering code and checkbox-group widget.

Problem:
- Fallback code in generateFieldHtml (line 3094) doesn't escape propLabel
  when building HTML strings, while main renderArrayObjectItem uses escapeHtml()
- Checkbox-group widget (lines 3012-3025) doesn't escape option or label values
- While risk is limited (values come from plugin schemas), malicious plugin
  schemas or untrusted schema sources could inject XSS
- Inconsistent with main renderArrayObjectItem which properly escapes

Solution:
- Added escapeHtml() calls for propLabel in fallback array-of-objects rendering
  (both locations: generateFieldHtml and addArrayObjectItem fallback)
- Added escapeHtml() calls for option values in checkbox-group widget:
  - checkboxId (contains option)
  - data-option-value attribute
  - value attribute
  - label text in span
- Ensures consistent XSS protection across all rendering paths

This prevents potential XSS if plugin schemas contain malicious HTML/script
content in enum values or property titles.

---------

Co-authored-by: Chuck <chuck@example.com>
2026-01-08 15:38:08 -05:00
Chuck
3fa032f7f6 Replace CIN.png version (#174)
Replaced Black CIN.png with White CIN.png

Signed-off-by: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
2026-01-07 21:33:22 -05:00
Chuck
5f4839b4f6 Remove detailed configuration sections from README (#172)
Removed detailed configuration sections for Calendar Display, Odds Ticker, Stocks, Football, Music Display, and Cache Information from README.

Signed-off-by: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
2026-01-05 12:54:37 -05:00
Chuck
20d58754b8 Fix/remove compatible versions requirement (#171)
* fix(plugins): Remove compatible_versions requirement from single plugin install

Remove compatible_versions from required fields in install_from_url method
to match install_plugin behavior. This allows installing plugins from URLs
without manifest version requirements, consistent with store plugin installation.

* fix(7-segment-clock): Update submodule with separator and spacing fixes

* fix(plugins): Add onchange handlers to existing custom feed inputs

- Add onchange handlers to key and value inputs for existing patternProperties fields
- Fixes bug where editing existing custom RSS feeds didn't save changes
- Ensures hidden JSON input field is updated when users edit feed entries
- Affects all plugins using patternProperties (custom_feeds, feed_logo_map, etc.)

---------

Co-authored-by: Chuck <chuck@example.com>
2026-01-04 17:04:45 -05:00