Commit Graph

1738 Commits

Author SHA1 Message Date
Chuck
36da426c29 fix(starlark): critical bug fixes and code quality improvements
Critical fixes:
- Fix stack overflow in safeLocalStorage (was recursively calling itself)
- Fix duplicate event listeners on Starlark grid (added sentinel check)
- Fix JSON validation to fail fast on malformed data instead of silently passing

Error handling improvements:
- Narrow exception catches to specific types (OSError, json.JSONDecodeError, ValueError)
- Use logger.exception() with exc_info=True for better stack traces
- Replace generic "except Exception" with specific exception types

Logging improvements:
- Add "[Starlark Pixlet]" context tags to pixlet_renderer logs
- Redact sensitive config values from debug logs (API keys, etc.)
- Add file_path context to schema parsing warnings

Documentation:
- Fix markdown lint issues (add language tags to code blocks)
- Fix time unit spacing: "(5min)" -> "(5 min)"

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-19 21:32:45 -05:00
Chuck
6a60a57421 fix(starlark): security and race condition improvements
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>
2026-02-19 19:37:09 -05:00
Chuck
aafb238ac9 fix(starlark): restore Starlark JS functionality lost in merge
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>
2026-02-19 17:12:31 -05:00
Chuck
b8564c952c fix(starlark): restore Starlark Apps section in plugins.html
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>
2026-02-19 17:09:50 -05:00
Chuck
e64caccae6 fix(starlark): convert Path to str in spec_from_file_location calls
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>
2026-02-19 17:02:57 -05:00
Chuck
441b3c56e9 fix(starlark): code review fixes - security, robustness, and schema parsing
## Security Fixes
- manager.py: Check _update_manifest_safe return values to prevent silent failures
- manager.py: Improve temp file cleanup in _save_manifest to prevent leaks
- manager.py: Fix uninstall order (manifest → memory → disk) for consistency
- api_v3.py: Add path traversal validation in uninstall endpoint
- api_v3.py: Implement atomic writes for manifest files with temp + rename
- pixlet_renderer.py: Relax config validation to only block dangerous shell metacharacters

## Frontend Robustness
- plugins_manager.js: Add safeLocalStorage wrapper for restricted contexts (private browsing)
- starlark_config.html: Scope querySelector to container to prevent modal conflicts

## Schema Parsing Improvements
- pixlet_renderer.py: Indentation-aware get_schema() extraction (handles nested functions)
- pixlet_renderer.py: Handle quoted defaults with commas (e.g., "New York, NY")
- tronbyte_repository.py: Validate file_name is string before path traversal checks

## Dependencies
- requirements.txt: Update Pillow (10.4.0), PyYAML (6.0.2), requests (2.32.0)

## Documentation
- docs/STARLARK_APPS_GUIDE.md: Comprehensive guide explaining:
  - How Starlark apps work
  - That apps come from Tronbyte (not LEDMatrix)
  - Installation, configuration, troubleshooting
  - Links to upstream projects

All changes improve security, reliability, and user experience.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-19 16:58:22 -05:00
Chuck
5d213b5747 Merge branch 'main' into feat/starlark-apps
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>
2026-02-19 16:44:24 -05:00
Chuck
8d1579a51b feat(starlark): implement schema extraction, asset download, and config persistence
## Schema Extraction
- Replace broken `pixlet serve --print-schema` with regex-based source parser
- Extract schema by parsing `get_schema()` function from .star files
- Support all field types: Location, Text, Toggle, Dropdown, Color, DateTime
- Handle variable-referenced dropdown options (e.g., `options = teamOptions`)
- Gracefully handle complex/unsupported field types (OAuth2, PhotoSelect, etc.)
- Extract schema for 90%+ of Tronbyte apps

## Asset Download
- Add `download_app_assets()` to fetch images/, sources/, fonts/ directories
- Download assets in binary mode for proper image/font handling
- Validate all paths to prevent directory traversal attacks
- Copy asset directories during app installation
- Enable apps like AnalogClock that require image assets

