mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
b374bfa8c646d76849b687ba13ef8a5ee08fba61
1761 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b374bfa8c6 |
docs: fix plugin config + store + dependency docs
PLUGIN_STORE_GUIDE.md
- 19 occurrences of port 5050 -> 5000
- All API paths missing /v3 (e.g. /api/plugins/install ->
/api/v3/plugins/install). Bulk fix.
PLUGIN_REGISTRY_SETUP_GUIDE.md
- Same port + /api/v3 fixes (3 occurrences each)
- "Go to Plugin Store tab" -> "Open the Plugin Manager tab and scroll
to the Install from GitHub section" (the real flow for registry
setup is the GitHub install section, not the Plugin Store search)
PLUGIN_CONFIG_QUICK_START.md
- Port 5001 -> 5000 (5001 is the dev_server.py default, not the web UI)
- "Plugin Store tab" install flow -> real Plugin Manager + Plugin Store
section + per-plugin tab in second nav row
- Removed reference to PLUGIN_CONFIG_TABS_SUMMARY.md (archived doc)
PLUGIN_CONFIGURATION_TABS.md
- "Plugin Management vs Configuration" section confusingly described
a "Plugins Tab" that doesn't exist as a single thing. Rewrote to
describe the real two-piece structure: Plugin Manager tab (browse,
install, toggle) vs per-plugin tabs (configure individual plugins).
PLUGIN_DEPENDENCY_GUIDE.md
- Port 5001 -> 5000
PLUGIN_DEPENDENCY_TROUBLESHOOTING.md
- Wrong port (8080) and wrong UI nav ("Plugin Store or Plugin
Management"). Fixed to the real flow.
PLUGIN_QUICK_REFERENCE.md
- "Plugin Location: ./plugins/ directory" -> default is plugin-repos/
(verified in config/config.template.json:130 and
display_controller.py:132). plugins/ is a fallback.
- File structure diagram showed plugins/ -> plugin-repos/.
- Web UI install flow: "Plugin Store tab" -> "Plugin Manager tab ->
Plugin Store section". Also fixed Configure ⚙️ button (doesn't
exist) and "Drag and drop reorder" (not implemented).
- API examples: replaced ad-hoc Python pseudocode with real curl
examples against /api/v3/plugins/* endpoints. Pointed at
REST_API_REFERENCE.md for the full list.
- "Migration Path Phase 1-5" was a roadmap written before the plugin
system shipped. The plugin system is now stable and live. Removed
the migration phases as they're history, not a roadmap.
- "Quick Migration" section called scripts/migrate_to_plugins.py
which doesn't exist anywhere in the repo. Removed.
- "Plugin Registry Structure" referenced
ChuckBuilds/ledmatrix-plugin-registry which doesn't exist. The
real registry is ChuckBuilds/ledmatrix-plugins. Fixed.
- "Next Steps" / "Questions to Resolve" sections were
pre-implementation planning notes. Replaced with a "Known
Limitations" section that documents the actually-real gaps
(sandboxing, resource limits, ratings, auto-updates).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
49287bdd1a |
docs: fix ADVANCED_FEATURES and REST_API_REFERENCE
REST_API_REFERENCE.md
- Wrong path: /fonts/delete/<font_family> -> /fonts/<font_family>
(verified the real DELETE route in
web_interface/blueprints/api_v3.py).
- Diffed the documented routes against the real api_v3 blueprint
(92 routes vs the 71 documented). Added missing sections:
- Error tracking (/errors/summary, /errors/plugin/<id>, /errors/clear)
- Health (/health)
- Schedule dim/power (/config/dim-schedule GET/POST)
- Plugin-specific endpoints (calendar/list-calendars,
of-the-day/json/upload+delete, plugins/<id>/static/<path>)
- Starlark Apps (12 endpoints: status, install-pixlet, apps CRUD,
repository browse/install, upload)
- Font preview (/fonts/preview)
- Updated table of contents with the new sections.
- Added a footer note that the API blueprint mounts at /api/v3
(app.py:144) and that SSE stream endpoints are defined directly on
the Flask app at app.py:607-615.
ADVANCED_FEATURES.md
- Vegas Scroll Mode section was actually accurate (verified all
config keys match src/vegas_mode/config.py:15-30).
- On-Demand Display section had multiple bugs:
- 5 occurrences of port 5050 -> 5000
- All API paths missing /v3 (e.g. /api/display/on-demand/start
should be /api/v3/display/on-demand/start)
- "Settings -> Plugin Management -> Show Now Button" UI flow doesn't
exist. Real flow: open the plugin's tab in the second nav row,
click Run On-Demand / Stop On-Demand.
- "Python API Methods" section showed
controller.show_on_demand() / clear_on_demand() /
is_on_demand_active() / get_on_demand_info() — none of these
methods exist on DisplayController. The on-demand machinery is
all internal (_set_on_demand_*, _activate_on_demand, etc) and
is driven through the cache_manager. Replaced the section with
a note pointing to the REST API.
- All Use Case Examples used the same fictional Python calls.
Replaced with curl examples against the real API.
- Cache Management section claimed "On-demand display uses Redis cache
keys". LEDMatrix doesn't use Redis — verified with grep that
src/cache_manager.py has no redis import. The cache is file-based,
managed by CacheManager (file at /var/cache/ledmatrix/ or fallback
paths). Rewrote the manual recovery section:
- Removed redis-cli commands
- Replaced cache.delete() Python calls with cache.clear_cache()
(the real public method per the same bug already flagged in
PLUGIN_API_REFERENCE.md)
- Replaced "Settings -> Cache Management" with the real Cache tab
- Documented the actual cache directory candidates
- Background Data Service section:
- Used "nfl_scoreboard" as the plugin id in the example.
The real plugin is "football-scoreboard" (handles both NFL and
NCAA). Fixed.
- "Implementation Status: Phase 1 NFL only / Phase 2 planned"
section was severely outdated. The background service is now
used by all sports scoreboards (football, hockey, baseball,
basketball, soccer, lacrosse, F1, UFC), the odds ticker, and
the leaderboard plugin. Replaced with a current "Plugins using
the background service" note.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
1d31465df0 |
docs: fix WEB_INTERFACE_GUIDE and WIFI_NETWORK_SETUP
WEB_INTERFACE_GUIDE.md - Web UI port: 5050 -> 5000 (4 occurrences) - Tab list was almost entirely fictional. Documented tabs: General Settings, Display Settings, Durations, Sports Configuration, Plugin Management, Plugin Store, Font Management. None of these exist. Real tabs (verified in web_interface/templates/v3/base.html: 935-1000): Overview, General, WiFi, Schedule, Display, Config Editor, Fonts, Logs, Cache, Operation History, plus Plugin Manager and per-plugin tabs in the second nav row. Rewrote the navigation section, the General/Display/Plugin sections, and the Common Tasks walkthroughs to match. - Quick Actions list referenced "Test Display" button (doesn't exist). Replaced with the real button list verified in partials/overview.html:88-152: Start/Stop Display, Restart Display Service, Restart Web Service, Update Code, Reboot, Shutdown. - API endpoints used /api/* paths. The api_v3 blueprint mounts at /api/v3 (web_interface/app.py:144), so the real paths are /api/v3/config/main, /api/v3/system/status, etc. Fixed. - Removed bogus "Sports Configuration tab" walkthrough; sports favorites live inside each scoreboard plugin's own tab now. - Plugin directory listed as /plugins/. Real default is plugin-repos/ (verified in config/config.template.json:130 and display_controller.py:132); plugins/ is a fallback. - Removed "Swipe navigation between tabs" mobile claim (not implemented). WIFI_NETWORK_SETUP.md - 21 occurrences of port 5050 -> 5000. - All /api/wifi/* curl examples used the wrong path. The real wifi API routes are at /api/v3/wifi/* (api_v3.py:6367-6609). Fixed. - ap_password default was documented as "" (empty/open network) but config/wifi_config.json ships with "ledmatrix123". Updated the Quick Start, Configuration table, AP Mode Settings section, and Security Recommendations to match. Also clarified that setting ap_password to "" is the way to make it an open network. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
2a7a318cf7 |
docs: refresh and correct stale documentation across repo
Walked the README and docs/ tree against current code and fixed several
real bugs and many stale references. Highlights:
User-facing
- README.md: web interface install instructions referenced
install_web_service.sh at the repo root, but it actually lives at
scripts/install/install_web_service.sh.
- docs/GETTING_STARTED.md: every web UI port reference said 5050, but
the real server in web_interface/start.py:123 binds 5000. Same bug
was duplicated in docs/TROUBLESHOOTING.md (17 occurrences). Fixed
both.
- docs/GETTING_STARTED.md: rewrote tab-by-tab instructions. The doc
referenced "Plugin Store", "Plugin Management", "Sports Configuration",
"Durations", and "Font Management" tabs - none of which exist. Real
tabs (verified in web_interface/templates/v3/base.html) are: Overview,
General, WiFi, Schedule, Display, Config Editor, Fonts, Logs, Cache,
Operation History, Plugin Manager (+ per-plugin tabs).
- docs/GETTING_STARTED.md: removed references to a "Test Display"
button (doesn't exist) and "Show Now" / "Stop" plugin buttons. Real
controls are "Run On-Demand" / "Stop On-Demand" inside each plugin's
tab (partials/plugin_config.html:792).
- docs/TROUBLESHOOTING.md: removed dead reference to
troubleshoot_weather.sh (doesn't exist anywhere in the repo); weather
is now a plugin in ledmatrix-plugins.
Developer-facing
- docs/PLUGIN_API_REFERENCE.md: documented draw_image() doesn't exist
on DisplayManager. Real plugins paste onto display_manager.image
directly (verified in src/base_classes/{baseball,basketball,football,
hockey}.py). Replaced with the canonical pattern.
- docs/PLUGIN_API_REFERENCE.md: documented cache_manager.delete() doesn't
exist. Real method is clear_cache(key=None). Updated the section.
- docs/PLUGIN_API_REFERENCE.md: added 10 missing BasePlugin methods that
the doc never mentioned: dynamic-duration hooks, live-priority hooks,
and the full Vegas-mode interface.
- docs/PLUGIN_DEVELOPMENT_GUIDE.md: same draw_image fix.
- docs/DEVELOPMENT.md: corrected the "Plugin Submodules" section. Plugins
are NOT git submodules - .gitmodules only contains
rpi-rgb-led-matrix-master. Plugins are installed at runtime into the
plugins directory configured by plugin_system.plugins_directory
(default plugin-repos/). Both internal links in this doc were also
broken (missing relative path adjustment).
- docs/HOW_TO_RUN_TESTS.md: removed pytest-timeout from install line
(not in requirements.txt) and corrected the test/integration/ path
(real integration tests are at test/web_interface/integration/).
Replaced the fictional file structure diagram with the real one.
- docs/EMULATOR_SETUP_GUIDE.md: clone URL was a placeholder; default
pixel_size was documented as 16 but emulator_config.json ships with 5.
Index
- docs/README.md: rewrote. Old index claimed "16-17 files after
consolidation" but docs/ actually has 38 .md files. Four were missing
from the index entirely (CONFIG_DEBUGGING, DEV_PREVIEW,
PLUGIN_ERROR_HANDLING, STARLARK_APPS_GUIDE). Trimmed the navel-gazing
consolidation/statistics sections.
Out of scope but worth flagging:
- src/plugin_system/resource_monitor.py:343 and src/common/api_helper.py:287
call cache_manager.delete(key) but no such method exists on
CacheManager. Both call sites would AttributeError at runtime if hit.
Not fixed in this docs PR - either add a delete() shim or convert
callers to clear_cache().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
efe6b1fe23 |
fix: reduce CPU usage, fix Vegas refresh, throttle high-FPS ticks (#304)
* fix: reduce CPU usage, fix Vegas mid-cycle refresh, and throttle high-FPS plugin ticks Web UI Info plugin was causing 90%+ CPU on RPi4 due to frequent subprocess calls and re-rendering. Fixed by: trying socket-based IP detection first (zero subprocess overhead), caching AP mode checks with 60s TTL, reducing IP refresh from 30s to 5m, caching rendered display images, and loading fonts once at init. Vegas mode was not updating the display mid-cycle because hot_swap_content() reset the scroll position to 0 on every recomposition. Now saves and restores scroll position for mid-cycle updates. High-FPS display loop was calling _tick_plugin_updates() 125x/sec with no benefit. Added throttled wrapper that limits to 1 call/sec. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review — respect plugin update_interval, narrow exception handlers Make _tick_plugin_updates_throttled default to no-throttle (min_interval=0) so plugin-configured update_interval values are never silently capped. The high-FPS call site passes an explicit 1.0s interval. Narrow _load_font exception handler from bare Exception to FileNotFoundError | OSError so unexpected errors surface. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(vegas): scale scroll position proportionally on mid-cycle hot-swap When content width changes during a mid-cycle recomposition (e.g., a plugin gains or loses items), blindly restoring the old scroll_position and total_distance_scrolled could overshoot the new total_scroll_width and trigger immediate false completion. Scale both values proportionally to the new width and clamp scroll_position to stay in bounds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
5ea2acd897 |
fix(web): array-table Add Item button creates rows with input fields (#302) (#303)
The data-item-properties attribute on the Add Item button was serialized
inside double-quoted HTML using {{ item_properties|tojson|e }}. Jinja2's
|tojson returns Markup (marked safe), making |e a no-op — the JSON
double quotes were not escaped to ". The browser truncated the
attribute at the first " in the JSON, so addArrayTableRow() parsed an
empty object and created rows with only a trash icon.
Fix: switch to single-quote attribute delimiters (JSON only uses double
quotes internally) and filter item_properties to only the display
columns, avoiding large nested objects in the attribute value.
Closes #302
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
68a0fe1182 |
fix(web): resolve plugin settings tabs not loading (#301)
* fix(web): resolve plugin settings tabs not loading due to enhancement race Two co-occurring bugs prevented plugin setting tabs from loading: 1. Both stub-to-full app() enhancement paths (tryEnhance and requestAnimationFrame) could fire independently, with the second overwriting installedPlugins back to [] after init() already fetched them. Added a guard flag (_appEnhanced) and runtime state preservation to prevent this race. 2. Plugin config x-init only loaded content if window.htmx was available at that exact moment, with no retry or fallback. Added retry loop (up to 3s) and fetch() fallback for resilience. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(web): use runtime default tab and add Alpine.initTree to fetch fallback - Replace hard-coded 'overview' comparison with runtime defaultTab (isAPMode ? 'wifi' : 'overview') in both enhancement paths, so activeTab is preserved correctly in AP mode - Add Alpine.initTree(el) call in the plugin config fetch() fallback so Alpine directives in the injected HTML are initialized, matching the pattern used by loadOverviewDirect and loadWifiDirect Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
7afc2c0670 |
fix(web): increase chain_length max from 8 to 32 (#300)
* fix(web): increase chain_length max from 8 to 32 The web UI form input capped chain_length at 8 panels, preventing users with larger displays (e.g. 16-panel setups) from configuring their hardware through the UI. The backend API had no such limit. Changed max="8" to max="32" to support large display configurations. Added panel count example to the help text. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(web): add server-side bounds validation for display hardware fields The API endpoint at /api/v3/config/main accepted any integer value for display hardware fields (chain_length, rows, cols, brightness, etc.) without bounds checking. Only the HTML form had min/max attributes, which are trivially bypassed by direct API calls. Added _int_field_limits dict with bounds for all integer hardware fields: chain_length: 1-32, parallel: 1-4, brightness: 1-100, rows: 8-128, cols: 16-128, scan_mode: 0-1, pwm_bits: 1-11, pwm_dither_bits: 0-2, pwm_lsb_nanoseconds: 50-500, limit_refresh_rate_hz: 0-1000, gpio_slowdown: 0-5 Out-of-bounds or non-integer values now return 400 with a clear error message (e.g. "Invalid chain_length value 99. Must be between 1 and 32.") before any config is persisted. Follows the same inline validation pattern already used for led_rgb_sequence, panel_type, and multiplexing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(api): strict int validation and add max_dynamic_duration_seconds bounds Reject bool/float types in _int_field_limits validation loop to prevent silent coercion, and add max_dynamic_duration_seconds to the validation map so it gets proper bounds checking instead of a raw int() call. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
ee4149dc49 |
fix(vegas): refresh scroll buffer on live score updates (#299)
* fix(vegas): refresh scroll buffer when plugins report live data updates should_recompose() only checked for cycle completion or staging buffer content, but plugin updates go to _pending_updates — not the staging buffer. The scroll display kept showing the old pre-rendered image until the full cycle ended, even though fresh scores were already fetched and logged. Add has_pending_updates() check so hot_swap_content() triggers immediately when plugins have new data. Fixes #230 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(vegas): scope hot-swap to visible segments; use monotonic clock 1. Replace has_pending_updates() with has_pending_updates_for_visible_segments() so hot_swap_content() only fires when a pending update affects a plugin that is actually in the active scroll buffer (with images). Avoids unnecessary recomposition when non-visible plugins report updates. 2. Switch all display-loop timing (start_time, elapsed, _next_live_priority_check) from time.time() to time.monotonic() to prevent clock-stepping issues from NTP adjustments on Raspberry Pi. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
5ddf8b1aea |
fix: live priority now interrupts long display durations (#196) (#298)
* fix: check live priority during display loops to interrupt long durations (#196) _check_live_priority() was only called once per main loop iteration, before entering the display duration loop. With dynamic duration enabled, the loop could run for 60-120+ seconds without ever checking if a favorite team's live game started — so the display stayed on leaderboard, weather, etc. while the live game played. Now both the high-FPS and normal FPS display loops check for live priority every ~30 seconds (throttled to avoid overhead). When live content is detected, the loop breaks immediately and switches to the live game mode. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update rotation index when live priority interrupts display loop The live priority break set current_display_mode but not current_mode_index, so the post-loop rotation logic (which checks the old active_mode) would overwrite the live mode on the next advance. Now both loops also set current_mode_index to match the live mode, mirroring the existing pattern at the top of the main loop (line 1385). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use timestamp throttle for live priority and skip post-loop rotation Two issues fixed: 1. The modulo-based throttle (elapsed % 30.0 < display_interval) could miss the narrow 8ms window due to timing jitter. Replaced with an explicit timestamp check (_next_live_priority_check) that fires reliably every 30 seconds. 2. After breaking out of the display loop for live priority, the post-loop code (remaining-duration sleep and rotation advancement) would still run and overwrite the live mode. Now a continue skips directly to the next main loop iteration when current_display_mode was changed during the loop. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
35df06b8e1 |
fix: resolve font upload "baseUrl is not defined" error (#235) (#297)
The baseUrl variable was declared inside an IIFE that skips re-execution on HTMX reloads, so it became undefined when the fonts tab was reloaded. Since baseUrl was just window.location.origin prepended to absolute paths like /api/v3/fonts/upload, it was unnecessary — fetch() with a leading slash already resolves against the current origin. Remove baseUrl entirely and use relative URLs in all 7 fetch calls. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
77e9eba294 |
fix: overhaul WiFi captive portal for reliable setup (#296)
* fix: overhaul WiFi captive portal for reliable device detection and fast setup The captive portal detection endpoints were returning "success" responses that told every OS (iOS, Android, Windows, Firefox) that internet was working — so the portal popup never appeared. This fixes the core issue and improves the full setup flow: - Return portal-triggering redirects when AP mode is active; normal success responses when not (no false popups on connected devices) - Add lightweight self-contained setup page (9KB, no frameworks) for the captive portal webview instead of the full UI - Cache AP mode check with 5s TTL (single systemctl call vs full WiFiManager instantiation per request) - Stop disabling AP mode during WiFi scans (which disconnected users); serve cached/pre-scanned results instead - Pre-scan networks before enabling AP mode so captive portal has results immediately - Use dnsmasq.d drop-in config instead of overwriting /etc/dnsmasq.conf (preserves Pi-hole and other services) - Fix manual SSID input bug that incorrectly overwrote dropdown selection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address review findings for WiFi captive portal - Remove orphaned comment left over from old scan_networks() finally block - Add sudoers rules for dnsmasq drop-in copy/remove to install script - Combine cached-network message into single showMsg call (was overwriting) - Return (networks, was_cached) tuple from scan_networks() so API endpoint derives cached flag from the scan itself instead of a redundant AP check - Narrow exception catch in AP mode cache to SubprocessError/OSError and log the failure for remote debugging - Bound checkNewIP retries to 20 attempts (60s) before showing fallback Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
6eccb74415 |
fix: handle dotted schema keys in plugin settings save (#295)
* fix: handle dotted schema keys in plugin settings save (issue #254) The soccer plugin uses dotted keys like "eng.1" for league identifiers. PR #260 fixed backend helpers but the JS frontend still corrupted these keys by naively splitting on dots. This fixes both the JS and remaining Python code paths: - JS getSchemaProperty(): greedy longest-match for dotted property names - JS dotToNested(): schema-aware key grouping to preserve "eng.1" as one key - Python fix_array_structures(): remove broken prefix re-navigation in recursion - Python ensure_array_defaults(): same prefix navigation fix Closes #254 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address review findings for dotted-key handling - ensure_array_defaults: replace None nodes with {} so recursion proceeds into nested objects (was skipping when key existed as None) - dotToNested: add tail-matching that checks the full remaining dotted tail against the current schema level before greedy intermediate matching, preventing leaf dotted keys from being split - syncFormToJson: replace naive key.split('.') reconstruction with dotToNested(flatConfig, schema) and schema-aware getSchemaProperty() so the JSON tab save path produces the same correct nesting as the form submit path - Add regression tests for dotted-key array normalization and None array default replacement Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address second round of review findings - Tests: replace conditional `if response.status_code == 200` guards with unconditional `assert response.status_code == 200` so failures are not silently swallowed - dotToNested: guard finalKey write with `if (i < parts.length)` to prevent empty-string key pollution when tail-matching consumed all parts - Extract normalizeFormDataForConfig() helper from handlePluginConfigSubmit and call it from both handlePluginConfigSubmit and syncFormToJson so the JSON tab sync uses the same robust FormData processing (including _data JSON inputs, bracket-notation checkboxes, array-of-objects, file-upload widgets, checkbox DOM detection, and unchecked boolean handling via collectBooleanFields) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
2c2fca2219 |
fix(web): use HTMX for Plugin Manager tab loading (#294)
* fix: auto-repair missing plugins and graceful config fallback Plugins whose directories are missing (failed update, migration, etc.) now get automatically reinstalled from the store on startup. The config endpoint no longer returns a hard 500 when a schema is unavailable — it falls back to conservative key-name-based masking so the settings page stays functional. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: handle ledmatrix- prefix in plugin updates and reconciliation The store registry uses unprefixed IDs (e.g., 'weather') while older installs used prefixed config keys (e.g., 'ledmatrix-weather'). Both update_plugin() and auto-repair now try the unprefixed ID as a fallback when the prefixed one isn't found in the registry. Also filters system config keys (schedule, display, etc.) from reconciliation to avoid false positives. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address code review findings for plugin auto-repair - Move backup-folder filter from _get_config_state to _get_disk_state where the artifact actually lives - Run startup reconciliation in a background thread so requests aren't blocked by plugin reinstallation - Set _reconciliation_done only after success so failures allow retries - Replace print() with proper logger in reconciliation - Wrap load_schema in try/except so exceptions fall through to conservative masking instead of 500 - Handle list values in _conservative_mask_config for nested secrets - Remove duplicate import re Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add thread-safe locking to PluginManager and fix reconciliation retry PluginManager thread safety: - Add RLock protecting plugin_manifests and plugin_directories - Build scan results locally in _scan_directory_for_plugins, then update shared state under lock - Protect reads in get_plugin_info, get_all_plugin_info, get_plugin_directory, get_plugin_display_modes, find_plugin_for_mode - Protect manifest mutation in reload_plugin - Prevents races between background reconciliation thread and request handlers reading plugin state Reconciliation retry: - Clear _reconciliation_started on exception so next request retries - Check result.reconciliation_successful before marking done - Reset _reconciliation_started on non-success results to allow retry Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(web): use HTMX for Plugin Manager tab loading instead of custom fetch The Plugin Manager tab was the only tab using a custom window.loadPluginsTab() function with plain fetch() instead of HTMX. This caused a race condition where plugins_manager.js listened for htmx:afterSwap to initialize, but that event never fired for the custom fetch. Users had to navigate to a plugin config tab and back to trigger initialization. Changes: - Switch plugins tab to hx-get/hx-trigger="revealed" matching all other tabs - Remove ~560 lines of dead code (script extraction for a partial with no scripts, nested retry intervals, inline HTML card rendering fallbacks) - Add simple loadPluginsDirect() fallback for when HTMX fails to load - Remove typeof htmx guard on afterSwap listener so it registers unconditionally - Tighten afterSwap target check to avoid spurious re-init from other tab swaps Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address CodeRabbit review findings across plugin system - plugin_manager.py: clear plugin_manifests/plugin_directories before update to prevent ghost entries for uninstalled plugins persisting across scans - state_reconciliation.py: remove 'enabled' key check that skipped legacy plugin configs, default to enabled=True matching PluginManager.load_plugin - app.py: add threading.Lock around reconciliation start guard to prevent race condition spawning duplicate threads; add -> None return annotation - store_manager.py: use resolved registry ID (alt_id) instead of original plugin_id when reinstalling during monorepo migration - base.html: check Response.ok in loadPluginsDirect fallback; trigger fallback on tab click when HTMX unavailable; remove active-tab check from 5-second timeout so content preloads regardless Skipped: api_v3.py secret redaction suggestion — the caller at line 2539 already tries schema-based mask_secret_fields() before falling back to _conservative_mask_config, making the suggested change redundant. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: skip backup dirs in plugin discovery and fix HTMX event syntax - plugin_manager.py: skip directories containing '.standalone-backup-' during discovery scan, matching state_reconciliation.py behavior and preventing backup manifests from overwriting live plugin entries - base.html: fix hx-on::htmx:response-error → hx-on::response-error (the :: shorthand already adds the htmx: prefix, so the original syntax resolved to htmx:htmx:response-error making the handler dead) Skipped findings: - web-ui-info in _SYSTEM_CONFIG_KEYS: it's a real plugin with manifest.json and config entry, not a system key - store_manager config key migration: valid feature request for handling ledmatrix- prefix rename, but new functionality outside this PR scope Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(web): add fetch timeout to loadPluginsDirect fallback Add AbortController with 10s timeout so a hanging fetch doesn't leave data-loaded set and block retries. Timer is cleared in both success and error paths. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
640a4c1706 |
fix: auto-repair missing plugins on startup (#293)
* fix: auto-repair missing plugins and graceful config fallback Plugins whose directories are missing (failed update, migration, etc.) now get automatically reinstalled from the store on startup. The config endpoint no longer returns a hard 500 when a schema is unavailable — it falls back to conservative key-name-based masking so the settings page stays functional. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: handle ledmatrix- prefix in plugin updates and reconciliation The store registry uses unprefixed IDs (e.g., 'weather') while older installs used prefixed config keys (e.g., 'ledmatrix-weather'). Both update_plugin() and auto-repair now try the unprefixed ID as a fallback when the prefixed one isn't found in the registry. Also filters system config keys (schedule, display, etc.) from reconciliation to avoid false positives. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address code review findings for plugin auto-repair - Move backup-folder filter from _get_config_state to _get_disk_state where the artifact actually lives - Run startup reconciliation in a background thread so requests aren't blocked by plugin reinstallation - Set _reconciliation_done only after success so failures allow retries - Replace print() with proper logger in reconciliation - Wrap load_schema in try/except so exceptions fall through to conservative masking instead of 500 - Handle list values in _conservative_mask_config for nested secrets - Remove duplicate import re Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add thread-safe locking to PluginManager and fix reconciliation retry PluginManager thread safety: - Add RLock protecting plugin_manifests and plugin_directories - Build scan results locally in _scan_directory_for_plugins, then update shared state under lock - Protect reads in get_plugin_info, get_all_plugin_info, get_plugin_directory, get_plugin_display_modes, find_plugin_for_mode - Protect manifest mutation in reload_plugin - Prevents races between background reconciliation thread and request handlers reading plugin state Reconciliation retry: - Clear _reconciliation_started on exception so next request retries - Check result.reconciliation_successful before marking done - Reset _reconciliation_started on non-success results to allow retry Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
81a022dbe8 |
fix(web): resolve file upload config lookup for server-rendered forms (#279)
* fix(web): resolve file upload config lookup for server-rendered forms
The file upload widget's getUploadConfig() function failed to map
server-rendered field IDs (e.g., "static-image-images") back to schema
property keys ("images"), causing upload config (plugin_id, endpoint,
allowed_types) to be lost. This could prevent image uploads from
working correctly in the static-image plugin and others.
Changes:
- Add data-* attributes to the Jinja2 file-upload template so upload
config is embedded directly on the file input element
- Update getUploadConfig() in both file-upload.js and plugins_manager.js
to read config from data attributes first, falling back to schema lookup
- Remove duplicate handleFiles/handleFileDrop/handleFileSelect from
plugins_manager.js that overwrote the more robust file-upload.js versions
- Bump cache-busting version strings so browsers fetch updated JS
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(web): harden file upload functions against CodeRabbit patterns
- Add response.ok guard before response.json() in handleFiles,
deleteUploadedFile, and handleCredentialsUpload to prevent
SyntaxError on non-JSON error responses (PR #271 finding)
- Remove duplicate getUploadConfig() from plugins_manager.js;
file-upload.js now owns this function exclusively
- Replace innerHTML with textContent/DOM methods in
handleCredentialsUpload to prevent XSS (PR #271 finding)
- Fix redundant if-check in getUploadConfig data-attribute reader
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(web): address CodeRabbit findings on file upload widget
- Add data-multiple="true" discriminator on array file inputs so
handleFileDrop routes multi-file drops to handleFiles() not
handleSingleFileUpload()
- Duplicate upload config data attributes onto drop zone wrapper so
getUploadConfig() survives progress-helper DOM re-renders that
remove the file input element
- Clear file input in finally block after credentials upload to allow
re-selecting the same file on retry
- Branch deleteUploadedFile on fileType: JSON deletes remove the DOM
element directly instead of routing through updateImageList() which
renders image-specific cards (thumbnails, scheduling controls)
Addresses CodeRabbit findings on PR #279:
- Major: drag-and-drop hits single-file path for array uploaders
- Major: config lookup fails after first upload (DOM node removed)
- Minor: same-file retry silently no-ops
- Major: JSON deletes re-render list as images
Co-Authored-By: 5ymb01 <5ymb01@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(web): address CodeRabbit round-2 findings on file upload widget
- Extract getConfigSourceElement() helper so handleFileDrop,
handleSingleFileUpload, and getUploadConfig all share the same
fallback logic: file input → drop zone wrapper
- Remove pluginId gate from getUploadConfig Strategy 1 — fields with
uploadEndpoint or fileType but no pluginId now return config instead
of falling through to generic defaults
- Fix JSON delete identifier mismatch: use file.id || file.category_name
(matching the renderer at line 3202) instead of f.file_id; remove
regex sanitization on DOM id lookup (renderer doesn't sanitize)
Addresses CodeRabbit round-2 findings on PR #279:
- Major: single-file uploads bypass drop-zone config fallback
- Major: getUploadConfig gated on data-plugin-id only
- Major: JSON delete file identifier mismatch vs renderer
Co-Authored-By: 5ymb01 <5ymb01@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(web): align delete handler file identifier with renderer logic
Remove f.file_id from JSON file delete filter to match the renderer's
identifier logic (file.id || file.category_name || idx). Prevents
deleted entries from persisting in the hidden input on next save.
Co-Authored-By: 5ymb01 <noreply@github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: 5ymb01 <noreply@github.com>
|
||
|
|
48ff624a85 |
fix: catch ConfigError in display preview generator (#288)
* fix: catch ConfigError in display preview generator PR #282 narrowed bare except blocks but missed ConfigError from config_manager.load_config(), which wraps FileNotFoundError, JSONDecodeError, and OSError. Without this, a corrupt or missing config crashes the display preview SSE endpoint instead of falling back to 128x64 defaults. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(security): comprehensive error handling cleanup - Remove all traceback.format_exc() from client responses (33 remaining instances) - Sanitize str(e) from client-facing messages, replacing with generic error messages - Replace ~65 bare print() calls with structured logger.exception/error/warning/info/debug - Remove ~35 redundant inline `import traceback` and `import logging` statements - Convert logging.error/warning calls to use module-level named logger - Fix WiFi endpoints that created redundant inline logger instances - Add logger.exception() at all WebInterfaceError.from_exception() call sites - Fix from_exception() in errors.py to use safe messages instead of raw str(exception) - Apply consistent [Tag] prefixes to all logger calls for production triage Only safe, user-input-derived str(e) kept: json.JSONDecodeError handlers (400 responses). Subprocess template print(stdout) calls preserved (not error logging). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(security): correct error inference, remove debug log leak, consolidate config handlers - _infer_error_code: map Config* exceptions to CONFIG_LOAD_FAILED (ConfigError is only raised by load_config(), so CONFIG_SAVE_FAILED produced wrong safe message and wrong suggested_fixes) - Remove leftover DEBUG logs in save_main_config that dumped full request body and all HTTP headers (Authorization, Cookie, etc.) - Replace dead FileNotFoundError/JSONDecodeError/IOError handlers in get_dim_schedule_config with single ConfigError catch (load_config already wraps these into ConfigError) - Remove redundant local `from src.exceptions import ConfigError` imports now covered by top-level import - Strip str(e) from client-facing error messages in dim schedule handler Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(security): fix plugin update logging and config validation leak - update_plugin: change logger.exception to logger.error in non-except branch (logger.exception outside an except block logs useless "NoneType: None" traceback) - update_plugin: remove duplicate logger.exception call in except block (was logging the same failure twice) - save_plugin_config validation: stop logging full plugin_config dict (can contain API keys, passwords, tokens) and raw form_data values; log only keys and validation errors instead 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> |
||
|
|
31ed854d4e |
fix(config): deduplicate uniqueItems arrays before schema validation (#292)
* fix(config): deduplicate uniqueItems arrays before schema validation When saving plugin config via the web UI, the form data is merged with the existing stored config. If a user adds an item that already exists (e.g. adding stock symbol "FNMA" when it's already in the list), the merged array contains duplicates. Schemas with `uniqueItems: true` then reject the config, making it impossible to save. Add a recursive dedup pass that runs after normalization/filtering but before validation. It walks the schema tree, finds arrays with the uniqueItems constraint, and removes duplicates while preserving order. Co-Authored-By: 5ymb01 <noreply@github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: recurse into array items and add tests for uniqueItems dedup Address CodeRabbit review: _dedup_unique_arrays now also recurses into array elements whose items schema is an object, so nested uniqueItems constraints inside arrays-of-objects are enforced. Add 11 unit tests covering: - flat arrays with/without duplicates - order preservation - arrays without uniqueItems left untouched - nested objects (feeds.stock_symbols pattern) - arrays of objects with inner uniqueItems arrays - edge cases (empty array, missing keys, integers) - real-world stock-news plugin config shape Co-Authored-By: 5ymb01 <noreply@github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: extract dedup_unique_arrays to shared validators module Move _dedup_unique_arrays from an inline closure in save_plugin_config to src/web_interface/validators.dedup_unique_arrays so tests import and exercise the production code path instead of a duplicated copy. Addresses CodeRabbit review: tests now validate the real function, preventing regressions from diverging copies. Co-Authored-By: 5ymb01 <noreply@github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com> Co-authored-by: 5ymb01 <noreply@github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
442638dd2c |
fix: add reset() alias to ScrollHelper for plugin compatibility (#290)
Multiple plugins (F1, UFC) independently called scroll_helper.reset() instead of scroll_helper.reset_scroll(), causing AttributeError and preventing scroll modes from displaying. Adding reset() as an alias prevents this class of bugs going forward. Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
8391832c90 |
fix(vegas): keep plugin data and visuals fresh during Vegas scroll mode (#291)
* fix(vegas): keep plugin data and visuals fresh during Vegas scroll mode Plugins using ESPN APIs and other data sources were not updating during Vegas mode because the render loop blocked for 60-600s per iteration, starving the scheduled update tick. This adds a non-blocking background thread that runs plugin updates every ~1s during Vegas mode, bridges update notifications to the stream manager, and clears stale scroll caches so all three content paths (native, scroll_helper, fallback) reflect fresh data. - Add background update tick thread in Vegas coordinator (non-blocking) - Add _tick_plugin_updates_for_vegas() bridge in display controller - Fix fallback capture to call update() instead of only update_data() - Clear scroll_helper.cached_image on update for scroll-based plugins - Drain background thread on Vegas stop/exit to prevent races Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(vegas): address review findings in update pipeline - Extract _drive_background_updates() helper and call it from both the render loop and the static-pause wait loop so plugin data stays fresh during static pauses (was skipped by the early `continue`) - Remove synchronous plugin.update() from the fallback capture path; the background update tick already handles API refreshes so the content-fetch thread should only call lightweight update_data() - Use scroll_helper.clear_cache() instead of just clearing cached_image so cached_array, total_scroll_width and scroll_position are also reset 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> |
||
|
|
c8737d1a6c |
fix(api): use sys.executable for plugin action subprocess calls (#277)
* fix(api): use sys.executable for plugin action subprocess calls The execute_plugin_action endpoint hardcoded 'python3' when spawning plugin scripts via subprocess. This can fail if the system Python is named differently or if a virtualenv is active, since 'python3' may not point to the correct interpreter. Changes: - Replace 'python3' with sys.executable in the non-OAuth script execution branch (uses the same interpreter running the web service) - Remove redundant 'import sys' inside the oauth_flow conditional block (sys is already imported at module level; the local import shadows the top-level binding for the entire function scope, which would cause UnboundLocalError if sys were referenced in the else branch on Python 3.12+) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(api): replace all remaining hardcoded python3 with sys.executable Fix 4 additional subprocess calls that still used 'python3' instead of sys.executable: parameterized action wrapper (line 5150), stdin-param wrapper (line 5211), no-param wrapper (line 5417), and OAuth auth script (line 5524). Ensures plugin actions work in virtualenvs and non-standard Python installations. Addresses CodeRabbit findings on PR #277. Co-Authored-By: 5ymb01 <5ymb01@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
28a374485f |
fix(test): repair test infrastructure and mock fixtures (#281)
* fix(test): repair test infrastructure and mock fixtures - Add test/__init__.py for proper test collection - Fix ConfigManager instantiation to use config_path parameter - Route schedule config through config_service mock - Update mock to match get_raw_file_content endpoint change Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(test): correct get_main_config assertion per CodeRabbit review The endpoint calls load_config(), not get_raw_file_content('main'). Also set up load_config mock return value in the fixture so the test's data assertions pass correctly. Co-Authored-By: 5ymb01 <noreply@github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(test): correct plugin config test mock structure and schema returns - Plugin configs live at top-level keys, not under 'plugins' subkey - Mock schema_manager.generate_default_config to return a dict - Mock schema_manager.merge_with_defaults to merge dicts (not MagicMock) - Fixes test_get_plugin_config returning 500 due to non-serializable MagicMock Co-Authored-By: 5ymb01 <noreply@github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(test): use patch.object for config_service.get_config in schedule tests config_service.get_config is a real method, not a mock — can't set return_value on it directly. Use patch.object context manager instead. Co-Authored-By: 5ymb01 <noreply@github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: 5ymb01 <noreply@github.com> |
||
|
|
fa92bfbdd8 |
fix(store): correct plugin store API endpoint path (#278)
Co-authored-by: sarjent <sarjent@users.noreply.github.com> |
||
|
|
f3e7c639ba |
fix: narrow bare except blocks to specific exception types (#282)
Replace 6 bare `except:` blocks with targeted exception types: - logo_downloader.py: OSError for file removal, (OSError, IOError) for font loading - layout_manager.py: (ValueError, TypeError, KeyError, IndexError) for format string - app.py: (OSError, ValueError) for CPU temp, (SubprocessError, OSError) for systemctl, (KeyError, TypeError, ValueError) for config parsing Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com> Co-authored-by: 5ymb01 <noreply@github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
f718305886 |
fix(security): stop leaking Python tracebacks to HTTP clients (#283)
* fix(security): stop leaking Python tracebacks to HTTP clients
Replace 13 instances where traceback.format_exc() was sent in API
JSON responses (via `details=`, `traceback:`, or `details:` keys).
- 5 error_response(details=traceback.format_exc()) → generic message
- 6 jsonify({'traceback': traceback.format_exc()}) → removed key
- 2 jsonify({'details': error_details}) → logger.error() instead
Tracebacks in debug mode (app.py error handlers) are preserved as
they are guarded by app.debug and expected during development.
Co-Authored-By: 5ymb01 <noreply@github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(security): sanitize str(e) from client responses, add server-side logging
Address CodeRabbit review findings:
- Replace str(e) in error_response message fields with generic messages
- Replace import logging/traceback + manual format with logger.exception()
- Add logger.exception() to 6 jsonify handlers that were swallowing errors
- All exception details now logged server-side only, not sent to clients
Co-Authored-By: 5ymb01 <noreply@github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: remove duplicate traceback logging, sanitize secrets config error
Address CodeRabbit nitpicks:
- Remove manual import logging/traceback + logging.error() that duplicated
the logger.exception() call in save_raw_main_config
- Apply same fix to save_raw_secrets_config: replace str(e) in client
response with generic message, use logger.exception() for server-side
Co-Authored-By: 5ymb01 <noreply@github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com>
Co-authored-by: 5ymb01 <noreply@github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
|
||
|
|
f0dc094cd6 |
fix(security): use Path.relative_to() for path confinement (#284)
* fix(security): use Path.relative_to() for path confinement check Replace str.startswith() path check with Path.relative_to() in the plugin file viewer endpoint. startswith() can be bypassed when a directory name is a prefix of another (e.g., /plugins/foo vs /plugins/foobar). relative_to() correctly validates containment. Co-Authored-By: 5ymb01 <noreply@github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: trigger CodeRabbit review --------- Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com> Co-authored-by: 5ymb01 <noreply@github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
178dfb0c2a |
fix(perf): cache fonts in sport base classes to avoid disk I/O per frame (#285)
* fix(perf): cache fonts in sport base classes to avoid disk I/O per frame Replace 7 ImageFont.truetype() calls in display methods with cached self.fonts['detail'] lookups. The 4x6-font.ttf at size 6 is already loaded once in _load_fonts() — loading it again on every display() call causes unnecessary disk I/O on each render frame (~30-50 FPS). Files: sports.py (2), football.py (1), hockey.py (2), basketball.py (1), baseball.py (1) Co-Authored-By: 5ymb01 <noreply@github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: trigger CodeRabbit review --------- Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com> Co-authored-by: 5ymb01 <noreply@github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
76c5bf5781 |
fix(security): mask secret fields in API responses and extract helpers (#276)
* fix(security): mask secret fields in API responses and extract helpers GET /config/secrets returned raw API keys in plaintext to the browser. GET /plugins/config returned merged config including deep-merged secrets. POST /plugins/config could overwrite existing secrets with empty strings when the GET endpoint returned masked values that were sent back unchanged. Changes: - Add src/web_interface/secret_helpers.py with reusable functions: find_secret_fields, separate_secrets, mask_secret_fields, mask_all_secret_values, remove_empty_secrets - GET /config/secrets: mask all values with '••••••••' - GET /plugins/config: mask x-secret fields with '' - POST /plugins/config: filter empty-string secrets before saving - pages_v3: mask secrets before rendering plugin config templates - Remove three duplicated inline find_secret_fields/separate_secrets definitions in api_v3.py (replaced by single imported module) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(security): harden secret masking against CodeRabbit findings - Fail-closed: return 500 when schema unavailable instead of leaking secrets - Fix falsey masking: use `is not None and != ''` instead of truthiness check so values like 0 or False are still redacted - Add array-item secret support: recurse into `type: array` items schema to detect and mask secrets like accounts[].token - pages_v3: fail-closed when schema properties missing Addresses CodeRabbit findings on PR #276: - Critical: fail-closed bypass when schema_mgr/schema missing - Major: falsey values not masked (0, False leak through) - Major: pages_v3 fail-open when schema absent - Major: array-item secrets unsupported Co-Authored-By: 5ymb01 <5ymb01@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
feee1dffde |
fix(web): remove shadowed sys import in plugin action handler (#280)
* fix(web): remove shadowed sys import in plugin action handler Two `import sys` statements inside execute_plugin_action() and authenticate_spotify() shadowed the module-level import, causing "cannot access local variable 'sys'" errors when sys.executable was referenced in earlier branches of the same function. Also fixes day number validation in the of-the-day upload endpoint to accept 366 (leap year). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(api): correct validation message from 1-365 to 1-366 The JSON structure validation message still said '1-365' while the actual range check accepts 1-366 for leap years. Make all three validation messages consistent. Addresses CodeRabbit finding on PR #280. Co-Authored-By: 5ymb01 <5ymb01@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
f05c357d57 |
fix(config): use correct plugin ID key in secrets template (#275)
The secrets template used "weather" as the key, but the weather plugin's ID is "ledmatrix-weather". Since ConfigManager deep-merges secrets into the main config by key, secrets under "weather" never reached the plugin config at config["ledmatrix-weather"], making the API key invisible to the plugin. Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
fe5c1d0d5e |
feat(web): add Google Calendar picker widget for dynamic multi-calendar selection (#274)
* fix(install): add --prefer-binary to pip installs to avoid /tmp exhaustion timezonefinder (~54 MB) includes large timezone polygon data files that pip unpacks into /tmp during installation. On Raspberry Pi, the default tmpfs /tmp size (often ~half of RAM) can be too small, causing the install to fail with an out-of-space error. Adding --prefer-binary tells pip to prefer pre-built binary wheels over source distributions. Since timezonefinder and most other packages publish wheels on PyPI (and piwheels.org has ARM wheels), this avoids the large temporary /tmp extraction and speeds up installs generally. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(timezone): use America/New_York instead of EST for ESPN API date queries EST is a fixed UTC-5 offset that does not observe daylight saving time, causing the ESPN API date to be off by one hour during EDT (March–November). America/New_York correctly handles DST transitions. The ESPN scoreboard API anchors its schedule calendar to Eastern US time, so this Eastern timezone is intentionally kept for the API date — it is not user-configurable. Game time display is converted separately to the user's configured timezone. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(web): add Google Calendar picker widget for dynamic calendar selection Adds a new google-calendar-picker widget and API endpoint that lets users load their available Google Calendars by name and check the ones they want, instead of manually typing calendar IDs. - GET /api/v3/plugins/calendar/list-calendars — calls plugin.get_calendars() and returns all accessible calendars with id, summary, and primary flag - google-calendar-picker.js — new widget: "Load My Calendars" button renders a checklist; selections update a hidden comma-separated input for form submit - plugin_config.html — handles x-widget: google-calendar-picker in array branch - base.html — loads the new widget script Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(web): address PR review findings in google-calendar-picker - api_v3.py: replace broad except block with specific exception handling, log full traceback via module logger, normalize/validate get_calendars() output to stable {id,summary,primary} objects, return opaque user-friendly error message instead of leaking str(e) - google-calendar-picker.js: fix button label only updating to "Refresh Calendars" on success (restore original label on error); update summary paragraph via syncHiddenAndSummary() on every checkbox change so UI stays in sync with hidden input; pass summary element through loadCalendars and renderCheckboxes instead of re-querying DOM - plugin_config.html: bound initWidget retry loop with MAX_RETRIES=40 to prevent infinite timers; normalize legacy comma-separated string values to arrays before passing to widget.render so pre-existing config populates correctly - install_dependencies_apt.py: update install_via_pip docstring to document both --break-system-packages and --prefer-binary flags Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(web): harden list_calendar_calendars input validation - Remove unused `as e` binding from ValueError/TypeError/KeyError except clause - Replace hasattr(__iter__) with isinstance(list|tuple) so non-sequence returns are rejected before iteration - Validate each calendar entry is a collections.abc.Mapping; skip and warn on malformed items rather than propagating a TypeError - Coerce id/summary to str safely if not already strings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(web): skip calendar entries with empty id in list_calendar_calendars After coercing cal_id to str, check it is non-empty before appending to the calendars list so entries with no usable id are never forwarded to the client. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
3e50fa5b1d |
fix(timezone): use America/New_York instead of EST for ESPN API date queries (#273)
* fix(install): add --prefer-binary to pip installs to avoid /tmp exhaustion timezonefinder (~54 MB) includes large timezone polygon data files that pip unpacks into /tmp during installation. On Raspberry Pi, the default tmpfs /tmp size (often ~half of RAM) can be too small, causing the install to fail with an out-of-space error. Adding --prefer-binary tells pip to prefer pre-built binary wheels over source distributions. Since timezonefinder and most other packages publish wheels on PyPI (and piwheels.org has ARM wheels), this avoids the large temporary /tmp extraction and speeds up installs generally. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(timezone): use America/New_York instead of EST for ESPN API date queries EST is a fixed UTC-5 offset that does not observe daylight saving time, causing the ESPN API date to be off by one hour during EDT (March–November). America/New_York correctly handles DST transitions. The ESPN scoreboard API anchors its schedule calendar to Eastern US time, so this Eastern timezone is intentionally kept for the API date — it is not user-configurable. Game time display is converted separately to the user's configured timezone. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
8ae82321ce |
fix(install): add --prefer-binary to pip installs to avoid /tmp exhaustion (#272)
timezonefinder (~54 MB) includes large timezone polygon data files that pip unpacks into /tmp during installation. On Raspberry Pi, the default tmpfs /tmp size (often ~half of RAM) can be too small, causing the install to fail with an out-of-space error. Adding --prefer-binary tells pip to prefer pre-built binary wheels over source distributions. Since timezonefinder and most other packages publish wheels on PyPI (and piwheels.org has ARM wheels), this avoids the large temporary /tmp extraction and speeds up installs generally. Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
eb143c44fa |
fix(web): render file-upload drop zone for string-type config fields (#271)
* feat: add March Madness plugin and tournament round logos New dedicated March Madness plugin with scrolling tournament ticker: - Fetches NCAA tournament data from ESPN scoreboard API - Shows seeded matchups with team logos, live scores, and round separators - Highlights upsets (higher seed beating lower seed) in gold - Auto-enables during tournament window (March 10 - April 10) - Configurable for NCAAM and NCAAW tournaments - Vegas mode support via get_vegas_content() Tournament round logo assets: - MARCH_MADNESS.png, ROUND_64.png, ROUND_32.png - SWEET_16.png, ELITE_8.png, FINAL_4.png, CHAMPIONSHIP.png Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(store): prevent bulk-update from stalling on bundled/in-repo plugins Three related bugs caused the bulk plugin update to stall at 3/19: 1. Bundled plugins (e.g. starlark-apps, shipped with LEDMatrix rather than the plugin registry) had no metadata file, so update_plugin() returned False → API returned 500 → frontend queue halted. Fix: check for .plugin_metadata.json with install_type=bundled and return True immediately (these plugins update with LEDMatrix itself). 2. git config --get remote.origin.url (without --local) walked up the directory tree and found the parent LEDMatrix repo's remote URL for plugins that live inside plugin-repos/. This caused the store manager to attempt a 60-second git clone of the wrong repo for every update. Fix: use --local to scope the lookup to the plugin directory only. 3. hello-world manifest.json had a trailing comma causing JSON parse errors on every plugin discovery cycle (fixed on devpi directly). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(march-madness): address PR #263 code review findings - Replace self.is_enabled with BasePlugin.self.enabled in update(), display(), and supports_dynamic_duration() so runtime toggles work - Support quarter-based period labels for NCAAW (Q1..Q4 vs H1..H2), detected via league key or status_detail content - Use live refresh interval (60s) for cache max_age during live games instead of hardcoded 300s - Narrow broad except in _load_round_logos to (OSError, ValueError) with a fallback except Exception using logger.exception for traces - Remove unused `situation` local variable from _parse_event() - Add numpy>=1.24.0 to requirements.txt (imported but was missing) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(web): render file-upload drop zone for string-type config fields String fields with x-widget: "file-upload" were falling through to a plain text input because the template only handled the array case. Adds a dedicated drop zone branch for string fields and corresponding handleSingleFileSelect/handleSingleFileUpload JS handlers that POST to the x-upload-config endpoint. Fixes credentials.json upload for the calendar plugin. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(march-madness): address PR #271 code review findings Inline fixes: - manager.py: swap min_duration/max_duration if misconfigured, log warning - manager.py: call session.close() and null session in cleanup() to prevent socket leaks on constrained hardware - manager.py: remove blocking network I/O from display(); update() is the sole fetch path (already uses 60s live-game interval) - manager.py: guard scroll_helper None before create_scrolling_image() in _create_ticker_image() to prevent crash when ScrollHelper is unavailable - store_manager.py: replace bare "except Exception: pass" with debug log including plugin_id and path when reading .plugin_metadata.json - file-upload.js: add endpoint guard (error if uploadEndpoint is falsy), client-side extension validation from data-allowed-extensions, and response.ok check before response.json() in handleSingleFileUpload - plugin_config.html: add data-allowed-extensions attribute to single-file input so JS handler can read the allowed extensions list Nitpick fixes: - manager.py: use logger.exception() (includes traceback) instead of logger.error() for league fetch errors - manager.py: remove redundant "{e}" from logger.exception() calls for round logo and March Madness logo load errors Not fixed (by design): - manifest.json repo naming: monorepo pattern is correct per project docs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(march-madness): address second round of PR #271 code review findings Inline fixes: - requirements.txt: bump Pillow to >=9.1.0 (required for Image.Resampling.LANCZOS) - file-upload.js: replace all statusDiv.innerHTML assignments with safe DOM creation (textContent + createElement) to prevent XSS from untrusted strings - plugin_config.html: add role="button", tabindex="0", aria-label, onkeydown (Enter/Space) to drop zone for keyboard accessibility; add aria-live="polite" to status div for screen-reader announcements - file-upload.js: tighten handleFileDrop endpoint check to non-empty string (dataset.uploadEndpoint.trim() !== '') so an empty attribute falls back to the multi-file handler Nitpick fixes: - manager.py: remove redundant cached_image/cached_array reassignments after create_scrolling_image() which already sets them internally - manager.py: narrow bare except in _get_team_logo to (FileNotFoundError, OSError, ValueError) for expected I/O errors; log unexpected exceptions - store_manager.py: narrow except to (OSError, ValueError) when reading .plugin_metadata.json so unrelated exceptions propagate Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
275fed402e |
fix(logos): support logo downloads for custom soccer leagues (#262)
* 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> * fix(web): preserve array item properties in _set_nested_value When saving config with array-of-objects fields (e.g., custom_leagues), _set_nested_value would replace existing list objects with dicts when navigating dot-notation paths like "custom_leagues.0.name". This destroyed any properties on array items that weren't submitted in the form (e.g., display_modes, game_limits, filtering). Now properly indexes into existing lists when encountering numeric path segments, preserving all non-submitted properties on array items. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(security): address PR #262 code review security findings - logo_downloader: validate league name against allowlist before constructing filesystem paths in get_logo_directory to prevent path traversal (reject anything not matching ^[a-z0-9_-]+$) - logo_downloader: validate league_code against allowlist before interpolating into ESPN API URL in _resolve_api_url to prevent URL path injection; return None on invalid input - api_v3: add MAX_LIST_EXPANSION=1000 cap to _set_nested_value list expansion; raise ValueError for out-of-bounds indices; replace silent break fallback with TypeError for unexpected traversal types Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
38a9c1ed1b |
feat(march-madness): add NCAA tournament plugin and round logos (#263)
* feat: add March Madness plugin and tournament round logos New dedicated March Madness plugin with scrolling tournament ticker: - Fetches NCAA tournament data from ESPN scoreboard API - Shows seeded matchups with team logos, live scores, and round separators - Highlights upsets (higher seed beating lower seed) in gold - Auto-enables during tournament window (March 10 - April 10) - Configurable for NCAAM and NCAAW tournaments - Vegas mode support via get_vegas_content() Tournament round logo assets: - MARCH_MADNESS.png, ROUND_64.png, ROUND_32.png - SWEET_16.png, ELITE_8.png, FINAL_4.png, CHAMPIONSHIP.png Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(store): prevent bulk-update from stalling on bundled/in-repo plugins Three related bugs caused the bulk plugin update to stall at 3/19: 1. Bundled plugins (e.g. starlark-apps, shipped with LEDMatrix rather than the plugin registry) had no metadata file, so update_plugin() returned False → API returned 500 → frontend queue halted. Fix: check for .plugin_metadata.json with install_type=bundled and return True immediately (these plugins update with LEDMatrix itself). 2. git config --get remote.origin.url (without --local) walked up the directory tree and found the parent LEDMatrix repo's remote URL for plugins that live inside plugin-repos/. This caused the store manager to attempt a 60-second git clone of the wrong repo for every update. Fix: use --local to scope the lookup to the plugin directory only. 3. hello-world manifest.json had a trailing comma causing JSON parse errors on every plugin discovery cycle (fixed on devpi directly). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(march-madness): address PR #263 code review findings - Replace self.is_enabled with BasePlugin.self.enabled in update(), display(), and supports_dynamic_duration() so runtime toggles work - Support quarter-based period labels for NCAAW (Q1..Q4 vs H1..H2), detected via league key or status_detail content - Use live refresh interval (60s) for cache max_age during live games instead of hardcoded 300s - Narrow broad except in _load_round_logos to (OSError, ValueError) with a fallback except Exception using logger.exception for traces - Remove unused `situation` local variable from _parse_event() - Add numpy>=1.24.0 to requirements.txt (imported but was missing) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
23f0176c18 |
feat: add dev preview server and CLI render script (#264)
* fix(web): wire up "Check & Update All" plugins button 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: Claude Opus 4.6 <noreply@anthropic.com> * feat: add dev preview server, CLI render script, and visual test display manager Adds local development tools for rapid plugin iteration without deploying to RPi: - VisualTestDisplayManager: renders real pixels via PIL (same fonts/interface as production) - Dev preview server (Flask): interactive web UI with plugin picker, auto-generated config forms, zoom/grid controls, and mock data support for API-dependent plugins - CLI render script: render any plugin to PNG for AI-assisted visual feedback loops - Updated test runner and conftest to auto-detect plugin-repos/ directory Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(dev-preview): address code review issues - Use get_logger() from src.logging_config instead of logging.getLogger() in visual_display_manager.py to match project logging conventions - Eliminate duplicate public/private weather draw methods — public draw_sun/ draw_cloud/draw_rain/draw_snow now delegate to the private _draw_* variants so plugins get consistent pixel output in tests vs production - Default install_deps=False in dev_server.py and render_plugin.py — dev scripts don't need to run pip install; developers are expected to have plugin deps installed in their venv already - Guard plugins_dir fixture against PermissionError during directory iteration - Fix PluginInstallManager.updateAll() to fall back to window.installedPlugins when PluginStateManager.installedPlugins is empty (plugins_manager.js populates window.installedPlugins independently of PluginStateManager) - Remove 5 debug console.log statements from plugins_manager.js button setup and initialization code Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(scroll): fix scroll completion to prevent multi-pass wrapping Change required_total_distance from total_scroll_width + display_width to total_scroll_width alone. The scrolling image already contains display_width pixels of blank initial padding, so reaching total_scroll_width means all content has scrolled off-screen. The extra display_width term was causing 1-2+ unnecessary wrap-arounds, making the same games appear multiple times and producing a black flicker between passes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(dev-preview): address PR #264 code review findings - docs/DEV_PREVIEW.md: add bash language tag to fenced code block - scripts/dev_server.py: add MAX/MIN_WIDTH/HEIGHT constants and validate width/height in render endpoint; add structured logger calls to discover_plugins (missing dirs, hidden entries, missing manifest, JSON/OS errors, duplicate ids); add type annotations to all helpers - scripts/render_plugin.py: add MIN/MAX_DIMENSION validation after parse_args; replace prints with get_logger() calls; narrow broad Exception catches to ImportError/OSError/ValueError in plugin load block; add type annotations to all helpers and main(); rename unused module binding to _module - scripts/run_plugin_tests.py: wrap plugins_path.iterdir() in try/except PermissionError with fallback to plugin-repos/ - scripts/templates/dev_preview.html: replace non-focusable div toggles with button role="switch" + aria-checked; add keyboard handlers (Enter/Space); sync aria-checked in toggleGrid/toggleAutoRefresh - src/common/scroll_helper.py: early-guard zero total_scroll_width to keep scroll_position at 0 and skip completion/wrap logic - src/plugin_system/testing/visual_display_manager.py: forward color arg in draw_cloud -> _draw_cloud; add color param to _draw_cloud; restore _scrolling_state in reset(); narrow broad Exception catches in _load_fonts to FileNotFoundError/OSError/ImportError; add explicit type annotations to draw_text - test/plugins/test_visual_rendering.py: use context manager for Image.open in test_save_snapshot - test/plugins/conftest.py: add return type hints to all fixtures Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: add bandit and gitleaks pre-commit hooks Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
9465fcda6e |
fix(store): fix installed status detection for plugins with path-derived IDs (#270)
The plugin registry uses short IDs (e.g. "weather", "stocks") but plugin_path points to the actual installed directory name (e.g. "plugins/ledmatrix-weather"). isStorePluginInstalled() was only comparing registry IDs, causing all monorepo plugins with mismatched IDs to show as not installed in the store UI. - Updated isStorePluginInstalled() to also check the last segment of plugin_path against installed plugin IDs - Updated all 3 call sites to pass the full plugin object instead of just plugin.id - Fixed the same bug in renderCustomRegistryPlugins() which used the same direct ID comparison Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
976c10c4ac |
fix(plugins): prevent module collision between plugins with shared module names (#265)
When plugins share identically-named local modules (scroll_display.py, game_renderer.py, sports.py), the first plugin to load would populate sys.modules with its version, and subsequent plugins would reuse it instead of loading their own. This caused hockey-scoreboard to use soccer-scoreboard's ScrollDisplay class, which passes unsupported kwargs to ScrollHelper.__init__(), breaking Vegas scroll mode entirely. Fix: evict stale bare-name module entries from sys.modules before each plugin's exec_module, and delete bare entries after namespace isolation so they can't leak to the next plugin. Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
b92ff3dfbd |
fix(schedule): hot-reload config in schedule/dim checks + normalize per-day mode variant (#266)
* 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: use hot-reload config for schedule and dim schedule checks The display controller was caching the config at startup and not picking up changes made via the web UI. Now _check_schedule and _check_dim_schedule read from config_service.get_config() to get the latest configuration, allowing schedule changes to take effect without restarting the service. --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> |
||
|
|
4c4efd614a |
fix(odds): use update_interval as cache TTL and fix live game cache refresh (#268)
* fix(odds): use 2-minute cache for live games instead of 30 minutes Live game odds were being cached for 30 minutes because the cache key didn't trigger the odds_live cache strategy. Added is_live parameter to get_odds() and include 'live' suffix in cache key for live games, which triggers the existing odds_live strategy (2 min TTL). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(base-odds): Use interval as TTL for cache operations - Pass interval variable as TTL to cache_manager.set() calls - Ensures cache expires after update interval, preventing stale data - Removes dead code by actually using the computed interval value * refactor(base-odds): Remove is_live parameter from base class for modularity - Remove is_live parameter from get_odds() method signature - Remove cache key modification logic from base class - Remove is_live handling from get_odds_for_games() - Keep base class minimal and generic for reuse by other plugins - Plugin-specific is_live logic moved to odds-ticker plugin override --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> |
||
|
|
14b6a0c6a3 |
fix(web): handle dotted keys in schema/config path helpers (#260)
* fix(web): handle dotted keys in schema/config path helpers Schema property names containing dots (e.g. "eng.1" for Premier League in soccer-scoreboard) were being incorrectly split on the dot separator in two path-navigation helpers: - _get_schema_property: split "leagues.eng.1.favorite_teams" into 4 segments and looked for "eng" in leagues.properties, which doesn't exist (the key is literally "eng.1"). Returned None, so the field type was unknown and values were not parsed correctly. - _set_nested_value: split the same path into 4 segments and created config["leagues"]["eng"]["1"]["favorite_teams"] instead of the correct config["leagues"]["eng.1"]["favorite_teams"]. Both functions now use a greedy longest-match approach: at each level they try progressively longer dot-joined candidates first (e.g. "eng.1" before "eng"), so dotted property names are handled transparently. Fixes favorite_teams (and other per-league fields) not saving via the soccer-scoreboard plugin config UI. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove debug artifacts from merged branches - Replace print() with logger.warning() for three error handlers in api_v3.py that bypassed the structured logging infrastructure - Simplify dead if/else in loadInstalledPlugins() — both branches did the same window.installedPlugins assignment; collapse to single line - Remove console.log registration line from schedule-picker widget that fired unconditionally on every page load Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
c2763d6447 |
Update Waveshare display information in README (#259)
Signed-off-by: Chuck <33324927+ChuckBuilds@users.noreply.github.com> |
||
|
|
1f0de9b354 |
fix(starlark): fix Python 3.13 importlib.reload() incompatibility (#258)
* fix(starlark): fix Python 3.13 importlib.reload() incompatibility In Python 3.13, importlib.reload() raises ModuleNotFoundError for modules loaded via spec_from_file_location when they aren't on sys.path, because _bootstrap._find_spec() can no longer resolve them by name. Replace the reload-on-cache-hit pattern in _get_tronbyte_repository_class() and _get_pixlet_renderer_class() with a simple return of the cached class — the reload was only useful for dev-time iteration and is unnecessary in production (the service restarts clean on each deploy). Also broaden the exception catch in upload_starlark_app() from (ValueError, OSError, IOError) to Exception so that any unexpected error (ImportError, ModuleNotFoundError, etc.) returns a proper JSON response instead of an unhandled Flask 500. Fixes: "Install failed: spec not found for the module 'tronbyte_repository'" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(starlark): use targeted exception handlers in upload_starlark_app() Replace the broad `except Exception` catch-all with specific handlers: - (OSError, IOError) for temp file creation/save failures - ImportError for module loading failures (_get_pixlet_renderer_class) - Exception as final catch-all that logs without leaking internals All handlers use `err` (not unused `e`) in both the log message and the JSON response body. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
ed90654bf2 |
fix(cache): move odds key check before live/scoreboard in get_data_type_from_key (#256)
* fix(cache): move odds key check before live/scoreboard check in get_data_type_from_key Cache keys like odds_espn_nba_game_123_live contain 'live', so they were matched by the generic ['live', 'current', 'scoreboard'] branch (sports_live, 30s TTL) before the 'odds' branch was ever reached. This caused live odds to expire every 30 seconds instead of every 120 seconds, hitting the ESPN odds API 4x more often than intended and risking rate-limiting. Fix: move the 'odds' check above the 'live'/'current'/'scoreboard' check so the more-specific prefix wins. No regressions: pure live_*/scoreboard_* keys (without 'odds') still route to sports_live. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(cache): remove dead soccer branch in get_data_type_from_key The inner `if 'soccer' in key_lower: return 'sports_live'` branch was dead code — both the soccer and non-soccer paths returned the same 'sports_live' value. Collapse to a single return statement. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
302235a357 |
feat: Starlark Apps Integration with Schema-Driven Config + Security Hardening (#253)
* 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> * 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> * 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> * 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> * 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> * 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> * 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> * 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> * 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> * 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> * 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> * 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> * 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> * fix(starlark): add List to typing imports for schema parser Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * 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> * 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> * 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> * 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> * 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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |