Security fixes:
- Add path traversal validation for output_path in download_star_file
- Remove XSS-vulnerable inline onclick handlers, use delegated events
- Add type hints to helper functions for better type safety
Race condition fixes:
- Lock manifest file BEFORE creating temp file in _save_manifest
- Hold exclusive lock for entire read-modify-write cycle in _update_manifest_safe
- Prevent concurrent writers from racing on manifest updates
Other improvements:
- Fix pages_v3.py standalone mode to load config.json from disk
- Improve error handling with proper logging in cleanup blocks
- Add explicit type annotations to Starlark helper functions
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
During the merge with main, all Starlark-specific JavaScript (104 lines)
was removed from plugins_manager.js, including:
- starlarkFilterState and filtering logic
- loadStarlarkApps() function
- Starlark app install/uninstall handlers
- Starlark section collapse/expand logic
- Pagination and sorting for Starlark apps
Restored from commit 942663ab and re-applied safeLocalStorage wrapper
from our code review fixes.
Fixes: Starlark Apps section non-functional in web UI
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The Starlark Apps UI section was lost during merge conflict resolution
with main branch. Restored from commit 942663ab which had the complete
implementation with filtering, sorting, and pagination.
Fixes: Starlark section not visible on plugin manager page
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The module import helpers were passing Path objects directly to
spec_from_file_location(), which caused spec to be None. This broke
the Starlark app store browser.
- Convert module_path to string in both _get_tronbyte_repository_class
and _get_pixlet_renderer_class
- Add None checks with clear error messages for debugging
Fixes: spec not found for the module 'tronbyte_repository'
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Resolved conflicts by accepting upstream plugin store filtering changes.
The plugin store filtering/sorting feature from main is compatible with
the starlark apps functionality in this branch.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The standalone API endpoint was returning schema: null because it didn't
load the schema.json file. Now reads schema from disk when returning
app details via web service.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The standalone install function (_install_star_file) wasn't extracting
schema from .star files, so apps installed via the web service had no
schema.json and the config panel couldn't render schema-driven forms.
Now uses PixletRenderer to extract schema during standalone install,
same as the plugin does.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The Tronbyte manifest uses 'fileName' (camelCase), not 'filename' (lowercase).
This caused the download to fall back to {app_id}.star which doesn't exist
for apps like analogclock (which has analog_clock.star).
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The web service caches imported modules in sys.modules. When deploying
code updates, the old cached version was still being used.
Now uses importlib.reload() when module is already loaded.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Tronbyte apps don't always name their .star file to match the directory.
For example, the "analogclock" app has "analog_clock.star" (with underscore).
The manifest.yaml contains a "filename" field with the correct name.
Changes:
- download_star_file() now accepts optional filename parameter
- Install endpoint passes metadata['filename'] to download_star_file()
- Falls back to {app_id}.star if filename not in manifest
Fixes: "Failed to download .star file for analogclock" error
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
storeFilterState, pluginStoreCache, and related variables were declared
inside an IIFE but referenced by top-level functions, causing
ReferenceError that broke all plugin loading.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Render interval and display duration are now always editable in the
starlark app config panel, not just shown as read-only status text.
App-specific settings from schema still appear below when present.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Read starlark app data from manifest file directly when the plugin
isn't loaded, matching the api_v3.py standalone pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The web service runs as a separate process with display_manager=None,
so plugins aren't instantiated. Refactor starlark API endpoints to
read/write the manifest file directly when the plugin isn't loaded,
enabling full CRUD operations from the web UI.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Plugin loader uses spec_from_file_location without package context,
so relative imports (.pixlet_renderer) fail. Use bare imports like
all other plugins do.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add starlark-apps plugin that renders Tidbyt/Tronbyte .star apps via
Pixlet binary and integrates them into the existing Plugin Manager UI
as virtual plugins. Includes vegas scroll support, Tronbyte repository
browsing, and per-app configuration.
- Extract working starlark plugin code from starlark branch onto fresh main
- Fix plugin conventions (get_logger, VegasDisplayMode, BasePlugin)
- Add 13 starlark API endpoints to api_v3.py (CRUD, browse, install, render)
- Virtual plugin entries (starlark:<app_id>) in installed plugins list
- Starlark-aware toggle and config routing in pages_v3.py
- Tronbyte repository browser section in Plugin Store UI
- Pixlet binary download script (scripts/download_pixlet.sh)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(store): add sorting, filtering, and fix Update All button
Add client-side sorting and filtering to the Plugin Store:
- Sort by A-Z, Z-A, Verified First, Recently Updated, Category
- Filter by verified, new, installed status, author, and tags
- Installed/Update Available badges on store cards
- Active filter count badge with clear-all button
- Sort preference persisted to localStorage
Fix three bugs causing button unresponsiveness:
- pluginsInitialized never reset on HTMX tab navigation (root cause
of Update All silently doing nothing on second visit)
- htmx:afterSwap condition too broad (fired on unrelated swaps)
- data-running guard tied to DOM element replaced by cloneNode
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(store): replace tag pills with category pills, fix sort dates
- Replace tag filter pills with category filter pills (less duplication)
- Prefer per-plugin last_updated over repo-wide pushed_at for sort
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* debug: add console logging to filter/sort handlers
* fix: bump cache-buster versions for JS and CSS
* feat(plugins): add sorting to installed plugins section
Add A-Z, Z-A, and Enabled First sort options for installed plugins
with localStorage persistence. Both installed and store sections
now default to A-Z sorting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(store): consolidate CSS, fix stale cache bug, add missing utilities, fix icon
- Consolidate .filter-pill and .category-filter-pill into shared selectors
and scope transition to only changed properties
- Fix applyStoreFiltersAndSort ignoring fresh server-filtered results by
accepting optional basePlugins parameter
- Add missing .py-1.5 and .rounded-full CSS utility classes
- Replace invalid fa-sparkles with fa-star (FA 6.0.0 compatible)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(store): semver-aware update badge and add missing gap-1.5 utility
- Replace naive version !== comparison with isNewerVersion() that does
semver greater-than check, preventing false "Update" badges on
same-version or downgrade scenarios
- Add missing .gap-1.5 CSS utility used by category pills and tag lists
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>
The previous fix (#249) wired window.updateAllPlugins to
PluginInstallManager.updateAll(), but that method reads from
PluginStateManager.installedPlugins which is never populated on
page load — only after individual install/update operations.
Meanwhile, base.html already defined a working updateAllPlugins
using window.installedPlugins (reliably populated by plugins_manager.js).
The override from install_manager.js masked this working version.
Fix: revert install_manager.js changes and rewrite runUpdateAllPlugins
to iterate window.installedPlugins directly, calling the API endpoint
without any middleman. Adds per-plugin progress in button text and
a summary notification on completion.
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The plugin store was making excessive GitHub API calls causing slow
page loads (10-30s):
- Installed plugins endpoint called get_plugin_info() per plugin (3
GitHub API calls each) just to read the `verified` field from the
registry. Use new get_registry_info() instead (zero API calls).
- _get_latest_commit_info() had no cache — all 31 monorepo plugins
share the same repo URL, causing 31 identical API calls. Add 5-min
cache keyed by repo:branch.
- _fetch_manifest_from_github() also uncached — add 5-min cache.
- load_config() called inside loop per-plugin — hoist outside loop.
- Install/update operations pass force_refresh=True to bypass caches
and always get the latest commit SHA from GitHub.
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
window.updateAllPlugins was never assigned, so the button always showed
"Bulk update handler unavailable." Wire it to PluginInstallManager.updateAll(),
add per-plugin progress feedback in the button text, show a summary
notification on completion, and skip redundant plugin list reloads.
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(web): add LED RGB sequence, multiplexing, and panel type settings
Expose three rpi-rgb-led-matrix hardware options in the Display Settings
UI so users can configure non-standard panels without editing config.json
manually. All defaults match existing behavior (RGB, Direct, Standard).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(api): validate led_rgb_sequence, multiplexing, and panel_type inputs
Reject invalid values with 400 errors before writing to config: whitelist
check for led_rgb_sequence and panel_type, range + type check for multiplexing.
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>
* fix(logos): support logo downloads for custom soccer leagues
LogoDownloader.fetch_teams_data() and fetch_single_team() only had
hardcoded API endpoints for predefined soccer leagues. Custom leagues
(e.g., por.1, mex.1) would silently fail when the ESPN game data
didn't include a direct logo URL. Now dynamically constructs the ESPN
teams API URL for any soccer_* league not in the predefined map.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(logos): address PR review — directory, bulk download, and dedup
- get_logo_directory: custom soccer leagues now resolve to shared
assets/sports/soccer_logos/ instead of creating per-league dirs
- download_all_missing_logos: use _resolve_api_url so custom soccer
leagues are no longer silently skipped
- Extract _resolve_api_url helper to deduplicate dynamic URL
construction between fetch_teams_data and fetch_single_team
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>
* fix(web): add dark mode overrides for collapsible config section headers
The collapsible section headers in plugin config schemas used bg-gray-100
and hover:bg-gray-200 which had no dark mode overrides, resulting in light
text on a light background when dark mode was active.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(web): add missing bg-gray-100 light-mode utility class
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>
* fix(web): add missing utility classes for log viewer readability
The log viewer uses text-gray-100, text-gray-200, text-gray-300,
text-red-300, text-yellow-300, bg-gray-800, bg-red-900, bg-yellow-900,
border-gray-700, and hover:bg-gray-800 — none of which were defined in
app.css. Without definitions, log text inherited the body's dark color
(#111827) which was invisible against the dark bg-gray-900 log container
in light mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(web): remove dead bg-opacity classes, use proper log level colors
The bg-opacity-10/bg-opacity-30 classes set a --bg-opacity CSS variable
that no background-color rule consumed, making them dead code. Replace
the broken two-class pattern (e.g. "bg-red-900 bg-opacity-10") with
dedicated log-level-error/warning/debug classes that use rgb() with
actual alpha values.
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>
* 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>
* 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>
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>
* 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>
* 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>
* 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>
* 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>
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>
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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
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>
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>
* 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>