## Config Persistence
- Create config.json file during installation with schema defaults
- Update both config.json and manifest when saving configuration
- Load config from config.json (not manifest) for consistency with plugin
- Separate timing keys (render_interval, display_duration) from app config
- Fix standalone web service mode to read/write config.json

## Pixlet Command Fix
- Fix Pixlet CLI invocation: config params are positional, not flags
- Change from `pixlet render file.star -c key=value` to `pixlet render file.star key=value -o output`
- Properly handle JSON config values (e.g., location objects)
- Enable config to be applied during rendering

## Security & Reliability
- Add threading.Lock for cache operations to prevent race conditions
- Reduce ThreadPoolExecutor workers from 20 to 5 for Raspberry Pi
- Add path traversal validation in download_star_file()
- Add YAML error logging in manifest fetching
- Add file size validation (5MB limit) for .star uploads
- Use sanitized app_id consistently in install endpoints
- Use atomic manifest updates to prevent race conditions
- Add missing Optional import for type hints

## Web UI
- Fix standalone mode schema loading in config partial
- Schema-driven config forms now render correctly for all apps
- Location fields show lat/lng/timezone inputs
- Dropdown, toggle, text, color, and datetime fields all supported

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-19 16:42:45 -05:00
Chuck
a8609aea18 fix(starlark): load schema from schema.json in standalone mode
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>
2026-02-19 11:35:16 -05:00
Chuck
0dc1a8f6f4 fix(starlark): add List to typing imports for schema parser
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-19 11:33:49 -05:00
Chuck
d876679b9f feat(starlark): implement source code parser for schema extraction
Pixlet CLI doesn't support schema extraction (--print-schema flag doesn't exist),
so apps were being installed without schemas even when they have them.

Implemented regex-based .star file parser that:
- Extracts get_schema() function from source code
- Parses schema.Schema(version, fields) structure
- Handles variable-referenced dropdown options (e.g., options = dialectOptions)
- Supports Location, Text, Toggle, Dropdown, Color, DateTime fields
- Gracefully handles unsupported fields (OAuth2, LocationBased, etc.)
- Returns formatted JSON matching web UI template expectations

Coverage: 90%+ of Tronbyte apps (static schemas + variable references)

Changes:
- Replace extract_schema() to parse .star files directly instead of using Pixlet CLI
- Add 6 helper methods for parsing schema structure
- Handle nested parentheses and brackets properly
- Resolve variable references for dropdown options

Tested with:
- analog_clock.star (Location field) ✓
- Multi-field test (Text + Dropdown + Toggle) ✓
- Variable-referenced options ✓

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-19 11:32:37 -05:00
Chuck
6a04e882c1 feat(starlark): extract schema during standalone install
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>
2026-02-19 09:47:08 -05:00
Chuck
45f6e7c20e fix(starlark): use correct 'fileName' field from manifest (camelCase)
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>
2026-02-19 09:30:25 -05:00
Chuck
a821060084 fix(starlark): reload tronbyte_repository module to pick up code changes
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>
2026-02-19 09:29:19 -05:00
Chuck
c584f227c1 fix(starlark): use manifest filename field for .star downloads
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>
2026-02-18 21:41:50 -05:00
Chuck
885fdeed62 feat(starlark): schema-driven config forms + critical security fixes
## Schema-Driven Config UI
- Render type-appropriate form inputs from schema.json (text, dropdown, toggle, color, datetime, location)
- Pre-populate config.json with schema defaults on install
- Auto-merge schema defaults when loading existing apps (handles schema updates)
- Location fields: 3-part mini-form (lat/lng/timezone) assembles into JSON
- Toggle fields: support both boolean and string "true"/"false" values
- Unsupported field types (oauth2, photo_select) show warning banners
- Fallback to raw key/value inputs for apps without schema

## Critical Security Fixes (P0)
- **Path Traversal**: Verify path safety BEFORE mkdir to prevent TOCTOU
- **Race Conditions**: Add file locking (fcntl) + atomic writes to manifest operations
- **Command Injection**: Validate config keys/values with regex before passing to Pixlet subprocess

## Major Logic Fixes (P1)
- **Config/Manifest Separation**: Store timing keys (render_interval, display_duration) ONLY in manifest
- **Location Validation**: Validate lat [-90,90] and lng [-180,180] ranges, reject malformed JSON
- **Schema Defaults Merge**: Auto-apply new schema defaults to existing app configs on load
- **Config Key Validation**: Enforce alphanumeric+underscore format, prevent prototype pollution

## Files Changed
- web_interface/templates/v3/partials/starlark_config.html — schema-driven form rendering
- plugin-repos/starlark-apps/manager.py — file locking, path safety, config validation, schema merge
- plugin-repos/starlark-apps/pixlet_renderer.py — config value sanitization
- web_interface/blueprints/api_v3.py — timing key separation, safe manifest updates

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-18 21:38:57 -05:00
Chuck
679d9cc2fe fix(store): move storeFilterState to global scope to fix scoping bug
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>
2026-02-18 16:14:41 -05:00
Chuck
942663abfd feat(store): add sort, filter, search, and pagination to Plugin Store and Starlark Apps
Plugin Store:
- Live search with 300ms debounce (replaces Search button)
- Sort dropdown: A→Z, Z→A, Category, Author, Newest
- Installed toggle filter (All / Installed / Not Installed)
- Per-page selector (12/24/48) with pagination controls
- "Installed" badge and "Reinstall" button on already-installed plugins
- Active filter count badge + clear filters button

Starlark Apps:
- Parallel bulk manifest fetching via ThreadPoolExecutor (20 workers)
- Server-side 2-hour cache for all 500+ Tronbyte app manifests
- Auto-loads all apps when section expands (no Browse button)
- Live search, sort (A→Z, Z→A, Category, Author), author dropdown
- Installed toggle filter, per-page selector (24/48/96), pagination
- "Installed" badge on cards, "Reinstall" button variant

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 14:54:29 -05:00
Chuck
5f2daa52b0 fix(starlark): always show editable timing settings in config panel
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>
2026-02-18 13:53:45 -05:00
Chuck
13ab4f7eee fix(starlark): make config partial work standalone in web service
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>
2026-02-18 13:39:22 -05:00
Chuck
4f438fc76a fix(starlark): make API endpoints work standalone in web service
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>
2026-02-18 13:37:35 -05:00
Chuck
f279e9eea5 fix(starlark): use bare imports instead of relative imports
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>
2026-02-18 13:28:48 -05:00
Chuck
3ec1e987a4 feat: integrate Starlark/Tronbyte app support into plugin system
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>
2026-02-18 13:27:22 -05:00
Chuck
636d0e181c feat(plugins): add sorting, filtering, and fix Update All button (#252)
* 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>
2026-02-17 07:38:16 -05:00
Chuck
963c4d3b91 fix(web): use window.installedPlugins for bulk update button (#250)
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>
2026-02-15 15:28:51 -05:00
Chuck
22c495ea7c perf(store): cache GitHub API calls and eliminate redundant requests (#251)
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>
2026-02-15 14:46:31 -05:00
Chuck
5b0ad5ab71 fix(web): wire up "Check & Update All" plugins button (#249)
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>
2026-02-15 13:06:18 -05:00
Chuck
bc8568604a feat(web): add LED RGB sequence, multiplexing, and panel type settings (#248)
* 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>
2026-02-13 18:16:21 -05:00
Chuck
878f339fb3 fix(logos): support logo downloads for custom soccer leagues (#247)
* 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>
2026-02-13 17:43:05 -05:00
Chuck
51616f1bc4 fix(web): dark mode for collapsible config section headers (#246)
* 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>
2026-02-13 15:50:34 -05:00
Chuck
82370a0253 Fix log viewer readability — add missing CSS utility classes (#244)
* 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>
2026-02-12 22:14:20 -05:00
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